package tracker import ( "log" "net/http" "sync" "time" "github.com/gorilla/websocket" ) type WebSocketTracker struct { upgrader websocket.Upgrader swarms map[string]*Swarm mu sync.RWMutex } type Swarm struct { peers map[string]*WebRTCPeer mu sync.RWMutex } type WebRTCPeer struct { ID string `json:"peer_id"` Conn *websocket.Conn `json:"-"` LastSeen time.Time `json:"last_seen"` InfoHashes []string `json:"info_hashes"` } type WebTorrentMessage struct { Action string `json:"action"` InfoHash string `json:"info_hash,omitempty"` PeerID string `json:"peer_id,omitempty"` Answer map[string]interface{} `json:"answer,omitempty"` Offer map[string]interface{} `json:"offer,omitempty"` ToPeerID string `json:"to_peer_id,omitempty"` FromPeerID string `json:"from_peer_id,omitempty"` NumWant int `json:"numwant,omitempty"` } func NewWebSocketTracker() *WebSocketTracker { return &WebSocketTracker{ upgrader: websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true // Allow all origins for WebTorrent compatibility }, }, swarms: make(map[string]*Swarm), } } func (wt *WebSocketTracker) HandleWS(w http.ResponseWriter, r *http.Request) { conn, err := wt.upgrader.Upgrade(w, r, nil) if err != nil { log.Printf("WebSocket upgrade failed: %v", err) return } defer conn.Close() log.Printf("WebTorrent client connected from %s", r.RemoteAddr) // Handle WebTorrent protocol for { var msg WebTorrentMessage if err := conn.ReadJSON(&msg); err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { log.Printf("WebSocket error: %v", err) } break } switch msg.Action { case "announce": wt.handleAnnounce(conn, msg) case "scrape": wt.handleScrape(conn, msg) case "offer": wt.handleOffer(conn, msg) case "answer": wt.handleAnswer(conn, msg) } } } func (wt *WebSocketTracker) handleAnnounce(conn *websocket.Conn, msg WebTorrentMessage) { wt.mu.Lock() defer wt.mu.Unlock() // Get or create swarm if wt.swarms[msg.InfoHash] == nil { wt.swarms[msg.InfoHash] = &Swarm{ peers: make(map[string]*WebRTCPeer), } } swarm := wt.swarms[msg.InfoHash] swarm.mu.Lock() defer swarm.mu.Unlock() // Add/update peer peer := &WebRTCPeer{ ID: msg.PeerID, Conn: conn, LastSeen: time.Now(), InfoHashes: []string{msg.InfoHash}, } swarm.peers[msg.PeerID] = peer // Return peer list (excluding the requesting peer) var peers []map[string]interface{} numWant := msg.NumWant if numWant == 0 { numWant = 30 // Default } count := 0 for peerID := range swarm.peers { if peerID != msg.PeerID && count < numWant { peers = append(peers, map[string]interface{}{ "id": peerID, }) count++ } } response := map[string]interface{}{ "action": "announce", "interval": 300, // 5 minutes for WebTorrent "info_hash": msg.InfoHash, "complete": len(swarm.peers), // Simplified "incomplete": 0, "peers": peers, } if err := conn.WriteJSON(response); err != nil { log.Printf("Failed to send announce response: %v", err) } } func (wt *WebSocketTracker) handleScrape(conn *websocket.Conn, msg WebTorrentMessage) { wt.mu.RLock() defer wt.mu.RUnlock() files := make(map[string]map[string]int) if swarm := wt.swarms[msg.InfoHash]; swarm != nil { swarm.mu.RLock() files[msg.InfoHash] = map[string]int{ "complete": len(swarm.peers), // Simplified "incomplete": 0, "downloaded": len(swarm.peers), } swarm.mu.RUnlock() } response := map[string]interface{}{ "action": "scrape", "files": files, } if err := conn.WriteJSON(response); err != nil { log.Printf("Failed to send scrape response: %v", err) } } func (wt *WebSocketTracker) handleOffer(conn *websocket.Conn, msg WebTorrentMessage) { wt.mu.RLock() defer wt.mu.RUnlock() if swarm := wt.swarms[msg.InfoHash]; swarm != nil { swarm.mu.RLock() if targetPeer := swarm.peers[msg.ToPeerID]; targetPeer != nil { // Forward offer to target peer offerMsg := map[string]interface{}{ "action": "offer", "info_hash": msg.InfoHash, "peer_id": msg.FromPeerID, "offer": msg.Offer, "from_peer_id": msg.FromPeerID, "to_peer_id": msg.ToPeerID, } if err := targetPeer.Conn.WriteJSON(offerMsg); err != nil { log.Printf("Failed to forward offer: %v", err) } } swarm.mu.RUnlock() } } func (wt *WebSocketTracker) handleAnswer(conn *websocket.Conn, msg WebTorrentMessage) { wt.mu.RLock() defer wt.mu.RUnlock() if swarm := wt.swarms[msg.InfoHash]; swarm != nil { swarm.mu.RLock() if targetPeer := swarm.peers[msg.ToPeerID]; targetPeer != nil { // Forward answer to target peer answerMsg := map[string]interface{}{ "action": "answer", "info_hash": msg.InfoHash, "peer_id": msg.FromPeerID, "answer": msg.Answer, "from_peer_id": msg.FromPeerID, "to_peer_id": msg.ToPeerID, } if err := targetPeer.Conn.WriteJSON(answerMsg); err != nil { log.Printf("Failed to forward answer: %v", err) } } swarm.mu.RUnlock() } } // Cleanup expired peers func (wt *WebSocketTracker) StartCleanup() { ticker := time.NewTicker(5 * time.Minute) go func() { defer ticker.Stop() for range ticker.C { wt.cleanupExpiredPeers() } }() } func (wt *WebSocketTracker) cleanupExpiredPeers() { wt.mu.Lock() defer wt.mu.Unlock() now := time.Now() expiry := now.Add(-10 * time.Minute) // 10 minute timeout for infoHash, swarm := range wt.swarms { swarm.mu.Lock() for peerID, peer := range swarm.peers { if peer.LastSeen.Before(expiry) { peer.Conn.Close() delete(swarm.peers, peerID) } } // Remove empty swarms if len(swarm.peers) == 0 { delete(wt.swarms, infoHash) } swarm.mu.Unlock() } }