241 lines
5.6 KiB
Go
241 lines
5.6 KiB
Go
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(),
|
|
})
|
|
}
|