heroagent/pkg/clients/ mycelium/client.go
2025-04-23 04:18:28 +02:00

429 lines
11 KiB
Go

// pkg/mycelium_client/client.go
package mycelium_client
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"time"
)
// DefaultAPIPort is the default port on which the Mycelium HTTP API listens
const DefaultAPIPort = 8989
// Default timeout values
const (
DefaultClientTimeout = 30 * time.Second
DefaultReplyTimeout = 60 // seconds
DefaultReceiveWait = 10 // seconds
)
// MyceliumClient represents a client for interacting with the Mycelium API
type MyceliumClient struct {
BaseURL string
HTTPClient *http.Client
}
// NewClient creates a new Mycelium client with the given base URL
// If baseURL is empty, it defaults to "http://localhost:8989"
func NewClient(baseURL string) *MyceliumClient {
if baseURL == "" {
baseURL = fmt.Sprintf("http://localhost:%d", DefaultAPIPort)
}
return &MyceliumClient{
BaseURL: baseURL,
HTTPClient: &http.Client{Timeout: DefaultClientTimeout},
}
}
// SetTimeout sets the HTTP client timeout
func (c *MyceliumClient) SetTimeout(timeout time.Duration) {
c.HTTPClient.Timeout = timeout
}
// Message Structures
// MessageDestination represents a destination for a message, either by IP or public key
type MessageDestination struct {
IP string `json:"ip,omitempty"` // IPv6 address in the overlay network
PK string `json:"pk,omitempty"` // Public key hex encoded
}
// PushMessage represents a message to be sent
type PushMessage struct {
Dst MessageDestination `json:"dst"`
Topic string `json:"topic,omitempty"`
Payload string `json:"payload"` // Base64 encoded
}
// InboundMessage represents a received message
type InboundMessage struct {
ID string `json:"id"`
SrcIP string `json:"srcIp"`
SrcPK string `json:"srcPk"`
DstIP string `json:"dstIp"`
DstPK string `json:"dstPk"`
Topic string `json:"topic,omitempty"`
Payload string `json:"payload"` // Base64 encoded
}
// MessageResponse represents the ID of a pushed message
type MessageResponse struct {
ID string `json:"id"`
}
// NodeInfo represents general information about the Mycelium node
type NodeInfo struct {
NodeSubnet string `json:"nodeSubnet"`
}
// PeerStats represents statistics about a peer
type PeerStats struct {
Endpoint Endpoint `json:"endpoint"`
Type string `json:"type"` // static, inbound, linkLocalDiscovery
ConnectionState string `json:"connectionState"` // alive, connecting, dead
TxBytes int64 `json:"txBytes,omitempty"`
RxBytes int64 `json:"rxBytes,omitempty"`
}
// Endpoint represents connection information for a peer
type Endpoint struct {
Proto string `json:"proto"` // tcp, quic
SocketAddr string `json:"socketAddr"` // IP:port
}
// Route represents a network route
type Route struct {
Subnet string `json:"subnet"`
NextHop string `json:"nextHop"`
Metric interface{} `json:"metric"` // Can be int or string "infinite"
Seqno int `json:"seqno"`
}
// Decode decodes the base64 payload of an inbound message
func (m *InboundMessage) Decode() ([]byte, error) {
return base64.StdEncoding.DecodeString(m.Payload)
}
// GetNodeInfo retrieves general information about the Mycelium node
func (c *MyceliumClient) GetNodeInfo(ctx context.Context) (*NodeInfo, error) {
req, err := http.NewRequestWithContext(ctx, "GET", c.BaseURL+"/api/v1/admin", nil)
if err != nil {
return nil, err
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := ioutil.ReadAll(resp.Body)
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
}
var info NodeInfo
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
return nil, err
}
return &info, nil
}
// SendMessage sends a message to a specified destination
// If waitForReply is true, it will wait for a reply up to the specified timeout
func (c *MyceliumClient) SendMessage(ctx context.Context, dst MessageDestination, payload []byte, topic string, waitForReply bool, replyTimeout int) (*InboundMessage, string, error) {
// Encode payload to base64
encodedPayload := base64.StdEncoding.EncodeToString(payload)
msg := PushMessage{
Dst: dst,
Topic: topic,
Payload: encodedPayload,
}
reqBody, err := json.Marshal(msg)
if err != nil {
return nil, "", err
}
// Build URL with optional reply_timeout
url := fmt.Sprintf("%s/api/v1/messages", c.BaseURL)
if waitForReply && replyTimeout > 0 {
url = fmt.Sprintf("%s?reply_timeout=%d", url, replyTimeout)
}
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(reqBody))
if err != nil {
return nil, "", err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, "", err
}
defer resp.Body.Close()
// Check for error status codes
if resp.StatusCode >= 400 {
body, _ := ioutil.ReadAll(resp.Body)
return nil, "", fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
}
// If we got a reply (status 200)
if resp.StatusCode == http.StatusOK && waitForReply {
var reply InboundMessage
if err := json.NewDecoder(resp.Body).Decode(&reply); err != nil {
return nil, "", err
}
return &reply, "", nil
}
// If we just got a message ID (status 201)
var result MessageResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, "", err
}
return nil, result.ID, nil
}
// ReplyToMessage sends a reply to a previously received message
func (c *MyceliumClient) ReplyToMessage(ctx context.Context, msgID string, payload []byte, topic string) error {
encodedPayload := base64.StdEncoding.EncodeToString(payload)
msg := PushMessage{
Dst: MessageDestination{}, // Not needed for replies
Topic: topic,
Payload: encodedPayload,
}
reqBody, err := json.Marshal(msg)
if err != nil {
return err
}
url := fmt.Sprintf("%s/api/v1/messages/reply/%s", c.BaseURL, msgID)
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(reqBody))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.HTTPClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
body, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
}
return nil
}
// ReceiveMessage waits for and receives a message, optionally filtering by topic
// If timeout is 0, it will return immediately if no message is available
func (c *MyceliumClient) ReceiveMessage(ctx context.Context, timeout int, topic string, peek bool) (*InboundMessage, error) {
params := url.Values{}
if timeout > 0 {
params.Add("timeout", fmt.Sprintf("%d", timeout))
}
if topic != "" {
params.Add("topic", topic)
}
if peek {
params.Add("peek", "true")
}
url := fmt.Sprintf("%s/api/v1/messages?%s", c.BaseURL, params.Encode())
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// No message available
if resp.StatusCode == http.StatusNoContent {
return nil, nil
}
if resp.StatusCode != http.StatusOK {
body, _ := ioutil.ReadAll(resp.Body)
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
}
var msg InboundMessage
if err := json.NewDecoder(resp.Body).Decode(&msg); err != nil {
return nil, err
}
return &msg, nil
}
// GetMessageStatus checks the status of a previously sent message
func (c *MyceliumClient) GetMessageStatus(ctx context.Context, msgID string) (map[string]interface{}, error) {
url := fmt.Sprintf("%s/api/v1/messages/status/%s", c.BaseURL, msgID)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := ioutil.ReadAll(resp.Body)
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
}
var status map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
return nil, err
}
return status, nil
}
// ListPeers retrieves a list of known peers
func (c *MyceliumClient) ListPeers(ctx context.Context) ([]PeerStats, error) {
req, err := http.NewRequestWithContext(ctx, "GET", c.BaseURL+"/api/v1/admin/peers", nil)
if err != nil {
return nil, err
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := ioutil.ReadAll(resp.Body)
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
}
var peers []PeerStats
if err := json.NewDecoder(resp.Body).Decode(&peers); err != nil {
return nil, err
}
return peers, nil
}
// AddPeer adds a new peer to the network
func (c *MyceliumClient) AddPeer(ctx context.Context, endpoint string) error {
// The API expects a direct endpoint string, not a JSON object
reqBody := []byte(endpoint)
req, err := http.NewRequestWithContext(ctx, "POST", c.BaseURL+"/api/v1/admin/peers", bytes.NewBuffer(reqBody))
if err != nil {
return err
}
req.Header.Set("Content-Type", "text/plain")
resp, err := c.HTTPClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
body, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
}
return nil
}
// RemovePeer removes a peer from the network
func (c *MyceliumClient) RemovePeer(ctx context.Context, endpoint string) error {
url := fmt.Sprintf("%s/api/v1/admin/peers/%s", c.BaseURL, url.PathEscape(endpoint))
req, err := http.NewRequestWithContext(ctx, "DELETE", url, nil)
if err != nil {
return err
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
body, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
}
return nil
}
// ListSelectedRoutes retrieves a list of selected routes
func (c *MyceliumClient) ListSelectedRoutes(ctx context.Context) ([]Route, error) {
req, err := http.NewRequestWithContext(ctx, "GET", c.BaseURL+"/api/v1/admin/routes/selected", nil)
if err != nil {
return nil, err
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := ioutil.ReadAll(resp.Body)
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
}
var routes []Route
if err := json.NewDecoder(resp.Body).Decode(&routes); err != nil {
return nil, err
}
return routes, nil
}
// ListFallbackRoutes retrieves a list of fallback routes
func (c *MyceliumClient) ListFallbackRoutes(ctx context.Context) ([]Route, error) {
req, err := http.NewRequestWithContext(ctx, "GET", c.BaseURL+"/api/v1/admin/routes/fallback", nil)
if err != nil {
return nil, err
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := ioutil.ReadAll(resp.Body)
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
}
var routes []Route
if err := json.NewDecoder(resp.Body).Decode(&routes); err != nil {
return nil, err
}
return routes, nil
}