diff --git a/cmd/gateway/main.go b/cmd/gateway/main.go index b624cef..bbd9e96 100644 --- a/cmd/gateway/main.go +++ b/cmd/gateway/main.go @@ -89,12 +89,15 @@ func main() { // Register API routes with /api prefix apiRouter := r.PathPrefix("/api").Subrouter() - // Register tracker routes on main router (no /api prefix for BitTorrent compatibility) - api.RegisterTrackerRoutes(r, cfg, storageBackend) - - // Register main API routes and get gateway instance + // Register main API routes and get gateway instance first gatewayInstance = api.RegisterRoutes(apiRouter, cfg, storageBackend) + // Register tracker routes on main router (no /api prefix for BitTorrent compatibility) + wsTracker := api.RegisterTrackerRoutes(r, cfg, storageBackend, gatewayInstance) + if wsTracker != nil { + gatewayInstance.SetWebSocketTracker(wsTracker) + } + // Serve static files webFS := web.GetFS() staticFS, _ := fs.Sub(webFS, "static") diff --git a/internal/admin/handlers.go b/internal/admin/handlers.go index caf1327..2a80456 100644 --- a/internal/admin/handlers.go +++ b/internal/admin/handlers.go @@ -15,6 +15,21 @@ import ( "github.com/gorilla/mux" ) +// TrackerInterface defines methods needed from the tracker +type TrackerInterface interface { + GetStats() map[string]interface{} +} + +// WebSocketTrackerInterface defines methods needed from the WebSocket tracker +type WebSocketTrackerInterface interface { + GetStats() map[string]interface{} +} + +// DHTInterface defines methods needed from the DHT +type DHTInterface interface { + GetStats() map[string]interface{} +} + // GatewayInterface defines the methods needed from the gateway type GatewayInterface interface { GetDB() *sql.DB @@ -23,6 +38,9 @@ type GatewayInterface interface { CleanupOrphanedChunks() (map[string]interface{}, error) CleanupInactiveUsers(days int) (map[string]interface{}, error) ReconstructTorrentFile(fileHash, fileName string) (string, error) + GetTrackerInstance() TrackerInterface + GetWebSocketTracker() WebSocketTrackerInterface + GetDHTNode() DHTInterface } // TranscodingManager interface for transcoding operations @@ -1243,11 +1261,35 @@ func (ah *AdminHandlers) gatherStreamingAnalytics(db *sql.DB) map[string]interfa func (ah *AdminHandlers) gatherP2PHealthMetrics() map[string]interface{} { p2p := make(map[string]interface{}) - // P2P health score calculation (mock implementation) - p2p["health_score"] = 87 - p2p["dht_node_count"] = 1249 - p2p["webseed_hit_ratio"] = "91.2%" - p2p["torrent_completion_avg"] = "2.1 mins" + // Get real P2P stats from tracker instance + if tracker := ah.gateway.GetTrackerInstance(); tracker != nil { + trackerStats := tracker.GetStats() + if peers, ok := trackerStats["peers"]; ok { + p2p["active_peers"] = peers + } + } + + // Get real DHT stats + if dht := ah.gateway.GetDHTNode(); dht != nil { + dhtStats := dht.GetStats() + if nodeCount, ok := dhtStats["routing_table_size"]; ok { + p2p["dht_node_count"] = nodeCount + } + if torrents, ok := dhtStats["torrents"]; ok { + p2p["dht_torrents"] = torrents + } + } + + // Get WebSocket tracker stats + if wsTracker := ah.gateway.GetWebSocketTracker(); wsTracker != nil { + wsStats := wsTracker.GetStats() + if swarms, ok := wsStats["swarms"]; ok { + p2p["websocket_swarms"] = swarms + } + if peers, ok := wsStats["peers"]; ok { + p2p["websocket_peers"] = peers + } + } return p2p } @@ -1256,12 +1298,14 @@ func (ah *AdminHandlers) gatherP2PHealthMetrics() map[string]interface{} { func (ah *AdminHandlers) gatherWebTorrentStats() map[string]interface{} { webtorrent := make(map[string]interface{}) - // WebTorrent peer statistics (mock data) - webtorrent["peers_active"] = 45 - webtorrent["browser_client_types"] = map[string]int{ - "Chrome": 60, - "Firefox": 25, - "Safari": 15, + // Get real WebSocket tracker stats (WebTorrent uses WebSocket tracker) + if wsTracker := ah.gateway.GetWebSocketTracker(); wsTracker != nil { + wsStats := wsTracker.GetStats() + webtorrent["active_swarms"] = wsStats["swarms"] + webtorrent["connected_peers"] = wsStats["peers"] + } else { + webtorrent["active_swarms"] = 0 + webtorrent["connected_peers"] = 0 } return webtorrent diff --git a/internal/api/auth_handlers.go b/internal/api/auth_handlers.go index df800e1..c3bd1e0 100644 --- a/internal/api/auth_handlers.go +++ b/internal/api/auth_handlers.go @@ -52,12 +52,13 @@ type UserStatsResponse struct { // UserFile represents a file in user's file list type UserFile struct { - Hash string `json:"hash"` - Name string `json:"name"` - Size int64 `json:"size"` - StorageType string `json:"storage_type"` - AccessLevel string `json:"access_level"` - UploadedAt string `json:"uploaded_at"` + Hash string `json:"hash"` + Name string `json:"name"` + Size int64 `json:"size"` + StorageType string `json:"storage_type"` + AccessLevel string `json:"access_level"` + UploadedAt string `json:"uploaded_at"` + TorrentInfoHash string `json:"torrent_info_hash,omitempty"` } // LoginHandler handles user authentication @@ -253,12 +254,13 @@ func (ah *AuthHandlers) UserFilesHandler(w http.ResponseWriter, r *http.Request) if files != nil { for _, file := range files { userFiles = append(userFiles, UserFile{ - Hash: file.Hash, - Name: file.OriginalName, - Size: file.Size, - StorageType: file.StorageType, - AccessLevel: file.AccessLevel, - UploadedAt: file.CreatedAt.Format(time.RFC3339), + Hash: file.Hash, + Name: file.OriginalName, + Size: file.Size, + StorageType: file.StorageType, + AccessLevel: file.AccessLevel, + UploadedAt: file.CreatedAt.Format(time.RFC3339), + TorrentInfoHash: file.InfoHash, }) } } diff --git a/internal/api/handlers.go b/internal/api/handlers.go index feaafe1..52fa675 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -112,6 +112,8 @@ type Gateway struct { publicURL string trackerInstance *tracker.Tracker dhtBootstrap DHTBootstrap + dhtNode *dht.DHTBootstrap // Add actual DHT instance + wsTracker *tracker.WebSocketTracker // Add WebSocket tracker instance transcodingManager TranscodingManager } @@ -330,11 +332,14 @@ type ChunkInfo struct { } type UploadResponse struct { - FileHash string `json:"file_hash"` - Message string `json:"message"` - TorrentHash string `json:"torrent_hash,omitempty"` - MagnetLink string `json:"magnet_link,omitempty"` + FileHash string `json:"file_hash"` + Message string `json:"message"` + TorrentHash string `json:"torrent_hash,omitempty"` + MagnetLink string `json:"magnet_link,omitempty"` NostrEventID string `json:"nostr_event_id,omitempty"` + NIP71EventID string `json:"nip71_event_id,omitempty"` + StreamingURL string `json:"streaming_url,omitempty"` + HLSURL string `json:"hls_url,omitempty"` } func NewGateway(cfg *config.Config, storage *storage.Backend) *Gateway { @@ -641,6 +646,39 @@ func (g *Gateway) GetAllTorrentHashes() []string { // SetDHTBootstrap sets the DHT bootstrap instance for torrent announcements func (g *Gateway) SetDHTBootstrap(dhtBootstrap DHTBootstrap) { g.dhtBootstrap = dhtBootstrap + // Also store the actual DHT node instance + if dhtBootstrapConcrete, ok := dhtBootstrap.(*dht.DHTBootstrap); ok { + g.dhtNode = dhtBootstrapConcrete + } +} + +// SetWebSocketTracker sets the WebSocket tracker instance for stats collection +func (g *Gateway) SetWebSocketTracker(wsTracker *tracker.WebSocketTracker) { + g.wsTracker = wsTracker +} + +// GetTrackerInstance returns the tracker instance for admin interface +func (g *Gateway) GetTrackerInstance() admin.TrackerInterface { + if g.trackerInstance == nil { + return nil + } + return g.trackerInstance +} + +// GetWebSocketTracker returns the WebSocket tracker instance for admin interface +func (g *Gateway) GetWebSocketTracker() admin.WebSocketTrackerInterface { + if g.wsTracker == nil { + return nil + } + return g.wsTracker +} + +// GetDHTNode returns the DHT node instance for admin interface +func (g *Gateway) GetDHTNode() admin.DHTInterface { + if g.dhtNode == nil { + return nil + } + return g.dhtNode } func (g *Gateway) UploadHandler(w http.ResponseWriter, r *http.Request) { @@ -818,6 +856,7 @@ func (g *Gateway) handleBlobUpload(w http.ResponseWriter, r *http.Request, file // Publish to Nostr for blobs var nostrEventID string + var nip71EventID string if g.nostrPublisher != nil { eventData := nostr.TorrentEventData{ Title: fmt.Sprintf("File: %s", fileName), @@ -859,6 +898,7 @@ func (g *Gateway) handleBlobUpload(w http.ResponseWriter, r *http.Request, file fmt.Printf("Warning: Failed to publish NIP-71 video event: %v\n", err) } else { fmt.Printf("Published NIP-71 video event: %s\n", nip71Event.ID) + nip71EventID = nip71Event.ID } } } @@ -875,6 +915,13 @@ func (g *Gateway) handleBlobUpload(w http.ResponseWriter, r *http.Request, file FileHash: metadata.Hash, Message: "File uploaded successfully as blob", NostrEventID: nostrEventID, + NIP71EventID: nip71EventID, + } + + // Add streaming URL if it's a video + if streamingInfo != nil { + response.StreamingURL = fmt.Sprintf("%s/api/stream/%s", g.getBaseURL(), metadata.Hash) + response.HLSURL = fmt.Sprintf("%s/api/stream/%s/playlist.m3u8", g.getBaseURL(), metadata.Hash) } w.Header().Set("Content-Type", "application/json") @@ -1052,6 +1099,7 @@ func (g *Gateway) handleTorrentUpload(w http.ResponseWriter, r *http.Request, fi // Publish to Nostr var nostrEventID string + var nip71EventID string if g.nostrPublisher != nil { eventData := nostr.TorrentEventData{ Title: fmt.Sprintf("Torrent: %s", fileName), @@ -1096,6 +1144,7 @@ func (g *Gateway) handleTorrentUpload(w http.ResponseWriter, r *http.Request, fi fmt.Printf("Warning: Failed to publish NIP-71 video event: %v\n", err) } else { fmt.Printf("Published NIP-71 video event: %s\n", nip71Event.ID) + nip71EventID = nip71Event.ID } } } @@ -1124,6 +1173,13 @@ func (g *Gateway) handleTorrentUpload(w http.ResponseWriter, r *http.Request, fi TorrentHash: torrentInfo.InfoHash, MagnetLink: torrentInfo.Magnet, NostrEventID: nostrEventID, + NIP71EventID: nip71EventID, + } + + // Add streaming URL if it's a video + if streamingInfo != nil { + response.StreamingURL = fmt.Sprintf("%s/api/stream/%s", g.getBaseURL(), metadata.Hash) + response.HLSURL = fmt.Sprintf("%s/api/stream/%s/playlist.m3u8", g.getBaseURL(), metadata.Hash) } w.Header().Set("Content-Type", "application/json") @@ -2247,24 +2303,37 @@ func (g *Gateway) P2PStatsHandler(w http.ResponseWriter, r *http.Request) { stats := make(map[string]interface{}) - // Tracker statistics + // BitTorrent tracker statistics - get real stats from tracker instance if g.trackerInstance != nil { - trackerStats := make(map[string]interface{}) - trackerStats["status"] = "active" - trackerStats["uptime_seconds"] = time.Since(time.Now()).Seconds() // Placeholder - - stats["tracker"] = trackerStats + stats["tracker"] = g.trackerInstance.GetStats() } - // DHT statistics - if g.dhtBootstrap != nil { - dhtStats := make(map[string]interface{}) - dhtStats["status"] = "active" - dhtStats["routing_table_size"] = "N/A" // Would need DHT interface methods - dhtStats["active_searches"] = 0 - dhtStats["stored_values"] = 0 - - stats["dht"] = dhtStats + // WebSocket tracker statistics - get real stats from WebSocket tracker + if g.wsTracker != nil { + stats["websocket_tracker"] = g.wsTracker.GetStats() + } + + // DHT statistics - get real stats from DHT node + if g.dhtNode != nil { + // Get stats from the actual DHT node within the bootstrap + if dhtNode := g.dhtNode.GetNode(); dhtNode != nil { + dhtStats := dhtNode.GetStats() + stats["dht"] = map[string]interface{}{ + "status": "active", + "packets_sent": dhtStats.PacketsSent, + "packets_received": dhtStats.PacketsReceived, + "nodes_in_table": dhtStats.NodesInTable, + "stored_items": dhtStats.StoredItems, + } + } + } else if g.dhtBootstrap != nil { + // Fallback to placeholder if we don't have the node reference yet + stats["dht"] = map[string]interface{}{ + "status": "active", + "routing_table_size": "N/A", + "active_searches": 0, + "stored_values": 0, + } } // WebSeed statistics (from our enhanced implementation) @@ -2302,7 +2371,8 @@ func (g *Gateway) P2PStatsHandler(w http.ResponseWriter, r *http.Request) { stats["coordination"] = map[string]interface{}{ "integration_active": g.trackerInstance != nil && g.dhtBootstrap != nil, "webseed_enabled": true, - "total_components": 3, // Tracker + DHT + WebSeed + "websocket_enabled": g.wsTracker != nil, + "total_components": 4, // Tracker + WebSocket Tracker + DHT + WebSeed "timestamp": time.Now().Format(time.RFC3339), } @@ -4141,12 +4211,11 @@ func systemStatsHandler(gateway *Gateway, storage *storage.Backend, trackerInsta } // RegisterTrackerRoutes registers tracker endpoints on the main router -func RegisterTrackerRoutes(r *mux.Router, cfg *config.Config, storage *storage.Backend) { +func RegisterTrackerRoutes(r *mux.Router, cfg *config.Config, storage *storage.Backend, gateway *Gateway) *tracker.WebSocketTracker { if !cfg.IsServiceEnabled("tracker") { - return + return nil } - gateway := NewGateway(cfg, storage) trackerInstance := tracker.NewTracker(&cfg.Tracker, gateway) announceHandler := tracker.NewAnnounceHandler(trackerInstance) scrapeHandler := tracker.NewScrapeHandler(trackerInstance) @@ -4161,6 +4230,7 @@ func RegisterTrackerRoutes(r *mux.Router, cfg *config.Config, storage *storage.B r.HandleFunc("/tracker", wsTracker.HandleWS).Methods("GET") // WebSocket upgrade log.Printf("Registered BitTorrent tracker endpoints with WebSocket support") + return wsTracker } // GetGatewayFromRoutes returns a gateway instance for DHT integration @@ -4244,7 +4314,15 @@ func (g *Gateway) generateWebTorrentMagnet(metadata *FileMetadata) string { "wss://tracker.btorrent.xyz", "wss://tracker.openwebtorrent.com", "wss://tracker.webtorrent.dev", - fmt.Sprintf("wss://localhost:%d/tracker", g.config.Gateway.Port), // Our WebSocket tracker + } + + // Add our WebSocket tracker using public URL + if g.publicURL != "" && g.publicURL != "http://localhost" { + // Extract domain from public URL and create WebSocket URL + publicURL := strings.TrimPrefix(g.publicURL, "http://") + publicURL = strings.TrimPrefix(publicURL, "https://") + publicDomain := strings.Split(publicURL, ":")[0] // Remove port if present + wsTrackers = append(wsTrackers, fmt.Sprintf("wss://%s/tracker", publicDomain)) } for _, tracker := range wsTrackers { diff --git a/internal/dht/bootstrap.go b/internal/dht/bootstrap.go index 0c16795..728bd0b 100644 --- a/internal/dht/bootstrap.go +++ b/internal/dht/bootstrap.go @@ -47,6 +47,22 @@ type NodeInfo struct { Reputation int `json:"reputation"` } +// GetNode returns the underlying DHT node instance +func (db *DHTBootstrap) GetNode() *DHT { + return db.node +} + +// GetStats returns DHT bootstrap statistics +func (d *DHTBootstrap) GetStats() map[string]interface{} { + d.mutex.RLock() + defer d.mutex.RUnlock() + return map[string]interface{}{ + "routing_table_size": len(d.knownNodes), + "torrents": len(d.torrents), + "bootstrap_nodes": len(d.config.BootstrapNodes), + } +} + // TorrentAnnounce represents a DHT torrent announcement type TorrentAnnounce struct { InfoHash string `json:"info_hash"` diff --git a/internal/tracker/websocket.go b/internal/tracker/websocket.go index 1a4d86f..352bb2e 100644 --- a/internal/tracker/websocket.go +++ b/internal/tracker/websocket.go @@ -243,4 +243,25 @@ func (wt *WebSocketTracker) cleanupExpiredPeers() { } swarm.mu.Unlock() } +} + +// GetStats returns WebSocket tracker statistics +func (wt *WebSocketTracker) GetStats() map[string]interface{} { + wt.mu.RLock() + defer wt.mu.RUnlock() + + totalPeers := 0 + totalSwarms := len(wt.swarms) + + for _, swarm := range wt.swarms { + swarm.mu.RLock() + totalPeers += len(swarm.peers) + swarm.mu.RUnlock() + } + + return map[string]interface{}{ + "total_swarms": totalSwarms, + "total_peers": totalPeers, + "status": "active", + } } \ No newline at end of file diff --git a/internal/web/index.html b/internal/web/index.html index 294de11..f5dc35f 100644 --- a/internal/web/index.html +++ b/internal/web/index.html @@ -1612,16 +1612,23 @@ async function shareFile(hash) { console.log('DEBUG: shareFile function called!', hash); - console.log('userFiles array:', userFiles); const file = userFiles.find(f => f.hash === hash); - console.log('Found file:', file); if (!file) { console.error('File not found in userFiles array'); return; } const baseUrl = window.location.origin; - const magnetHash = file.torrent_info?.InfoHash || hash; + + // Use torrent info hash for magnet if available, otherwise use file hash + let magnetHash = hash; + if (file.torrent_info_hash) { + magnetHash = file.torrent_info_hash; + console.log('Using torrent InfoHash for magnet:', magnetHash); + } else { + console.log('No torrent InfoHash found, using file hash:', magnetHash); + } + const links = { direct: `${baseUrl}/api/download/${hash}`, torrent: `${baseUrl}/api/torrent/${hash}`, diff --git a/torrent-gateway b/torrent-gateway index 3c0d1b6..c11c4b6 100755 Binary files a/torrent-gateway and b/torrent-gateway differ