github.com/anacrolix/torrent@v1.61.0/webtorrent/tracker-client.go (about) 1 package webtorrent 2 3 import ( 4 "context" 5 "crypto/rand" 6 "encoding/json" 7 "fmt" 8 "net/http" 9 "sync" 10 "time" 11 12 g "github.com/anacrolix/generics" 13 "github.com/anacrolix/log" 14 "github.com/gorilla/websocket" 15 "github.com/pion/webrtc/v4" 16 "go.opentelemetry.io/otel/trace" 17 18 "github.com/anacrolix/torrent/tracker" 19 "github.com/anacrolix/torrent/types/infohash" 20 ) 21 22 type TrackerClientStats struct { 23 Dials int64 24 ConvertedInboundConns int64 25 ConvertedOutboundConns int64 26 } 27 28 // Client represents the webtorrent client 29 type TrackerClient struct { 30 Url string 31 GetAnnounceRequest func(_ tracker.AnnounceEvent, infoHash [20]byte) (tracker.AnnounceRequest, error) 32 PeerId [20]byte 33 OnConn onDataChannelOpen 34 Logger log.Logger 35 Dialer *websocket.Dialer 36 37 mu sync.Mutex 38 cond sync.Cond 39 outboundOffers map[string]outboundOfferValue // OfferID to outboundOfferValue 40 wsConn *websocket.Conn 41 closed bool 42 stats TrackerClientStats 43 pingTicker *time.Ticker 44 45 WebsocketTrackerHttpHeader func() http.Header 46 ICEServers []webrtc.ICEServer 47 48 // Used for stats only I think. 49 rtcPeerConns map[string]*wrappedPeerConnection 50 51 // callbacks 52 OnConnected func(error) 53 OnDisconnected func(error) 54 OnAnnounceSuccessful func(ih string) 55 OnAnnounceError func(ih string, err error) 56 } 57 58 func (me *TrackerClient) Stats() TrackerClientStats { 59 me.mu.Lock() 60 defer me.mu.Unlock() 61 return me.stats 62 } 63 64 func (me *TrackerClient) peerIdBinary() string { 65 return binaryToJsonString(me.PeerId[:]) 66 } 67 68 type outboundOffer struct { 69 offerId string 70 outboundOfferValue 71 } 72 73 // outboundOfferValue represents an outstanding offer. 74 type outboundOfferValue struct { 75 originalOffer webrtc.SessionDescription 76 peerConnection *wrappedPeerConnection 77 infoHash [20]byte 78 dataChannel *webrtc.DataChannel 79 } 80 81 type DataChannelContext struct { 82 OfferId string 83 LocalOffered bool 84 InfoHash [20]byte 85 // This is private as some methods might not be appropriate with data channel context. 86 peerConnection *wrappedPeerConnection 87 Span trace.Span 88 Context context.Context 89 } 90 91 func (me *DataChannelContext) GetSelectedIceCandidatePair() (*webrtc.ICECandidatePair, error) { 92 return me.peerConnection.SCTP().Transport().ICETransport().GetSelectedCandidatePair() 93 } 94 95 type onDataChannelOpen func(_ DataChannelConn, dcc DataChannelContext) 96 97 func (tc *TrackerClient) doWebsocket() error { 98 metrics.Add("websocket dials", 1) 99 tc.mu.Lock() 100 tc.stats.Dials++ 101 tc.mu.Unlock() 102 103 var header http.Header 104 if tc.WebsocketTrackerHttpHeader != nil { 105 header = tc.WebsocketTrackerHttpHeader() 106 } 107 108 c, _, err := tc.Dialer.Dial(tc.Url, header) 109 if err != nil { 110 tc.OnDisconnected(err) 111 return fmt.Errorf("dialing tracker: %w", err) 112 } 113 defer c.Close() 114 tc.Logger.Levelf(log.Debug, "connected") 115 tc.mu.Lock() 116 tc.wsConn = c 117 tc.cond.Broadcast() 118 tc.mu.Unlock() 119 tc.announceOffers() 120 closeChan := make(chan struct{}) 121 go func() { 122 for { 123 select { 124 case <-tc.pingTicker.C: 125 tc.mu.Lock() 126 err := c.WriteMessage(websocket.PingMessage, []byte{}) 127 tc.mu.Unlock() 128 if err != nil { 129 return 130 } 131 case <-closeChan: 132 return 133 134 } 135 } 136 }() 137 tc.OnConnected(nil) 138 err = tc.trackerReadLoop(tc.wsConn) 139 close(closeChan) 140 tc.mu.Lock() 141 c.Close() 142 tc.mu.Unlock() 143 return err 144 } 145 146 // Finishes initialization and spawns the run routine, calling onStop when it completes with the 147 // result. We don't let the caller just spawn the runner directly, since then we can race against 148 // .Close to finish initialization. 149 func (tc *TrackerClient) Start(onStop func(error)) { 150 tc.pingTicker = time.NewTicker(60 * time.Second) 151 tc.cond.L = &tc.mu 152 go func() { 153 onStop(tc.run()) 154 }() 155 } 156 157 func (tc *TrackerClient) run() error { 158 tc.mu.Lock() 159 for !tc.closed { 160 tc.mu.Unlock() 161 err := tc.doWebsocket() 162 tc.mu.Lock() 163 if tc.closed { 164 //level = log.Debug 165 } 166 tc.mu.Unlock() 167 tc.Logger.WithDefaultLevel(log.Debug).Printf("websocket instance ended: %v", err) 168 time.Sleep(time.Minute) 169 tc.mu.Lock() 170 } 171 tc.mu.Unlock() 172 return nil 173 } 174 175 func (tc *TrackerClient) Close() error { 176 tc.mu.Lock() 177 tc.closed = true 178 if tc.wsConn != nil { 179 tc.wsConn.Close() 180 } 181 tc.closeUnusedOffers() 182 tc.pingTicker.Stop() 183 tc.mu.Unlock() 184 tc.cond.Broadcast() 185 return nil 186 } 187 188 func (tc *TrackerClient) announceOffers() { 189 // tc.Announce grabs a lock on tc.outboundOffers. It also handles the case where outboundOffers 190 // is nil. Take ownership of outboundOffers here. 191 tc.mu.Lock() 192 offers := tc.outboundOffers 193 tc.outboundOffers = nil 194 tc.mu.Unlock() 195 196 if offers == nil { 197 return 198 } 199 200 // Iterate over our locally-owned offers, close any existing "invalid" ones from before the 201 // socket reconnected, reannounce the infohash, adding it back into the tc.outboundOffers. 202 tc.Logger.WithDefaultLevel(log.Info).Printf("reannouncing %d infohashes after restart", len(offers)) 203 for _, offer := range offers { 204 // TODO: Capture the errors? Are we even in a position to do anything with them? 205 offer.peerConnection.Close() 206 // Use goroutine here to allow read loop to start and ensure the buffer drains. 207 go tc.Announce(tracker.Started, offer.infoHash) 208 } 209 } 210 211 func (tc *TrackerClient) closeUnusedOffers() { 212 for _, offer := range tc.outboundOffers { 213 offer.peerConnection.Close() 214 offer.dataChannel.Close() 215 } 216 tc.outboundOffers = nil 217 } 218 219 func (tc *TrackerClient) CloseOffersForInfohash(infoHash [20]byte) { 220 tc.mu.Lock() 221 defer tc.mu.Unlock() 222 for key, offer := range tc.outboundOffers { 223 if offer.infoHash == infoHash { 224 offer.peerConnection.Close() 225 delete(tc.outboundOffers, key) 226 } 227 } 228 } 229 230 func (tc *TrackerClient) Announce(event tracker.AnnounceEvent, infoHash [20]byte) error { 231 metrics.Add("outbound announces", 1) 232 if event == tracker.Stopped { 233 return tc.announce(event, infoHash, nil) 234 } 235 var randOfferId [20]byte 236 _, err := rand.Read(randOfferId[:]) 237 if err != nil { 238 return fmt.Errorf("generating offer_id bytes: %w", err) 239 } 240 offerIDBinary := binaryToJsonString(randOfferId[:]) 241 242 pc, dc, offer, err := tc.newOffer(tc.Logger, offerIDBinary, infoHash) 243 if err != nil { 244 return fmt.Errorf("creating offer: %w", err) 245 } 246 247 // save the leecher peer connections 248 tc.storePeerConnection(fmt.Sprintf("%x", randOfferId[:]), pc) 249 250 // Register handler in another package. 251 pc.OnClose(func() { 252 // Asynchronous because we might hold the current lock here, depending on where Close is 253 // called from. 254 go tc.removePeerConn(offerIDBinary) 255 }) 256 257 tc.Logger.Levelf(log.Debug, "announcing offer") 258 err = tc.announce(event, infoHash, []outboundOffer{{ 259 offerId: offerIDBinary, 260 outboundOfferValue: outboundOfferValue{ 261 originalOffer: offer, 262 peerConnection: pc, 263 infoHash: infoHash, 264 dataChannel: dc, 265 }}, 266 }) 267 if err != nil { 268 dc.Close() 269 pc.Close() 270 } 271 return err 272 } 273 274 // Remove peer conn so it doesn't come up in stats. 275 func (tc *TrackerClient) removePeerConn(key string) { 276 tc.mu.Lock() 277 defer tc.mu.Unlock() 278 delete(tc.rtcPeerConns, key) 279 } 280 281 func (tc *TrackerClient) announce(event tracker.AnnounceEvent, infoHash [20]byte, offers []outboundOffer) error { 282 request, err := tc.GetAnnounceRequest(event, infoHash) 283 if err != nil { 284 tc.OnAnnounceError(infohash.T(infoHash).HexString(), err) 285 return fmt.Errorf("getting announce parameters: %w", err) 286 } 287 288 req := AnnounceRequest{ 289 Numwant: len(offers), 290 Uploaded: request.Uploaded, 291 Downloaded: request.Downloaded, 292 Left: request.Left, 293 Event: request.Event.String(), 294 Action: "announce", 295 InfoHash: binaryToJsonString(infoHash[:]), 296 PeerID: tc.peerIdBinary(), 297 } 298 for _, offer := range offers { 299 req.Offers = append(req.Offers, Offer{ 300 OfferID: offer.offerId, 301 Offer: offer.originalOffer, 302 }) 303 } 304 305 data, err := json.Marshal(req) 306 if err != nil { 307 return fmt.Errorf("marshalling request: %w", err) 308 } 309 310 tc.mu.Lock() 311 defer tc.mu.Unlock() 312 err = tc.writeMessage(data) 313 if err != nil { 314 tc.OnAnnounceError(infohash.T(infoHash).HexString(), err) 315 return fmt.Errorf("write AnnounceRequest: %w", err) 316 } 317 tc.OnAnnounceSuccessful(infohash.T(infoHash).HexString()) 318 g.MakeMapIfNil(&tc.outboundOffers) 319 for _, offer := range offers { 320 g.MapInsert(tc.outboundOffers, offer.offerId, offer.outboundOfferValue) 321 } 322 return nil 323 } 324 325 // Calculate the stats for all the peer connections the moment they are requested. 326 // As the stats will change over the life of a peer connection, this ensures that 327 // the updated values are returned. 328 func (tc *TrackerClient) RtcPeerConnStats() map[string]webrtc.StatsReport { 329 tc.mu.Lock() 330 defer tc.mu.Unlock() 331 sr := make(map[string]webrtc.StatsReport) 332 for id, pc := range tc.rtcPeerConns { 333 sr[id] = GetPeerConnStats(pc) 334 } 335 return sr 336 } 337 338 func (tc *TrackerClient) writeMessage(data []byte) error { 339 for tc.wsConn == nil { 340 if tc.closed { 341 return fmt.Errorf("%T closed", tc) 342 } 343 tc.cond.Wait() 344 } 345 return tc.wsConn.WriteMessage(websocket.TextMessage, data) 346 } 347 348 func (tc *TrackerClient) trackerReadLoop(tracker *websocket.Conn) error { 349 for { 350 _, message, err := tracker.ReadMessage() 351 if err != nil { 352 return fmt.Errorf("read message error: %w", err) 353 } 354 tc.Logger.Levelf(log.Debug, "received message: %q", message) 355 356 var ar AnnounceResponse 357 if err := json.Unmarshal(message, &ar); err != nil { 358 tc.Logger.WithDefaultLevel(log.Warning).Printf("error unmarshalling announce response: %v", err) 359 continue 360 } 361 switch { 362 case ar.Offer != nil: 363 ih, err := jsonStringToInfoHash(ar.InfoHash) 364 if err != nil { 365 tc.Logger.WithDefaultLevel(log.Warning).Printf("error decoding info_hash in offer: %v", err) 366 break 367 } 368 err = tc.handleOffer(offerContext{ 369 SessDesc: *ar.Offer, 370 Id: ar.OfferID, 371 InfoHash: ih, 372 }, ar.PeerID) 373 if err != nil { 374 tc.Logger.Levelf(log.Error, "handling offer for infohash %x: %v", ih, err) 375 } 376 case ar.Answer != nil: 377 tc.handleAnswer(ar.OfferID, *ar.Answer) 378 default: 379 // wss://tracker.openwebtorrent.com appears to respond to an initial announces without 380 // an offer or answer. I think that's fine. Let's check it at least contains an 381 // infohash. 382 _, err := jsonStringToInfoHash(ar.InfoHash) 383 if err != nil { 384 tc.Logger.Levelf(log.Warning, "unexpected announce response %q", message) 385 } 386 } 387 } 388 } 389 390 type offerContext struct { 391 SessDesc webrtc.SessionDescription 392 Id string 393 InfoHash [20]byte 394 } 395 396 func (tc *TrackerClient) handleOffer( 397 offerContext offerContext, 398 peerId string, 399 ) error { 400 peerConnection, answer, err := tc.newAnsweringPeerConnection(offerContext) 401 if err != nil { 402 return fmt.Errorf("creating answering peer connection: %w", err) 403 } 404 405 // save the seeder peer connections 406 tc.storePeerConnection(fmt.Sprintf("%x", offerContext.Id[:]), peerConnection) 407 408 response := AnnounceResponse{ 409 Action: "announce", 410 InfoHash: binaryToJsonString(offerContext.InfoHash[:]), 411 PeerID: tc.peerIdBinary(), 412 ToPeerID: peerId, 413 Answer: &answer, 414 OfferID: offerContext.Id, 415 } 416 data, err := json.Marshal(response) 417 if err != nil { 418 peerConnection.Close() 419 return fmt.Errorf("marshalling response: %w", err) 420 } 421 tc.mu.Lock() 422 defer tc.mu.Unlock() 423 if err := tc.writeMessage(data); err != nil { 424 peerConnection.Close() 425 return fmt.Errorf("writing response: %w", err) 426 } 427 return nil 428 } 429 430 func (tc *TrackerClient) handleAnswer(offerId string, answer webrtc.SessionDescription) { 431 tc.mu.Lock() 432 defer tc.mu.Unlock() 433 offer, ok := tc.outboundOffers[offerId] 434 if !ok { 435 tc.Logger.WithDefaultLevel(log.Warning).Printf("could not find offer for id %+q", offerId) 436 return 437 } 438 // tc.Logger.WithDefaultLevel(log.Debug).Printf("offer %q got answer %v", offerId, answer) 439 metrics.Add("outbound offers answered", 1) 440 err := offer.peerConnection.SetRemoteDescription(answer) 441 if err != nil { 442 err = fmt.Errorf("using outbound offer answer: %w", err) 443 offer.peerConnection.span.RecordError(err) 444 tc.Logger.LevelPrint(log.Error, err) 445 return 446 } 447 delete(tc.outboundOffers, offerId) 448 go tc.Announce(tracker.None, offer.infoHash) 449 } 450 451 func (tc *TrackerClient) storePeerConnection(offerId string, pc *wrappedPeerConnection) { 452 tc.mu.Lock() 453 defer tc.mu.Unlock() 454 g.MakeMapIfNil(&tc.rtcPeerConns) 455 tc.rtcPeerConns[offerId] = pc 456 }