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  }