]> Sergey Matveev's repositories - btrtrc.git/blob - tracker_scraper.go
Rework HTTP announce timeouts
[btrtrc.git] / tracker_scraper.go
1 package torrent
2
3 import (
4         "bytes"
5         "context"
6         "errors"
7         "fmt"
8         "net"
9         "net/url"
10         "time"
11
12         "github.com/anacrolix/dht/v2/krpc"
13         "github.com/anacrolix/log"
14
15         "github.com/anacrolix/torrent/tracker"
16 )
17
18 // Announces a torrent to a tracker at regular intervals, when peers are
19 // required.
20 type trackerScraper struct {
21         u            url.URL
22         t            *Torrent
23         lastAnnounce trackerAnnounceResult
24         allow, done  func()
25 }
26
27 type torrentTrackerAnnouncer interface {
28         statusLine() string
29         URL() *url.URL
30 }
31
32 func (me trackerScraper) URL() *url.URL {
33         return &me.u
34 }
35
36 func (ts *trackerScraper) statusLine() string {
37         var w bytes.Buffer
38         fmt.Fprintf(&w, "next ann: %v, last ann: %v",
39                 func() string {
40                         na := time.Until(ts.lastAnnounce.Completed.Add(ts.lastAnnounce.Interval))
41                         if na > 0 {
42                                 na /= time.Second
43                                 na *= time.Second
44                                 return na.String()
45                         } else {
46                                 return "anytime"
47                         }
48                 }(),
49                 func() string {
50                         if ts.lastAnnounce.Err != nil {
51                                 return ts.lastAnnounce.Err.Error()
52                         }
53                         if ts.lastAnnounce.Completed.IsZero() {
54                                 return "never"
55                         }
56                         return fmt.Sprintf("%d peers", ts.lastAnnounce.NumPeers)
57                 }(),
58         )
59         return w.String()
60 }
61
62 type trackerAnnounceResult struct {
63         Err       error
64         NumPeers  int
65         Interval  time.Duration
66         Completed time.Time
67 }
68
69 func (me *trackerScraper) getIp() (ip net.IP, err error) {
70         ips, err := net.LookupIP(me.u.Hostname())
71         if err != nil {
72                 return
73         }
74         if len(ips) == 0 {
75                 err = errors.New("no ips")
76                 return
77         }
78         for _, ip = range ips {
79                 if me.t.cl.ipIsBlocked(ip) {
80                         continue
81                 }
82                 switch me.u.Scheme {
83                 case "udp4":
84                         if ip.To4() == nil {
85                                 continue
86                         }
87                 case "udp6":
88                         if ip.To4() != nil {
89                                 continue
90                         }
91                 }
92                 return
93         }
94         err = errors.New("no acceptable ips")
95         return
96 }
97
98 func (me *trackerScraper) trackerUrl(ip net.IP) string {
99         u := me.u
100         if u.Port() != "" {
101                 u.Host = net.JoinHostPort(ip.String(), u.Port())
102         }
103         return u.String()
104 }
105
106 // Return how long to wait before trying again. For most errors, we return 5
107 // minutes, a relatively quick turn around for DNS changes.
108 func (me *trackerScraper) announce(event tracker.AnnounceEvent) (ret trackerAnnounceResult) {
109         defer func() {
110                 ret.Completed = time.Now()
111         }()
112         ret.Interval = time.Minute
113         me.allow()
114         defer me.done()
115         ip, err := me.getIp()
116         if err != nil {
117                 ret.Err = fmt.Errorf("error getting ip: %s", err)
118                 return
119         }
120         me.t.cl.rLock()
121         req := me.t.announceRequest(event)
122         me.t.cl.rUnlock()
123         ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
124         defer cancel()
125         me.t.logger.WithDefaultLevel(log.Debug).Printf("announcing to %q: %#v", me.u.String(), req)
126         res, err := tracker.Announce{
127                 Context:    ctx,
128                 HTTPProxy:  me.t.cl.config.HTTPProxy,
129                 UserAgent:  me.t.cl.config.HTTPUserAgent,
130                 TrackerUrl: me.trackerUrl(ip),
131                 Request:    req,
132                 HostHeader: me.u.Host,
133                 ServerName: me.u.Hostname(),
134                 UdpNetwork: me.u.Scheme,
135                 ClientIp4:  krpc.NodeAddr{IP: me.t.cl.config.PublicIp4},
136                 ClientIp6:  krpc.NodeAddr{IP: me.t.cl.config.PublicIp6},
137         }.Do()
138         if err != nil {
139                 ret.Err = fmt.Errorf("error announcing: %s", err)
140                 return
141         }
142         me.t.AddPeers(peerInfos(nil).AppendFromTracker(res.Peers))
143         ret.NumPeers = len(res.Peers)
144         ret.Interval = time.Duration(res.Interval) * time.Second
145         return
146 }
147
148 func (me *trackerScraper) Run() {
149         defer me.announceStopped()
150         // make sure first announce is a "started"
151         e := tracker.Started
152         for {
153                 ar := me.announce(e)
154                 // after first announce, get back to regular "none"
155                 e = tracker.None
156                 me.t.cl.lock()
157                 me.lastAnnounce = ar
158                 me.t.cl.unlock()
159
160         wait:
161                 // Make sure we don't announce for at least a minute since the last one.
162                 interval := ar.Interval
163                 if interval < time.Minute {
164                         interval = time.Minute
165                 }
166
167                 me.t.cl.lock()
168                 wantPeers := me.t.wantPeersEvent.C()
169                 closed := me.t.closed.C()
170                 me.t.cl.unlock()
171
172                 // If we want peers, reduce the interval to the minimum.
173                 select {
174                 case <-wantPeers:
175                         if interval > time.Minute {
176                                 interval = time.Minute
177                         }
178                         // Now we're at the minimum, don't trigger on it anymore.
179                         wantPeers = nil
180                 default:
181                 }
182
183                 select {
184                 case <-closed:
185                         return
186                 case <-wantPeers:
187                         // Recalculate the interval.
188                         goto wait
189                 case <-time.After(time.Until(ar.Completed.Add(interval))):
190                 }
191         }
192 }
193
194 func (me *trackerScraper) announceStopped() {
195         me.announce(tracker.Stopped)
196 }