package main import ( "context" "crypto/rand" "encoding/json" "fmt" "log" "net/http" "os" "os/signal" "syscall" "time" "github.com/libp2p/go-libp2p" "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/p2p/transport/websocket" "github.com/multiformats/go-multiaddr" ) var ( p2pHost host.Host ) // PeerInfo contains information about this P2P node type PeerInfo struct { PeerID string `json:"peerId"` Addresses []string `json:"addresses"` } // HealthResponse for health check type HealthResponse struct { Status string `json:"status"` PeerID string `json:"peerId,omitempty"` Peers int `json:"connectedPeers"` Addresses []string `json:"listenAddresses"` } func main() { port := os.Getenv("PORT") if port == "" { port = "8080" } // Initialize libp2p host if err := initP2P(); err != nil { log.Fatalf("Failed to initialize P2P: %v", err) } // Setup HTTP routes mux := http.NewServeMux() mux.HandleFunc("/health", healthHandler) mux.HandleFunc("/healthz", healthHandler) mux.HandleFunc("/peer-info", peerInfoHandler) mux.HandleFunc("/connect", connectHandler) mux.HandleFunc("/peers", peersHandler) // CORS middleware handler := corsMiddleware(mux) server := &http.Server{ Addr: ":" + port, Handler: handler, } // Graceful shutdown go func() { sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) <-sigChan log.Println("Shutting down...") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if p2pHost != nil { p2pHost.Close() } server.Shutdown(ctx) }() log.Printf("IPFS/P2P Service starting on port %s", port) if p2pHost != nil { log.Printf("Peer ID: %s", p2pHost.ID().String()) for _, addr := range p2pHost.Addrs() { log.Printf("Listening on: %s/p2p/%s", addr, p2pHost.ID()) } } if err := server.ListenAndServe(); err != http.ErrServerClosed { log.Fatalf("Server error: %v", err) } } func initP2P() error { // Generate a new identity for this node priv, _, err := crypto.GenerateKeyPairWithReader(crypto.Ed25519, -1, rand.Reader) if err != nil { return fmt.Errorf("failed to generate key pair: %w", err) } // Create libp2p host with WebSocket transport for browser compatibility h, err := libp2p.New( libp2p.Identity(priv), libp2p.ListenAddrStrings( "/ip4/0.0.0.0/tcp/4001", "/ip4/0.0.0.0/tcp/4002/ws", ), libp2p.Transport(websocket.New), libp2p.NATPortMap(), libp2p.EnableRelay(), ) if err != nil { return fmt.Errorf("failed to create libp2p host: %w", err) } p2pHost = h return nil } func corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") if origin == "" { origin = "*" } w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") w.Header().Set("Access-Control-Allow-Credentials", "true") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } next.ServeHTTP(w, r) }) } func healthHandler(w http.ResponseWriter, r *http.Request) { response := HealthResponse{ Status: "healthy", Addresses: []string{}, } if p2pHost != nil { response.PeerID = p2pHost.ID().String() response.Peers = len(p2pHost.Network().Peers()) for _, addr := range p2pHost.Addrs() { response.Addresses = append(response.Addresses, addr.String()) } } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } func peerInfoHandler(w http.ResponseWriter, r *http.Request) { info := PeerInfo{ Addresses: []string{}, } if p2pHost != nil { info.PeerID = p2pHost.ID().String() for _, addr := range p2pHost.Addrs() { fullAddr := fmt.Sprintf("%s/p2p/%s", addr, p2pHost.ID()) info.Addresses = append(info.Addresses, fullAddr) } } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(info) } func peersHandler(w http.ResponseWriter, r *http.Request) { peers := []string{} if p2pHost != nil { for _, p := range p2pHost.Network().Peers() { peers = append(peers, p.String()) } } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "count": len(peers), "peers": peers, }) } func connectHandler(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var request struct { PeerAddr string `json:"peerAddr"` } if err := json.NewDecoder(r.Body).Decode(&request); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if p2pHost == nil { http.Error(w, "P2P host not available", http.StatusServiceUnavailable) return } ma, err := multiaddr.NewMultiaddr(request.PeerAddr) if err != nil { http.Error(w, fmt.Sprintf("Invalid multiaddr: %v", err), http.StatusBadRequest) return } peerInfo, err := peer.AddrInfoFromP2pAddr(ma) if err != nil { http.Error(w, fmt.Sprintf("Invalid peer address: %v", err), http.StatusBadRequest) return } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := p2pHost.Connect(ctx, *peerInfo); err != nil { http.Error(w, fmt.Sprintf("Failed to connect: %v", err), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "status": "connected", "peerId": peerInfo.ID.String(), }) }