package tracker import ( "fmt" "log" "net" "net/http" "net/url" "strconv" "strings" "time" "github.com/anacrolix/torrent/bencode" ) // AnnounceHandler handles BitTorrent announce requests type AnnounceHandler struct { tracker *Tracker encoder *BencodeEncoder } // NewAnnounceHandler creates a new announce handler func NewAnnounceHandler(tracker *Tracker) *AnnounceHandler { return &AnnounceHandler{ tracker: tracker, encoder: NewBencodeEncoder(), } } // ServeHTTP implements http.Handler for the /announce endpoint func (h *AnnounceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { h.writeError(w, "Method not allowed") return } // Parse and validate announce request req, err := h.parseAnnounceRequest(r) if err != nil { log.Printf("Invalid announce request: %v", err) h.writeError(w, fmt.Sprintf("Invalid request: %v", err)) return } // Validate info_hash with gateway if !h.tracker.gateway.IsValidInfoHash(req.InfoHash) { log.Printf("Unknown info_hash: %s", req.InfoHash) h.writeError(w, "Unknown info_hash") return } // Process the announce resp := h.processAnnounce(req) // Write successful response h.writeResponse(w, resp) } // parseAnnounceRequest parses HTTP parameters into AnnounceRequest func (h *AnnounceHandler) parseAnnounceRequest(r *http.Request) (*AnnounceRequest, error) { query := r.URL.Query() // Extract and validate required parameters infoHashRaw := query.Get("info_hash") if infoHashRaw == "" { return nil, fmt.Errorf("missing required parameter: info_hash") } // URL decode info_hash and convert to hex string infoHashBytes, err := url.QueryUnescape(infoHashRaw) if err != nil { return nil, fmt.Errorf("invalid info_hash encoding: %w", err) } if len(infoHashBytes) != 20 { return nil, fmt.Errorf("info_hash must be 20 bytes, got %d", len(infoHashBytes)) } infoHash := fmt.Sprintf("%x", infoHashBytes) peerID := query.Get("peer_id") if peerID == "" { return nil, fmt.Errorf("missing required parameter: peer_id") } if len(peerID) != 20 { return nil, fmt.Errorf("peer_id must be 20 bytes, got %d", len(peerID)) } portStr := query.Get("port") if portStr == "" { return nil, fmt.Errorf("missing required parameter: port") } port, err := strconv.Atoi(portStr) if err != nil || port <= 0 || port > 65535 { return nil, fmt.Errorf("invalid port: %s", portStr) } // Parse optional numeric parameters uploaded := parseIntParam(query, "uploaded", 0) downloaded := parseIntParam(query, "downloaded", 0) left := parseIntParam(query, "left", 0) // Parse optional parameters event := query.Get("event") if event != "" && event != "started" && event != "completed" && event != "stopped" { return nil, fmt.Errorf("invalid event: %s", event) } numWant := parseIntParam(query, "numwant", int64(h.tracker.config.DefaultNumWant)) if numWant > int64(h.tracker.config.MaxNumWant) { numWant = int64(h.tracker.config.MaxNumWant) } if numWant < 0 { numWant = 0 } compact := query.Get("compact") == "1" key := query.Get("key") // Extract client IP ip := h.getClientIP(r) return &AnnounceRequest{ InfoHash: infoHash, PeerID: peerID, Port: port, Uploaded: uploaded, Downloaded: downloaded, Left: left, Event: event, IP: ip, NumWant: int(numWant), Key: key, Compact: compact, }, nil } // processAnnounce handles the announce business logic func (h *AnnounceHandler) processAnnounce(req *AnnounceRequest) *AnnounceResponse { h.tracker.mutex.Lock() defer h.tracker.mutex.Unlock() // Initialize torrent if not exists if h.tracker.peers[req.InfoHash] == nil { h.tracker.peers[req.InfoHash] = make(map[string]*PeerInfo) } torrentPeers := h.tracker.peers[req.InfoHash] // Handle peer lifecycle events switch req.Event { case "stopped": // Remove peer delete(torrentPeers, req.PeerID) log.Printf("Peer %s stopped for torrent %s", req.PeerID[:8], req.InfoHash[:8]) case "completed": // Mark as seeder and update peer := h.updateOrCreatePeer(req, torrentPeers) peer.Left = 0 // Completed download log.Printf("Peer %s completed torrent %s", req.PeerID[:8], req.InfoHash[:8]) case "started": // Add new peer h.updateOrCreatePeer(req, torrentPeers) log.Printf("Peer %s started torrent %s", req.PeerID[:8], req.InfoHash[:8]) default: // Regular update h.updateOrCreatePeer(req, torrentPeers) } // Count seeders and leechers complete, incomplete := h.countPeers(torrentPeers) // Build peer list for response peers := h.buildPeerList(req, torrentPeers) log.Printf("Announce for %s: %d seeders, %d leechers, returning %d peers", req.InfoHash[:8], complete, incomplete, h.countResponsePeers(peers)) return &AnnounceResponse{ Interval: h.tracker.config.AnnounceInterval, MinInterval: h.tracker.config.MinInterval, Complete: complete, Incomplete: incomplete, Peers: peers, } } // updateOrCreatePeer updates existing peer or creates new one func (h *AnnounceHandler) updateOrCreatePeer(req *AnnounceRequest, torrentPeers map[string]*PeerInfo) *PeerInfo { peer, exists := torrentPeers[req.PeerID] if !exists { peer = &PeerInfo{} torrentPeers[req.PeerID] = peer } // Update peer information peer.PeerID = req.PeerID peer.IP = req.IP peer.Port = req.Port peer.Uploaded = req.Uploaded peer.Downloaded = req.Downloaded peer.Left = req.Left peer.LastSeen = time.Now() peer.Event = req.Event peer.Key = req.Key peer.Compact = req.Compact return peer } // buildPeerList creates the peer list for the response func (h *AnnounceHandler) buildPeerList(req *AnnounceRequest, torrentPeers map[string]*PeerInfo) interface{} { var selectedPeers []*PeerInfo // Always include gateway WebSeed if available webSeedURL := h.tracker.gateway.GetWebSeedURL(req.InfoHash) if webSeedURL != "" { if gatewyPeer := h.createGatewayPeer(webSeedURL); gatewyPeer != nil { selectedPeers = append(selectedPeers, gatewyPeer) } } // Add other peers (excluding the requesting peer) count := 0 maxPeers := req.NumWant if len(selectedPeers) > 0 { maxPeers-- // Account for gateway peer } for peerID, peer := range torrentPeers { if peerID != req.PeerID && count < maxPeers { selectedPeers = append(selectedPeers, peer) count++ } } // Return in requested format if req.Compact { return h.createCompactPeerList(selectedPeers) } return h.createDictPeerList(selectedPeers) } // createGatewayPeer creates a peer entry for the gateway WebSeed func (h *AnnounceHandler) createGatewayPeer(webSeedURL string) *PeerInfo { gatewayURL := h.tracker.gateway.GetPublicURL() if gatewayURL == "" { return nil } u, err := url.Parse(gatewayURL) if err != nil { log.Printf("Invalid gateway URL: %v", err) return nil } host := u.Hostname() portStr := u.Port() if portStr == "" { if u.Scheme == "https" { portStr = "443" } else { portStr = "80" } } port, err := strconv.Atoi(portStr) if err != nil { log.Printf("Invalid gateway port: %v", err) return nil } return &PeerInfo{ PeerID: generateWebSeedPeerID(), IP: host, Port: port, Uploaded: 0, Downloaded: 0, Left: 0, // Gateway is always a complete seeder LastSeen: time.Now(), Event: "completed", } } // createCompactPeerList converts peers to compact binary format func (h *AnnounceHandler) createCompactPeerList(peers []*PeerInfo) []byte { var compactPeers []byte for _, peer := range peers { peerBytes := h.peerToCompactBytes(peer) if peerBytes != nil { compactPeers = append(compactPeers, peerBytes...) } } return compactPeers } // createDictPeerList converts peers to dictionary format func (h *AnnounceHandler) createDictPeerList(peers []*PeerInfo) []DictPeer { var dictPeers []DictPeer for _, peer := range peers { dictPeers = append(dictPeers, DictPeer{ PeerID: peer.PeerID, IP: peer.IP, Port: peer.Port, }) } return dictPeers } // peerToCompactBytes converts a peer to compact 6-byte format func (h *AnnounceHandler) peerToCompactBytes(peer *PeerInfo) []byte { // Parse IP address ip := parseIPv4(peer.IP) if ip == nil { return nil } // 6 bytes: 4 for IP, 2 for port (big-endian) compactPeer := make([]byte, 6) copy(compactPeer[0:4], ip) compactPeer[4] = byte(peer.Port >> 8) // High byte compactPeer[5] = byte(peer.Port & 0xFF) // Low byte return compactPeer } // countPeers counts complete and incomplete peers func (h *AnnounceHandler) countPeers(torrentPeers map[string]*PeerInfo) (complete, incomplete int) { for _, peer := range torrentPeers { if peer.Left == 0 { complete++ } else { incomplete++ } } return } // countResponsePeers counts peers in response (for logging) func (h *AnnounceHandler) countResponsePeers(peers interface{}) int { switch p := peers.(type) { case []byte: return len(p) / 6 // Compact format: 6 bytes per peer case []DictPeer: return len(p) default: return 0 } } // getClientIP extracts the real client IP from request headers func (h *AnnounceHandler) getClientIP(r *http.Request) string { // Check X-Forwarded-For header (proxy/load balancer) if xff := r.Header.Get("X-Forwarded-For"); xff != "" { // Take the first IP (client) if ip := extractFirstIP(xff); ip != "" { return ip } } // Check X-Real-IP header if xri := r.Header.Get("X-Real-IP"); xri != "" { if parseIPv4(xri) != nil { return xri } } // Fall back to connection remote address if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { return host } return r.RemoteAddr } // writeResponse writes a successful announce response func (h *AnnounceHandler) writeResponse(w http.ResponseWriter, resp *AnnounceResponse) { w.Header().Set("Content-Type", "text/plain") w.Header().Set("Cache-Control", "no-cache") data, err := bencode.Marshal(resp) if err != nil { log.Printf("Error encoding response: %v", err) h.writeError(w, "Internal server error") return } w.WriteHeader(http.StatusOK) w.Write(data) } // writeError writes an error response in bencode format func (h *AnnounceHandler) writeError(w http.ResponseWriter, message string) { w.Header().Set("Content-Type", "text/plain") w.Header().Set("Cache-Control", "no-cache") resp := map[string]interface{}{ "failure reason": message, } data, err := bencode.Marshal(resp) if err != nil { // Fallback to plain text if bencode fails w.WriteHeader(http.StatusBadRequest) w.Write([]byte("d14:failure reason" + strconv.Itoa(len(message)) + ":" + message + "e")) return } w.WriteHeader(http.StatusBadRequest) w.Write(data) } // Helper functions // parseIntParam safely parses integer parameters with default fallback func parseIntParam(query url.Values, param string, defaultValue int64) int64 { valueStr := query.Get(param) if valueStr == "" { return defaultValue } value, err := strconv.ParseInt(valueStr, 10, 64) if err != nil { return defaultValue } return value } // parseIPv4 parses an IPv4 address string to 4-byte representation func parseIPv4(ipStr string) []byte { parts := strings.Split(ipStr, ".") if len(parts) != 4 { return nil } ip := make([]byte, 4) for i, part := range parts { val, err := strconv.Atoi(part) if err != nil || val < 0 || val > 255 { return nil } ip[i] = byte(val) } return ip } // extractFirstIP extracts the first valid IP from X-Forwarded-For header func extractFirstIP(xff string) string { parts := strings.Split(xff, ",") for _, part := range parts { ip := strings.TrimSpace(part) if parseIPv4(ip) != nil { return ip } } return "" } // ScrapeHandler handles scrape requests (optional BitTorrent feature) type ScrapeHandler struct { tracker *Tracker } // NewScrapeHandler creates a new scrape handler func NewScrapeHandler(tracker *Tracker) *ScrapeHandler { return &ScrapeHandler{tracker: tracker} } // ServeHTTP implements http.Handler for the /scrape endpoint func (h *ScrapeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { h.writeError(w, "Method not allowed") return } query := r.URL.Query() infoHashes := query["info_hash"] if len(infoHashes) == 0 { h.writeError(w, "Missing info_hash parameter") return } h.tracker.mutex.RLock() defer h.tracker.mutex.RUnlock() // Build scrape response files := make(map[string]interface{}) for _, infoHashRaw := range infoHashes { infoHashBytes, err := url.QueryUnescape(infoHashRaw) if err != nil || len(infoHashBytes) != 20 { continue } infoHash := fmt.Sprintf("%x", infoHashBytes) // Check if torrent exists if torrentPeers, exists := h.tracker.peers[infoHash]; exists { complete, incomplete := h.countPeers(torrentPeers) downloaded := complete // Approximate downloads as seeders files[infoHash] = map[string]interface{}{ "complete": complete, "incomplete": incomplete, "downloaded": downloaded, } } else { // Unknown torrent files[infoHash] = map[string]interface{}{ "complete": 0, "incomplete": 0, "downloaded": 0, } } } response := map[string]interface{}{ "files": files, } w.Header().Set("Content-Type", "text/plain") data, err := bencode.Marshal(response) if err != nil { h.writeError(w, "Internal server error") return } w.WriteHeader(http.StatusOK) w.Write(data) } // countPeers counts complete and incomplete peers for scrape func (h *ScrapeHandler) countPeers(torrentPeers map[string]*PeerInfo) (complete, incomplete int) { for _, peer := range torrentPeers { if peer.Left == 0 { complete++ } else { incomplete++ } } return } // writeError writes a scrape error response func (h *ScrapeHandler) writeError(w http.ResponseWriter, message string) { w.Header().Set("Content-Type", "text/plain") resp := map[string]interface{}{ "failure reason": message, } data, err := bencode.Marshal(resp) if err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("d14:failure reason" + strconv.Itoa(len(message)) + ":" + message + "e")) return } w.WriteHeader(http.StatusBadRequest) w.Write(data) }