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
246 lines
5.9 KiB
Go
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()
|
|
}
|
|
} |