8 "github.com/anacrolix/log"
10 "github.com/anacrolix/torrent/tracker"
11 "github.com/anacrolix/torrent/webtorrent/buffer"
12 "github.com/gorilla/websocket"
13 "github.com/pion/datachannel"
14 "github.com/pion/webrtc/v2"
17 // Client represents the webtorrent client
22 outboundOffers map[string]outboundOffer // OfferID to outboundOffer
23 tracker *websocket.Conn
24 onConn onDataChannelOpen
28 // outboundOffer represents an outstanding offer.
29 type outboundOffer struct {
30 originalOffer webrtc.SessionDescription
34 func binaryToJsonString(b []byte) string {
37 seq = append(seq, rune(v))
42 type DataChannelContext struct {
43 Local, Remote webrtc.SessionDescription
48 type onDataChannelOpen func(_ datachannel.ReadWriteCloser, dcc DataChannelContext)
50 func NewClient(peerId, infoHash [20]byte, onConn onDataChannelOpen, logger log.Logger) *Client {
52 outboundOffers: make(map[string]outboundOffer),
53 peerIDBinary: binaryToJsonString(peerId[:]),
54 infoHashBinary: binaryToJsonString(infoHash[:]),
60 func (c *Client) Run(ar tracker.AnnounceRequest, url string) error {
61 t, _, err := websocket.DefaultDialer.Dial(url, nil)
63 return fmt.Errorf("failed to dial tracker: %w", err)
66 c.logger.WithValues(log.Info).Printf("dialed tracker %q", url)
72 c.logger.WithValues(log.Error).Printf("error announcing: %v", err)
75 return c.trackerReadLoop()
78 func (c *Client) announce(request tracker.AnnounceRequest) error {
79 transport, offer, err := NewTransport()
81 return fmt.Errorf("failed to create transport: %w", err)
84 randOfferID, err := buffer.RandomBytes(20)
86 return fmt.Errorf("failed to generate bytes: %w", err)
88 offerIDBinary := randOfferID.ToStringLatin1()
91 c.outboundOffers[offerIDBinary] = outboundOffer{
97 req := AnnounceRequest{
98 Numwant: 1, // If higher we need to create equal amount of offers
104 InfoHash: c.infoHashBinary,
105 PeerID: c.peerIDBinary,
107 OfferID: offerIDBinary,
112 data, err := json.Marshal(req)
114 return fmt.Errorf("failed to marshal request: %w", err)
118 err = tracker.WriteMessage(websocket.TextMessage, data)
120 return fmt.Errorf("write AnnounceRequest: %w", err)
127 func (c *Client) trackerReadLoop() error {
133 _, message, err := tracker.ReadMessage()
135 return fmt.Errorf("read error: %w", err)
137 c.logger.WithValues(log.Debug).Printf("received message from tracker: %q", message)
139 var ar AnnounceResponse
140 if err := json.Unmarshal(message, &ar); err != nil {
141 log.Printf("error unmarshaling announce response: %v", err)
144 if ar.InfoHash != c.infoHashBinary {
145 log.Printf("announce response for different hash: expected %q got %q", c.infoHashBinary, ar.InfoHash)
149 case ar.Offer != nil:
150 _, answer, err := NewTransportFromOffer(*ar.Offer, c.onConn, ar.OfferID)
152 return fmt.Errorf("write AnnounceResponse: %w", err)
155 req := AnnounceResponse{
157 InfoHash: c.infoHashBinary,
158 PeerID: c.peerIDBinary,
163 data, err := json.Marshal(req)
165 return fmt.Errorf("failed to marshal request: %w", err)
169 err = tracker.WriteMessage(websocket.TextMessage, data)
171 return fmt.Errorf("write AnnounceResponse: %w", err)
175 case ar.Answer != nil:
177 offer, ok := c.outboundOffers[ar.OfferID]
180 c.logger.WithValues(log.Warning).Printf("could not find offer for id %q", ar.OfferID)
183 c.logger.Printf("offer %q got answer %v", ar.OfferID, *ar.Answer)
184 err = offer.transport.SetAnswer(*ar.Answer, func(dc datachannel.ReadWriteCloser) {
185 c.onConn(dc, DataChannelContext{
186 Local: offer.originalOffer,
193 return fmt.Errorf("failed to sent answer: %w", err)
199 type AnnounceRequest struct {
200 Numwant int `json:"numwant"`
201 Uploaded int `json:"uploaded"`
202 Downloaded int `json:"downloaded"`
203 Left int64 `json:"left"`
204 Event string `json:"event"`
205 Action string `json:"action"`
206 InfoHash string `json:"info_hash"`
207 PeerID string `json:"peer_id"`
208 Offers []Offer `json:"offers"`
212 OfferID string `json:"offer_id"`
213 Offer webrtc.SessionDescription `json:"offer"`
216 type AnnounceResponse struct {
217 InfoHash string `json:"info_hash"`
218 Action string `json:"action"`
219 Interval *int `json:"interval,omitempty"`
220 Complete *int `json:"complete,omitempty"`
221 Incomplete *int `json:"incomplete,omitempty"`
222 PeerID string `json:"peer_id,omitempty"`
223 ToPeerID string `json:"to_peer_id,omitempty"`
224 Answer *webrtc.SessionDescription `json:"answer,omitempty"`
225 Offer *webrtc.SessionDescription `json:"offer,omitempty"`
226 OfferID string `json:"offer_id,omitempty"`