network/pkg/client/network_client.go
2026-01-20 10:03:55 +02:00

271 lines
7.2 KiB
Go

package client
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/multiformats/go-multiaddr"
)
// NetworkInfoImpl implements NetworkInfo
type NetworkInfoImpl struct {
client *Client
}
// GetPeers returns information about connected peers
func (n *NetworkInfoImpl) GetPeers(ctx context.Context) ([]PeerInfo, error) {
if !n.client.isConnected() {
return nil, fmt.Errorf("client not connected")
}
if err := n.client.requireAccess(ctx); err != nil {
return nil, fmt.Errorf("authentication required: %w - run CLI commands to authenticate automatically", err)
}
// Get peers from LibP2P host
host := n.client.host
if host == nil {
return nil, fmt.Errorf("no host available")
}
// Get connected peers
connectedPeers := host.Network().Peers()
peers := make([]PeerInfo, 0, len(connectedPeers)+1) // +1 for self
// Add connected peers
for _, peerID := range connectedPeers {
// Get peer addresses
peerInfo := host.Peerstore().PeerInfo(peerID)
// Convert multiaddrs to strings
addrs := make([]string, len(peerInfo.Addrs))
for i, addr := range peerInfo.Addrs {
addrs[i] = addr.String()
}
peers = append(peers, PeerInfo{
ID: peerID.String(),
Addresses: addrs,
Connected: true,
LastSeen: time.Now(), // LibP2P doesn't track last seen, so use current time
})
}
// Add self node
selfPeerInfo := host.Peerstore().PeerInfo(host.ID())
selfAddrs := make([]string, len(selfPeerInfo.Addrs))
for i, addr := range selfPeerInfo.Addrs {
selfAddrs[i] = addr.String()
}
// Insert self node at the beginning of the list
selfPeer := PeerInfo{
ID: host.ID().String(),
Addresses: selfAddrs,
Connected: true,
LastSeen: time.Now(),
}
// Prepend self to the list
peers = append([]PeerInfo{selfPeer}, peers...)
return peers, nil
}
// GetStatus returns network status
func (n *NetworkInfoImpl) GetStatus(ctx context.Context) (*NetworkStatus, error) {
if !n.client.isConnected() {
return nil, fmt.Errorf("client not connected")
}
if err := n.client.requireAccess(ctx); err != nil {
return nil, fmt.Errorf("authentication required: %w - run CLI commands to authenticate automatically", err)
}
host := n.client.host
if host == nil {
return nil, fmt.Errorf("no host available")
}
// Get actual network status
connectedPeers := host.Network().Peers()
// Try to get database size from RQLite (optional - don't fail if unavailable)
var dbSize int64 = 0
dbClient := n.client.database
if conn, err := dbClient.getRQLiteConnection(); err == nil {
// Query database size (rough estimate)
if result, err := conn.QueryOne("SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()"); err == nil {
for result.Next() {
if row, err := result.Slice(); err == nil && len(row) > 0 {
if size, ok := row[0].(int64); ok {
dbSize = size
}
}
}
}
}
// Try to get IPFS peer info (optional - don't fail if unavailable)
ipfsInfo := queryIPFSPeerInfo()
// Try to get IPFS Cluster peer info (optional - don't fail if unavailable)
ipfsClusterInfo := queryIPFSClusterPeerInfo()
return &NetworkStatus{
NodeID: host.ID().String(),
PeerID: host.ID().String(),
Connected: true,
PeerCount: len(connectedPeers),
DatabaseSize: dbSize,
Uptime: time.Since(n.client.startTime),
IPFS: ipfsInfo,
IPFSCluster: ipfsClusterInfo,
}, nil
}
// queryIPFSPeerInfo queries the local IPFS API for peer information
// Returns nil if IPFS is not running or unavailable
func queryIPFSPeerInfo() *IPFSPeerInfo {
// IPFS API typically runs on port 4501 in our setup
client := &http.Client{Timeout: 2 * time.Second}
resp, err := client.Post("http://localhost:4501/api/v0/id", "", nil)
if err != nil {
return nil // IPFS not available
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil
}
var result struct {
ID string `json:"ID"`
Addresses []string `json:"Addresses"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil
}
// Filter addresses to only include public/routable ones
var swarmAddrs []string
for _, addr := range result.Addresses {
// Skip loopback and private addresses for external discovery
if !strings.Contains(addr, "127.0.0.1") && !strings.Contains(addr, "/ip6/::1") {
swarmAddrs = append(swarmAddrs, addr)
}
}
return &IPFSPeerInfo{
PeerID: result.ID,
SwarmAddresses: swarmAddrs,
}
}
// queryIPFSClusterPeerInfo queries the local IPFS Cluster API for peer information
// Returns nil if IPFS Cluster is not running or unavailable
func queryIPFSClusterPeerInfo() *IPFSClusterPeerInfo {
// IPFS Cluster API typically runs on port 9094 in our setup
client := &http.Client{Timeout: 2 * time.Second}
resp, err := client.Get("http://localhost:9094/id")
if err != nil {
return nil // IPFS Cluster not available
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil
}
var result struct {
ID string `json:"id"`
Addresses []string `json:"addresses"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil
}
// Filter addresses to only include public/routable ones for cluster discovery
var clusterAddrs []string
for _, addr := range result.Addresses {
// Skip loopback addresses - only keep routable addresses
if !strings.Contains(addr, "127.0.0.1") && !strings.Contains(addr, "/ip6/::1") {
clusterAddrs = append(clusterAddrs, addr)
}
}
return &IPFSClusterPeerInfo{
PeerID: result.ID,
Addresses: clusterAddrs,
}
}
// ConnectToPeer connects to a specific peer
func (n *NetworkInfoImpl) ConnectToPeer(ctx context.Context, peerAddr string) error {
if !n.client.isConnected() {
return fmt.Errorf("client not connected")
}
if err := n.client.requireAccess(ctx); err != nil {
return fmt.Errorf("authentication required: %w - run CLI commands to authenticate automatically", err)
}
host := n.client.host
if host == nil {
return fmt.Errorf("no host available")
}
// Parse the multiaddr
ma, err := multiaddr.NewMultiaddr(peerAddr)
if err != nil {
return fmt.Errorf("invalid multiaddr: %w", err)
}
// Extract peer info
peerInfo, err := peer.AddrInfoFromP2pAddr(ma)
if err != nil {
return fmt.Errorf("failed to extract peer info: %w", err)
}
// Connect to the peer
if err := host.Connect(ctx, *peerInfo); err != nil {
return fmt.Errorf("failed to connect to peer: %w", err)
}
return nil
}
// DisconnectFromPeer disconnects from a specific peer
func (n *NetworkInfoImpl) DisconnectFromPeer(ctx context.Context, peerID string) error {
if !n.client.isConnected() {
return fmt.Errorf("client not connected")
}
if err := n.client.requireAccess(ctx); err != nil {
return fmt.Errorf("authentication required: %w - run CLI commands to authenticate automatically", err)
}
host := n.client.host
if host == nil {
return fmt.Errorf("no host available")
}
// Parse the peer ID
pid, err := peer.Decode(peerID)
if err != nil {
return fmt.Errorf("invalid peer ID: %w", err)
}
// Close the connection to the peer
if err := host.Network().ClosePeer(pid); err != nil {
return fmt.Errorf("failed to disconnect from peer: %w", err)
}
return nil
}