-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathscrape.go
275 lines (225 loc) · 6.49 KB
/
scrape.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
package goscrape
/*
This library partially implement BEP-0015.
See http://www.bittorrent.org/beps/bep_0015.html
*/
import (
"bytes"
"encoding/binary"
"encoding/hex"
"errors"
"math/rand"
"net"
"net/url"
"sync"
"time"
)
const (
pid uint64 = 0x41727101980 // protocol id magic constant
actionConnect uint32 = 0
actionAnnounce uint32 = 1
actionScrap uint32 = 2
actionError uint32 = 3
defaultTimeout = time.Second * 15
)
var (
// ErrUnsupportedScheme is returned if the URL scheme is unsupported
ErrUnsupportedScheme = errors.New("unsupported scrape scheme")
// ErrTooManyInfohash is returned if more than 74 infohash are given
ErrTooManyInfohash = errors.New("cannot lookup more than 74 infohash at once")
// ErrRequest is returned if a write was not done completely
ErrRequest = errors.New("udp packet was not entirely written")
// ErrResponse is returned if the tracker sent an invalid response
ErrResponse = errors.New("invalid response received from tracker")
// ErrInvalidAction is returned if the tracker answered with an invalid action
ErrInvalidAction = errors.New("invalid action")
// ErrInvalidTransactionID is returned if the tracker answered with an invalid transaction id
ErrInvalidTransactionID = errors.New("invalid transaction id received")
// ErrRemote is returned if a remote error occured
ErrRemote = errors.New("service unavailable")
// ErrRetryLimit is returned when the maximum number of retries is exceeded
ErrRetryLimit = errors.New("maximum number of retries exceeded")
)
// ScrapeResult represents one result returned by the Scrape method
type ScrapeResult struct {
Infohash []byte
Seeders uint32
Leechers uint32
Completed uint32
}
// TorrentScrape represents the internal structure of goscrape
type Goscrape struct {
sync.Mutex
url string
conn net.Conn
connectionID uint64
session time.Time
retries int
timeout time.Duration
}
func init() {
rand.Seed(time.Now().UTC().UnixNano())
}
// New creates a new instance of goscrape for the given torrent tracker
func New(rawurl string) (*Goscrape, error) {
u, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
if u.Scheme != "udp" {
return nil, ErrUnsupportedScheme
}
return &Goscrape{
url: u.Host,
retries: 3,
timeout: defaultTimeout,
}, nil
}
// SetRetryLimit sets the maximum number of attempts to do before giving up
func (g *Goscrape) SetRetryLimit(retries int) {
g.retries = retries
}
// SetTimeout configure the time to wait for a tracker to answer a query
func (g *Goscrape) SetTimeout(timeout time.Duration) {
g.timeout = timeout
}
func (g *Goscrape) transactionID() uint32 {
return uint32(rand.Int31())
}
func (g *Goscrape) connect() (net.Conn, uint64, error) {
var err error
g.Lock()
defer g.Unlock()
if time.Since(g.session) > time.Minute {
// Get a new transaction ID
tid := g.transactionID()
// Prepare our outgoing UDP packet
buf := make([]byte, 16)
binary.BigEndian.PutUint64(buf[0:], pid) // magic constant
binary.BigEndian.PutUint32(buf[8:], 0) // action connect
binary.BigEndian.PutUint32(buf[12:], tid) // transaction id
g.conn, err = net.DialTimeout("udp", g.url, g.timeout)
if err != nil {
return nil, 0, err
}
var n, retries int
for {
retries++
// Set a write deadline
g.conn.SetWriteDeadline(time.Now().Add(g.timeout))
n, err = g.conn.Write(buf)
if err != nil {
return nil, 0, err
}
if n != len(buf) {
return nil, 0, ErrRequest
}
// Set a read deadline
g.conn.SetReadDeadline(time.Now().Add(g.timeout))
// Reuse our buffer to read the response
n, err = g.conn.Read(buf)
if err, ok := err.(net.Error); ok && err.Timeout() {
if retries > g.retries {
return nil, 0, ErrRetryLimit
}
continue
} else if err != nil {
return nil, 0, err
}
break
}
if n != len(buf) {
return nil, 0, ErrResponse
}
if action := binary.BigEndian.Uint32(buf[0:]); action != actionConnect {
return nil, 0, ErrInvalidAction
}
if tid := binary.BigEndian.Uint32(buf[4:]); tid != tid {
return nil, 0, ErrInvalidTransactionID
}
g.connectionID = binary.BigEndian.Uint64(buf[8:])
g.session = time.Now()
}
return g.conn, g.connectionID, nil
}
// Scrape will scrape the given list of infohash and return a ScrapeResult struct
func (g *Goscrape) Scrape(infohash ...[]byte) ([]*ScrapeResult, error) {
if len(infohash) > 74 {
return nil, ErrTooManyInfohash
}
conn, connectionid, err := g.connect()
if err != nil {
return nil, err
}
// Get a new transaction ID
tid := g.transactionID()
// Prepare our outgoing UDP packet
buf := make([]byte, 16+(len(infohash)*20))
binary.BigEndian.PutUint64(buf[0:], connectionid) // connection id
binary.BigEndian.PutUint32(buf[8:], 2) // action scrape
binary.BigEndian.PutUint32(buf[12:], tid) // transaction id
// Pack all the infohash together
src := bytes.Join(infohash, []byte(""))
// Create our temporary hex-decoded buffer
dst := make([]byte, hex.DecodedLen(len(src)))
_, err = hex.Decode(dst, src)
if err != nil {
return nil, err
}
// Copy the binary representation of the infohash
// to the packet buffer
copy(buf[16:], dst)
response := make([]byte, 8+(12*len(infohash)))
var n, retries int
for {
retries++
// Set a write deadline
conn.SetWriteDeadline(time.Now().Add(g.timeout))
// Send the packet to the tracker
n, err = conn.Write(buf)
if err != nil {
return nil, err
}
if n != len(buf) {
return nil, ErrRequest
}
// Set a read deadline
conn.SetReadDeadline(time.Now().Add(g.timeout))
n, err = conn.Read(response)
if err, ok := err.(net.Error); ok && err.Timeout() {
if retries > g.retries {
return nil, ErrRetryLimit
}
continue
} else if err != nil {
return nil, err
}
break
}
// Check expected packet size
if n < 8+(12*len(infohash)) {
return nil, ErrResponse
}
action := binary.BigEndian.Uint32(response[0:])
if transactionid := binary.BigEndian.Uint32(response[4:]); transactionid != tid {
return nil, ErrInvalidTransactionID
}
if action == actionError {
return nil, ErrRemote
}
if action != actionScrap {
return nil, ErrInvalidAction
}
r := make([]*ScrapeResult, len(infohash))
offset := 8
for i := 0; i < len(infohash); i++ {
r[i] = &ScrapeResult{
Infohash: infohash[i],
Seeders: binary.BigEndian.Uint32(response[offset:]),
Completed: binary.BigEndian.Uint32(response[offset+4:]),
Leechers: binary.BigEndian.Uint32(response[offset+8:]),
}
offset += 12
}
return r, nil
}