package tracker import ( "crypto/rand" "database/sql" "encoding/hex" "fmt" "log" "net" "net/http" "net/url" "strconv" "strings" "sync" "time" "github.com/anacrolix/torrent/bencode" "torrentGateway/internal/config" ) // Database interface for tracker operations type Database interface { Exec(query string, args ...interface{}) (sql.Result, error) Query(query string, args ...interface{}) (*sql.Rows, error) QueryRow(query string, args ...interface{}) *sql.Row } // Tracker represents a BitTorrent tracker instance with database backing type Tracker struct { peers map[string]map[string]*PeerInfo // infoHash -> peerID -> peer (memory cache) mutex sync.RWMutex config *config.TrackerConfig gateway Gateway // Interface to gateway for WebSeed functionality coordinator P2PCoordinator // Interface to P2P coordinator db Database // Database interface startTime time.Time } // P2PCoordinator interface for tracker integration type P2PCoordinator interface { GetPeers(infoHash string) []CoordinatorPeerInfo OnPeerConnect(infoHash string, peer CoordinatorPeerInfo) AnnounceToExternalServices(infoHash string, port int) error } // CoordinatorPeerInfo represents peer info for coordination type CoordinatorPeerInfo struct { IP string Port int PeerID string Source string Quality int LastSeen time.Time } // Gateway interface for accessing gateway functionality type Gateway interface { GetPublicURL() string IsValidInfoHash(infoHash string) bool GetWebSeedURL(infoHash string) string } // PeerInfo represents a peer in the tracker with enhanced state tracking type PeerInfo struct { PeerID string `json:"peer_id"` InfoHash string `json:"info_hash"` IP string `json:"ip"` IPv6 string `json:"ipv6,omitempty"` // IPv6 address if available Port int `json:"port"` Uploaded int64 `json:"uploaded"` Downloaded int64 `json:"downloaded"` Left int64 `json:"left"` LastSeen time.Time `json:"last_seen"` FirstSeen time.Time `json:"first_seen"` Event string `json:"event"` Key string `json:"key"` Compact bool `json:"compact"` UserAgent string `json:"user_agent"` IsSeeder bool `json:"is_seeder"` // Cached seeder status IsWebSeed bool `json:"is_webseed"` // True if this is a WebSeed Priority int `json:"priority"` // Peer priority (higher = better) } // TorrentStats represents statistics for a torrent type TorrentStats struct { InfoHash string `json:"info_hash"` Seeders int `json:"seeders"` Leechers int `json:"leechers"` Completed int `json:"completed"` LastUpdate time.Time `json:"last_update"` } // CompactPeerIPv6 represents a peer in compact IPv6 format (18 bytes: 16 for IP, 2 for port) type CompactPeerIPv6 struct { IP [16]byte Port uint16 } // AnnounceRequest represents an announce request from a peer type AnnounceRequest struct { InfoHash string `json:"info_hash"` PeerID string `json:"peer_id"` Port int `json:"port"` Uploaded int64 `json:"uploaded"` Downloaded int64 `json:"downloaded"` Left int64 `json:"left"` Event string `json:"event"` IP string `json:"ip"` NumWant int `json:"numwant"` Key string `json:"key"` Compact bool `json:"compact"` } // AnnounceResponse represents the tracker's response to an announce type AnnounceResponse struct { FailureReason string `bencode:"failure reason,omitempty"` WarningMessage string `bencode:"warning message,omitempty"` Interval int `bencode:"interval"` MinInterval int `bencode:"min interval,omitempty"` TrackerID string `bencode:"tracker id,omitempty"` Complete int `bencode:"complete"` Incomplete int `bencode:"incomplete"` Peers interface{} `bencode:"peers"` } // CompactPeer represents a peer in compact format (6 bytes: 4 for IP, 2 for port) type CompactPeer struct { IP [4]byte Port uint16 } // DictPeer represents a peer in dictionary format type DictPeer struct { PeerID string `bencode:"peer id"` IP string `bencode:"ip"` Port int `bencode:"port"` } // NewTracker creates a new tracker instance with database backing func NewTracker(config *config.TrackerConfig, gateway Gateway, db Database) *Tracker { t := &Tracker{ peers: make(map[string]map[string]*PeerInfo), config: config, gateway: gateway, db: db, startTime: time.Now(), } // Initialize database tables if err := t.initializeDatabase(); err != nil { log.Printf("Warning: Failed to initialize tracker database: %v", err) } // Start cleanup routine go t.cleanupRoutine() return t } // SetCoordinator sets the P2P coordinator for integration func (t *Tracker) SetCoordinator(coordinator P2PCoordinator) { t.coordinator = coordinator } // detectAbuse checks for suspicious announce patterns func (t *Tracker) detectAbuse(req *AnnounceRequest, clientIP string) bool { // Check for too frequent announces from same IP if t.isAnnounceSpam(clientIP, req.InfoHash) { log.Printf("Abuse detected: Too frequent announces from IP %s", clientIP) return true } // Check for invalid peer_id patterns if t.isInvalidPeerID(req.PeerID) { log.Printf("Abuse detected: Invalid peer_id pattern from IP %s", clientIP) return true } // Check for suspicious port numbers if t.isSuspiciousPort(req.Port) { log.Printf("Abuse detected: Suspicious port %d from IP %s", req.Port, clientIP) return true } // Check for known bad actors (would be a database in production) if t.isKnownBadActor(clientIP) { log.Printf("Abuse detected: Known bad actor IP %s", clientIP) return true } return false } // Abuse detection helper methods func (t *Tracker) isAnnounceSpam(clientIP, infoHash string) bool { // In production, this would check a time-windowed database // For now, use simple in-memory tracking _ = clientIP + ":" + infoHash // Would be used for tracking // Simple spam detection: more than 10 announces per minute // This would be more sophisticated in production return false // Placeholder } func (t *Tracker) isInvalidPeerID(peerID string) bool { // Check for invalid peer_id patterns if len(peerID) != 20 { return true } // Check for all zeros or all same character (suspicious) allSame := true firstChar := peerID[0] for i := 1; i < len(peerID); i++ { if peerID[i] != firstChar { allSame = false break } } return allSame } func (t *Tracker) isSuspiciousPort(port int) bool { // Flag potentially suspicious ports suspiciousPorts := map[int]bool{ 22: true, // SSH 23: true, // Telnet 25: true, // SMTP 53: true, // DNS 80: true, // HTTP (web servers shouldn't be P2P clients) 135: true, // Windows RPC 139: true, // NetBIOS 443: true, // HTTPS (web servers shouldn't be P2P clients) 445: true, // SMB 993: true, // IMAPS 995: true, // POP3S 1433: true, // SQL Server 3389: true, // RDP 5432: true, // PostgreSQL } // Ports < 1024 are privileged and suspicious for P2P // Ports > 65535 are invalid return suspiciousPorts[port] || port < 1024 || port > 65535 } func (t *Tracker) isKnownBadActor(clientIP string) bool { // In production, this would check against: // - Blocklists from organizations like Bluetack // - Local abuse database // - Cloud provider IP ranges (if configured to block) // For now, just block obvious local/private ranges if configured privateRanges := []string{ "192.168.", "10.", "172.16.", "172.17.", "172.18.", "172.19.", "172.20.", "172.21.", "172.22.", "172.23.", "172.24.", "172.25.", "172.26.", "172.27.", "172.28.", "172.29.", "172.30.", "172.31.", } // Only block private IPs if we're in a production environment // (you wouldn't want to block private IPs in development) for _, prefix := range privateRanges { if strings.HasPrefix(clientIP, prefix) { // In development, allow private IPs return false } } return false } // applyClientCompatibility adjusts response for specific BitTorrent clients func (t *Tracker) applyClientCompatibility(userAgent string, response *AnnounceResponse) { client := t.detectClient(userAgent) switch client { case "qBittorrent": // qBittorrent works well with default settings // No special adjustments needed case "Transmission": // Transmission prefers shorter intervals if response.Interval > 1800 { response.Interval = 1800 // Max 30 minutes } case "WebTorrent": // WebTorrent needs specific adjustments for web compatibility // Ensure reasonable intervals for web clients if response.Interval > 300 { response.Interval = 300 // Max 5 minutes for web clients } if response.MinInterval > 60 { response.MinInterval = 60 // Min 1 minute for web clients } case "Deluge": // Deluge can handle longer intervals // No special adjustments needed case "uTorrent": // uTorrent specific compatibility // Some versions have issues with very short intervals if response.MinInterval < 60 { response.MinInterval = 60 } } } // detectClient identifies BitTorrent client from User-Agent func (t *Tracker) detectClient(userAgent string) string { if userAgent == "" { return "Unknown" } userAgent = strings.ToLower(userAgent) if strings.Contains(userAgent, "qbittorrent") { return "qBittorrent" } if strings.Contains(userAgent, "transmission") { return "Transmission" } if strings.Contains(userAgent, "webtorrent") { return "WebTorrent" } if strings.Contains(userAgent, "deluge") { return "Deluge" } if strings.Contains(userAgent, "utorrent") || strings.Contains(userAgent, "µtorrent") { return "uTorrent" } if strings.Contains(userAgent, "libtorrent") { return "libtorrent" } if strings.Contains(userAgent, "azureus") || strings.Contains(userAgent, "vuze") { return "Azureus" } if strings.Contains(userAgent, "bitcomet") { return "BitComet" } return "Unknown" } // getClientIP extracts the real client IP address func getClientIP(r *http.Request) string { // Check X-Forwarded-For header first (proxy/load balancer) if xff := r.Header.Get("X-Forwarded-For"); xff != "" { // Take the first IP in the chain if ips := strings.Split(xff, ","); len(ips) > 0 { return strings.TrimSpace(ips[0]) } } // Check X-Real-IP header (nginx proxy) if xri := r.Header.Get("X-Real-IP"); xri != "" { return strings.TrimSpace(xri) } // Fall back to RemoteAddr ip, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { return r.RemoteAddr // Return as-is if can't parse } return ip } // HandleAnnounce processes announce requests from peers func (t *Tracker) HandleAnnounce(w http.ResponseWriter, r *http.Request) { // Get client IP for abuse detection clientIP := getClientIP(r) // Parse announce request req, err := t.parseAnnounceRequest(r) if err != nil { t.writeErrorResponse(w, fmt.Sprintf("Invalid announce request: %v", err)) return } // Detect and prevent abuse if t.detectAbuse(req, clientIP) { t.writeErrorResponse(w, "Request rejected due to abuse detection") return } // Validate info hash with gateway if !t.gateway.IsValidInfoHash(req.InfoHash) { t.writeErrorResponse(w, "Invalid info_hash") return } // Process the announce with client compatibility resp := t.processAnnounce(req, r) t.applyClientCompatibility(r.Header.Get("User-Agent"), resp) // Write response w.Header().Set("Content-Type", "text/plain") data, err := bencode.Marshal(resp) if err != nil { t.writeErrorResponse(w, "Internal server error") return } w.Write(data) } // parseAnnounceRequest extracts announce parameters from HTTP request func (t *Tracker) parseAnnounceRequest(r *http.Request) (*AnnounceRequest, error) { query := r.URL.Query() // Required parameters infoHashHex := query.Get("info_hash") if infoHashHex == "" { return nil, fmt.Errorf("missing info_hash") } // URL decode the info_hash infoHash, err := url.QueryUnescape(infoHashHex) if err != nil { return nil, fmt.Errorf("invalid info_hash encoding") } infoHashStr := hex.EncodeToString([]byte(infoHash)) peerID := query.Get("peer_id") if peerID == "" { return nil, fmt.Errorf("missing peer_id") } portStr := query.Get("port") if portStr == "" { return nil, fmt.Errorf("missing port") } port, err := strconv.Atoi(portStr) if err != nil || port <= 0 || port > 65535 { return nil, fmt.Errorf("invalid port") } // Parse numeric parameters uploaded, _ := strconv.ParseInt(query.Get("uploaded"), 10, 64) downloaded, _ := strconv.ParseInt(query.Get("downloaded"), 10, 64) left, _ := strconv.ParseInt(query.Get("left"), 10, 64) // Optional parameters event := query.Get("event") numWantStr := query.Get("numwant") numWant := t.config.DefaultNumWant if numWantStr != "" { if nw, err := strconv.Atoi(numWantStr); err == nil && nw > 0 { numWant = nw if numWant > t.config.MaxNumWant { numWant = t.config.MaxNumWant } } } compact := query.Get("compact") == "1" key := query.Get("key") // Get client IP ip := t.getClientIP(r) return &AnnounceRequest{ InfoHash: infoHashStr, PeerID: peerID, Port: port, Uploaded: uploaded, Downloaded: downloaded, Left: left, Event: event, IP: ip, NumWant: numWant, Key: key, Compact: compact, }, nil } // processAnnounce handles the announce logic and returns a response func (t *Tracker) processAnnounce(req *AnnounceRequest, r *http.Request) *AnnounceResponse { t.mutex.Lock() defer t.mutex.Unlock() // Initialize torrent peer map if not exists if t.peers[req.InfoHash] == nil { t.peers[req.InfoHash] = make(map[string]*PeerInfo) } torrentPeers := t.peers[req.InfoHash] // Handle peer events switch req.Event { case "stopped": delete(torrentPeers, req.PeerID) // Remove from database if err := t.removePeerFromDatabase(req.PeerID, req.InfoHash); err != nil { log.Printf("Failed to remove peer from database: %v", err) } default: // Update or add peer now := time.Now() peer := &PeerInfo{ PeerID: req.PeerID, InfoHash: req.InfoHash, IP: req.IP, Port: req.Port, Uploaded: req.Uploaded, Downloaded: req.Downloaded, Left: req.Left, LastSeen: now, FirstSeen: now, // Will be preserved by database if peer already exists Event: req.Event, Key: req.Key, Compact: req.Compact, UserAgent: r.Header.Get("User-Agent"), IsSeeder: req.Left == 0, IsWebSeed: false, Priority: 50, // Default priority } torrentPeers[req.PeerID] = peer // Store in database if err := t.storePeerInDatabase(peer); err != nil { log.Printf("Failed to store peer in database: %v", err) } // Notify coordinator of new peer connection if t.coordinator != nil { coordPeer := CoordinatorPeerInfo{ IP: peer.IP, Port: peer.Port, PeerID: peer.PeerID, Source: "tracker", Quality: 70, // Tracker peers have good quality LastSeen: peer.LastSeen, } t.coordinator.OnPeerConnect(req.InfoHash, coordPeer) // Announce to external services (DHT, etc.) for new torrents if req.Event == "started" { go func() { if err := t.coordinator.AnnounceToExternalServices(req.InfoHash, req.Port); err != nil { log.Printf("Failed to announce to external services: %v", err) } }() } } } // Count seeders and leechers complete, incomplete := t.countPeers(torrentPeers) // Get peer list for response peers := t.getPeerList(req, torrentPeers) return &AnnounceResponse{ Interval: t.config.AnnounceInterval, MinInterval: t.config.MinInterval, Complete: complete, Incomplete: incomplete, Peers: peers, } } // getPeerList returns a list of peers with WebSeed injection and priority handling func (t *Tracker) getPeerList(req *AnnounceRequest, torrentPeers map[string]*PeerInfo) interface{} { var selectedPeers []*PeerInfo // Get peers from database (includes both tracker and coordinator peers) dbPeers, err := t.GetPeersForTorrent(req.InfoHash) if err != nil { log.Printf("Failed to get peers from database: %v", err) // Fall back to memory peers for peerID, peer := range torrentPeers { if peerID != req.PeerID { dbPeers = append(dbPeers, peer) } } } // Always inject WebSeed as highest priority peer if available webSeedURL := t.gateway.GetWebSeedURL(req.InfoHash) if webSeedURL != "" { if webSeedPeer := t.createWebSeedPeer(req.InfoHash); webSeedPeer != nil { // Store WebSeed peer in database for consistency if err := t.storePeerInDatabase(webSeedPeer); err != nil { log.Printf("Failed to store WebSeed peer: %v", err) } // Add to front of list (highest priority) selectedPeers = append([]*PeerInfo{webSeedPeer}, selectedPeers...) } } // Filter out the requesting peer and add others by priority for _, peer := range dbPeers { if peer.PeerID != req.PeerID && len(selectedPeers) < req.NumWant { // Skip if we already added this peer (avoid duplicates) duplicate := false for _, existing := range selectedPeers { if existing.PeerID == peer.PeerID { duplicate = true break } } if !duplicate { selectedPeers = append(selectedPeers, peer) } } } // Use coordinator for additional peers if available and we need more if t.coordinator != nil && len(selectedPeers) < req.NumWant { coordinatorPeers := t.coordinator.GetPeers(req.InfoHash) for _, coordPeer := range coordinatorPeers { if coordPeer.PeerID == req.PeerID || len(selectedPeers) >= req.NumWant { continue } // Check for duplicates duplicate := false for _, existing := range selectedPeers { if existing.IP == coordPeer.IP && existing.Port == coordPeer.Port { duplicate = true break } } if !duplicate { trackerPeer := &PeerInfo{ PeerID: coordPeer.PeerID, InfoHash: req.InfoHash, IP: coordPeer.IP, Port: coordPeer.Port, Left: 0, // Assume seeder from coordinator LastSeen: coordPeer.LastSeen, IsSeeder: true, IsWebSeed: false, Priority: coordPeer.Quality, } selectedPeers = append(selectedPeers, trackerPeer) } } } // Return in requested format if req.Compact { return t.createCompactPeerList(selectedPeers) } return t.createDictPeerList(selectedPeers) } // createWebSeedPeer creates a WebSeed peer for the gateway func (t *Tracker) createWebSeedPeer(infoHash string) *PeerInfo { webSeedURL := t.gateway.GetWebSeedURL(infoHash) if webSeedURL == "" { return nil } // Parse gateway URL to get IP and port u, err := url.Parse(t.gateway.GetPublicURL()) if err != nil { return nil } host := u.Hostname() portStr := u.Port() if portStr == "" { portStr = "80" if u.Scheme == "https" { portStr = "443" } } port, err := strconv.Atoi(portStr) if err != nil { return nil } return &PeerInfo{ PeerID: generateWebSeedPeerID(), InfoHash: infoHash, IP: host, Port: port, Uploaded: 0, Downloaded: 0, Left: 0, // WebSeed is always a seeder FirstSeen: time.Now(), LastSeen: time.Now(), Event: "started", IsSeeder: true, IsWebSeed: true, Priority: 100, // Highest priority for WebSeed UserAgent: "TorrentGateway-WebSeed/1.0", } } // createCompactPeerList creates compact peer list supporting both IPv4 and IPv6 func (t *Tracker) createCompactPeerList(peers []*PeerInfo) interface{} { // Create separate lists for IPv4 and IPv6 var ipv4Peers []byte var ipv6Peers []byte for _, peer := range peers { // Try IPv4 first ip := net.ParseIP(peer.IP) if ip != nil { if ipv4 := ip.To4(); ipv4 != nil { // 6 bytes: 4 for IPv4, 2 for port peerBytes := make([]byte, 6) copy(peerBytes[0:4], ipv4) peerBytes[4] = byte(peer.Port >> 8) peerBytes[5] = byte(peer.Port & 0xFF) ipv4Peers = append(ipv4Peers, peerBytes...) } } // Try IPv6 if available if peer.IPv6 != "" { if ipv6 := net.ParseIP(peer.IPv6); ipv6 != nil && ipv6.To4() == nil { // 18 bytes: 16 for IPv6, 2 for port peerBytes := make([]byte, 18) copy(peerBytes[0:16], ipv6) peerBytes[16] = byte(peer.Port >> 8) peerBytes[17] = byte(peer.Port & 0xFF) ipv6Peers = append(ipv6Peers, peerBytes...) } } } // Return format depends on what peers we have if len(ipv6Peers) > 0 && len(ipv4Peers) > 0 { // Return both IPv4 and IPv6 peers return map[string]interface{}{ "peers": ipv4Peers, "peers6": ipv6Peers, } } else if len(ipv6Peers) > 0 { // Return only IPv6 peers return map[string]interface{}{ "peers6": ipv6Peers, } } else { // Return only IPv4 peers (traditional format) return ipv4Peers } } // createDictPeerList creates dictionary peer list func (t *Tracker) 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 } // countPeers counts seeders and leechers func (t *Tracker) countPeers(torrentPeers map[string]*PeerInfo) (complete, incomplete int) { for _, peer := range torrentPeers { if peer.Left == 0 { complete++ } else { incomplete++ } } return } // getClientIP extracts the client IP from the request func (t *Tracker) getClientIP(r *http.Request) string { // Check X-Forwarded-For header first xff := r.Header.Get("X-Forwarded-For") if xff != "" { // Take the first IP in the chain parts := strings.Split(xff, ",") ip := strings.TrimSpace(parts[0]) if net.ParseIP(ip) != nil { return ip } } // Check X-Real-IP header xri := r.Header.Get("X-Real-IP") if xri != "" && net.ParseIP(xri) != nil { return xri } // Fall back to RemoteAddr host, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { return r.RemoteAddr } return host } // writeErrorResponse writes an error response in bencode format func (t *Tracker) writeErrorResponse(w http.ResponseWriter, message string) { resp := map[string]interface{}{ "failure reason": message, } w.Header().Set("Content-Type", "text/plain") data, _ := bencode.Marshal(resp) w.Write(data) } // cleanupRoutine periodically removes expired peers func (t *Tracker) cleanupRoutine() { ticker := time.NewTicker(t.config.CleanupInterval) defer ticker.Stop() for range ticker.C { t.cleanupExpiredPeers() } } // cleanupExpiredPeers removes peers that haven't announced recently (45 minutes) func (t *Tracker) cleanupExpiredPeers() { t.mutex.Lock() defer t.mutex.Unlock() // Clean up memory cache now := time.Now() expiry := now.Add(-45 * time.Minute) // 45-minute expiration for infoHash, torrentPeers := range t.peers { for peerID, peer := range torrentPeers { if peer.LastSeen.Before(expiry) { delete(torrentPeers, peerID) } } // Remove empty torrent entries if len(torrentPeers) == 0 { delete(t.peers, infoHash) } } // Clean up database - remove peers older than 45 minutes dbCleanupQuery := `DELETE FROM tracker_peers WHERE last_seen < datetime('now', '-45 minutes')` result, err := t.db.Exec(dbCleanupQuery) if err != nil { log.Printf("Failed to cleanup expired peers from database: %v", err) } else { if rowsAffected, _ := result.RowsAffected(); rowsAffected > 0 { log.Printf("Cleaned up %d expired peers from database", rowsAffected) } } // Clean up old torrent stats (older than 24 hours) statsCleanupQuery := `DELETE FROM torrent_stats WHERE last_update < datetime('now', '-24 hours')` if _, err := t.db.Exec(statsCleanupQuery); err != nil { log.Printf("Failed to cleanup old torrent stats: %v", err) } } // ============ DATABASE OPERATIONS ============ // initializeDatabase creates the necessary database tables func (t *Tracker) initializeDatabase() error { tables := []string{ `CREATE TABLE IF NOT EXISTS tracker_peers ( peer_id TEXT NOT NULL, info_hash TEXT NOT NULL, ip TEXT NOT NULL, ipv6 TEXT, port INTEGER NOT NULL, uploaded INTEGER DEFAULT 0, downloaded INTEGER DEFAULT 0, left_bytes INTEGER DEFAULT 0, first_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP, event TEXT, key_value TEXT, user_agent TEXT, is_seeder BOOLEAN DEFAULT FALSE, is_webseed BOOLEAN DEFAULT FALSE, priority INTEGER DEFAULT 50, PRIMARY KEY (peer_id, info_hash) )`, `CREATE TABLE IF NOT EXISTS torrent_stats ( info_hash TEXT PRIMARY KEY, seeders INTEGER DEFAULT 0, leechers INTEGER DEFAULT 0, completed INTEGER DEFAULT 0, last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP )`, } // Create indexes for performance indexes := []string{ `CREATE INDEX IF NOT EXISTS idx_tracker_peers_info_hash ON tracker_peers(info_hash)`, `CREATE INDEX IF NOT EXISTS idx_tracker_peers_last_seen ON tracker_peers(last_seen)`, `CREATE INDEX IF NOT EXISTS idx_tracker_peers_is_seeder ON tracker_peers(is_seeder)`, `CREATE INDEX IF NOT EXISTS idx_tracker_peers_priority ON tracker_peers(priority DESC)`, } for _, query := range tables { if _, err := t.db.Exec(query); err != nil { return fmt.Errorf("failed to create table: %w", err) } } for _, query := range indexes { if _, err := t.db.Exec(query); err != nil { return fmt.Errorf("failed to create index: %w", err) } } log.Printf("Tracker database tables initialized successfully") return nil } // storePeerInDatabase stores or updates a peer in the database func (t *Tracker) storePeerInDatabase(peer *PeerInfo) error { query := ` INSERT OR REPLACE INTO tracker_peers ( peer_id, info_hash, ip, ipv6, port, uploaded, downloaded, left_bytes, first_seen, last_seen, event, key_value, user_agent, is_seeder, is_webseed, priority ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, COALESCE((SELECT first_seen FROM tracker_peers WHERE peer_id = ? AND info_hash = ?), ?), ?, ?, ?, ?, ?, ?, ? )` _, err := t.db.Exec(query, peer.PeerID, peer.InfoHash, peer.IP, peer.IPv6, peer.Port, peer.Uploaded, peer.Downloaded, peer.Left, peer.PeerID, peer.InfoHash, peer.FirstSeen, // For COALESCE peer.LastSeen, peer.Event, peer.Key, peer.UserAgent, peer.IsSeeder, peer.IsWebSeed, peer.Priority) if err != nil { return fmt.Errorf("failed to store peer: %w", err) } // Update torrent stats go t.updateTorrentStats(peer.InfoHash) return nil } // removePeerFromDatabase removes a peer from the database func (t *Tracker) removePeerFromDatabase(peerID, infoHash string) error { query := `DELETE FROM tracker_peers WHERE peer_id = ? AND info_hash = ?` _, err := t.db.Exec(query, peerID, infoHash) if err != nil { return fmt.Errorf("failed to remove peer: %w", err) } // Update torrent stats go t.updateTorrentStats(infoHash) return nil } // updateTorrentStats updates the cached statistics for a torrent func (t *Tracker) updateTorrentStats(infoHash string) { query := ` SELECT COUNT(CASE WHEN is_seeder = 1 THEN 1 END) as seeders, COUNT(CASE WHEN is_seeder = 0 THEN 1 END) as leechers, COUNT(CASE WHEN left_bytes = 0 THEN 1 END) as completed FROM tracker_peers WHERE info_hash = ? AND last_seen > datetime('now', '-45 minutes')` row := t.db.QueryRow(query, infoHash) var seeders, leechers, completed int if err := row.Scan(&seeders, &leechers, &completed); err != nil { log.Printf("Failed to update torrent stats for %s: %v", infoHash, err) return } updateQuery := ` INSERT OR REPLACE INTO torrent_stats (info_hash, seeders, leechers, completed, last_update) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)` if _, err := t.db.Exec(updateQuery, infoHash, seeders, leechers, completed); err != nil { log.Printf("Failed to store torrent stats for %s: %v", infoHash, err) } } // ============ PUBLIC API METHODS ============ // GetPeersForTorrent returns the list of peers for a specific torrent func (t *Tracker) GetPeersForTorrent(infoHash string) ([]*PeerInfo, error) { query := ` SELECT peer_id, info_hash, ip, COALESCE(ipv6, '') as ipv6, port, uploaded, downloaded, left_bytes, first_seen, last_seen, COALESCE(event, '') as event, COALESCE(key_value, '') as key_value, COALESCE(user_agent, '') as user_agent, is_seeder, is_webseed, priority FROM tracker_peers WHERE info_hash = ? AND last_seen > datetime('now', '-45 minutes') ORDER BY priority DESC, is_webseed DESC, is_seeder DESC, last_seen DESC` rows, err := t.db.Query(query, infoHash) if err != nil { return nil, fmt.Errorf("failed to query peers: %w", err) } defer rows.Close() var peers []*PeerInfo for rows.Next() { peer := &PeerInfo{} err := rows.Scan( &peer.PeerID, &peer.InfoHash, &peer.IP, &peer.IPv6, &peer.Port, &peer.Uploaded, &peer.Downloaded, &peer.Left, &peer.FirstSeen, &peer.LastSeen, &peer.Event, &peer.Key, &peer.UserAgent, &peer.IsSeeder, &peer.IsWebSeed, &peer.Priority, ) if err != nil { log.Printf("Failed to scan peer: %v", err) continue } peers = append(peers, peer) } if err = rows.Err(); err != nil { return nil, fmt.Errorf("error iterating peers: %w", err) } return peers, nil } // GetTorrentStats returns statistics for a specific torrent func (t *Tracker) GetTorrentStats(infoHash string) (*TorrentStats, error) { // Try to get cached stats first query := `SELECT seeders, leechers, completed, last_update FROM torrent_stats WHERE info_hash = ?` row := t.db.QueryRow(query, infoHash) stats := &TorrentStats{InfoHash: infoHash} err := row.Scan(&stats.Seeders, &stats.Leechers, &stats.Completed, &stats.LastUpdate) // If no cached stats or they're old, recalculate if err != nil || time.Since(stats.LastUpdate) > 5*time.Minute { // Calculate real-time stats realTimeQuery := ` SELECT COUNT(CASE WHEN is_seeder = 1 OR left_bytes = 0 THEN 1 END) as seeders, COUNT(CASE WHEN is_seeder = 0 AND left_bytes > 0 THEN 1 END) as leechers, COUNT(CASE WHEN left_bytes = 0 THEN 1 END) as completed FROM tracker_peers WHERE info_hash = ? AND last_seen > datetime('now', '-45 minutes')` realTimeRow := t.db.QueryRow(realTimeQuery, infoHash) if err := realTimeRow.Scan(&stats.Seeders, &stats.Leechers, &stats.Completed); err != nil { return nil, fmt.Errorf("failed to calculate torrent stats: %w", err) } stats.LastUpdate = time.Now() // Update cache asynchronously go t.updateTorrentStats(infoHash) } return stats, nil } // GetAllTorrents returns a list of all active torrents with their stats func (t *Tracker) GetAllTorrents() (map[string]*TorrentStats, error) { query := ` SELECT DISTINCT p.info_hash, COALESCE(s.seeders, 0) as seeders, COALESCE(s.leechers, 0) as leechers, COALESCE(s.completed, 0) as completed, COALESCE(s.last_update, p.last_seen) as last_update FROM tracker_peers p LEFT JOIN torrent_stats s ON p.info_hash = s.info_hash WHERE p.last_seen > datetime('now', '-45 minutes') ORDER BY last_update DESC` rows, err := t.db.Query(query) if err != nil { return nil, fmt.Errorf("failed to query torrents: %w", err) } defer rows.Close() torrents := make(map[string]*TorrentStats) for rows.Next() { stats := &TorrentStats{} err := rows.Scan(&stats.InfoHash, &stats.Seeders, &stats.Leechers, &stats.Completed, &stats.LastUpdate) if err != nil { log.Printf("Failed to scan torrent stats: %v", err) continue } torrents[stats.InfoHash] = stats } if err = rows.Err(); err != nil { return nil, fmt.Errorf("error iterating torrents: %w", err) } return torrents, nil } // generateWebSeedPeerID generates a consistent peer ID for the gateway WebSeed func generateWebSeedPeerID() string { // Use a predictable prefix for WebSeed peers prefix := "-GT0001-" // Gateway Tracker v0.0.1 // Generate random suffix suffix := make([]byte, 6) rand.Read(suffix) return prefix + hex.EncodeToString(suffix) } // GetStats returns comprehensive tracker statistics func (t *Tracker) GetStats() map[string]interface{} { // Get stats from database for accurate counts statsQuery := ` SELECT COUNT(DISTINCT info_hash) as total_torrents, COUNT(*) as total_peers, COUNT(CASE WHEN is_seeder = 1 OR left_bytes = 0 THEN 1 END) as total_seeders, COUNT(CASE WHEN is_seeder = 0 AND left_bytes > 0 THEN 1 END) as total_leechers, COUNT(CASE WHEN is_webseed = 1 THEN 1 END) as webseeds FROM tracker_peers WHERE last_seen > datetime('now', '-45 minutes')` row := t.db.QueryRow(statsQuery) var totalTorrents, totalPeers, totalSeeders, totalLeechers, webseeds int err := row.Scan(&totalTorrents, &totalPeers, &totalSeeders, &totalLeechers, &webseeds) stats := map[string]interface{}{ "uptime_seconds": int(time.Since(t.startTime).Seconds()), "torrents": totalTorrents, "peers": totalPeers, "seeders": totalSeeders, "leechers": totalLeechers, "webseeds": webseeds, } if err != nil { log.Printf("Failed to get database stats, using memory stats: %v", err) // Fallback to memory stats t.mutex.RLock() memoryTorrents := len(t.peers) memoryPeers := 0 memorySeeders := 0 memoryLeechers := 0 for _, torrentPeers := range t.peers { memoryPeers += len(torrentPeers) for _, peer := range torrentPeers { if peer.Left == 0 { memorySeeders++ } else { memoryLeechers++ } } } t.mutex.RUnlock() stats["torrents"] = memoryTorrents stats["peers"] = memoryPeers stats["seeders"] = memorySeeders stats["leechers"] = memoryLeechers stats["webseeds"] = 0 } return stats }