enki 76979d055b
Some checks are pending
CI Pipeline / Run Tests (push) Waiting to run
CI Pipeline / Lint Code (push) Waiting to run
CI Pipeline / Security Scan (push) Waiting to run
CI Pipeline / Build Docker Images (push) Blocked by required conditions
CI Pipeline / E2E Tests (push) Blocked by required conditions
Transcoding and Nip71 update
2025-08-21 19:32:26 -07:00

246 lines
5.9 KiB
Go

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()
}
}