]> gitweb.factorcode.org Git - factor.git/blob - extra/bittorrent/bittorrent.factor
b46b00ad8094f86ed1b658965b4c8b58421e7806
[factor.git] / extra / bittorrent / bittorrent.factor
1 ! Copyright (C) 2020 John Benediktsson
2 ! See http://factorcode.org/license.txt for BSD license
3 USING: accessors arrays assocs bencode byte-arrays byte-vectors
4 calendar checksums checksums.sha combinators destructors endian
5 grouping http.client io io.encodings.binary io.files
6 io.pathnames io.sockets io.streams.byte-array io.streams.duplex
7 kernel literals make math math.bitwise math.functions math.order
8 math.parser namespaces random ranges sequences splitting strings
9 timers ui ui.gadgets.panes ui.tools.listener urls ;
10
11 IN: bittorrent
12
13 <<
14 CONSTANT: ALPHANUMERIC $[
15     [
16         CHAR: a CHAR: z [a..b] %
17         CHAR: A CHAR: Z [a..b] %
18         CHAR: 0 CHAR: 9 [a..b] %
19         ".-_~" %
20     ] { } make
21 ]
22
23 : random-peer-id ( -- bytes )
24     20 [ ALPHANUMERIC random ] B{ } replicate-as ;
25 >>
26
27 SYMBOL: torrent-peer-id
28 torrent-peer-id [ random-peer-id ] initialize
29
30 SYMBOL: torrent-port
31 torrent-port [ 6881 ] initialize
32
33
34 ! bitfield
35
36 : bitfield-index ( n -- j i )
37     8 /mod 7 swap - ;
38
39 : set-bitfield ( elt n bitfield -- )
40     [ bitfield-index rot ] dip -rot
41     '[ _ _ [ set-bit ] [ clear-bit ] if ] change-nth ;
42
43 : check-bitfield ( n bitfield -- ? )
44     [ bitfield-index swap ] dip nth swap bit? ;
45
46
47 ! http
48
49 : http-get-bencode ( url -- obj )
50     <get-request> BV{ } clone [
51         '[ _ push-all ] with-http-request* check-response drop
52     ] keep B{ } like bencode> ;
53
54
55 ! metainfo
56
57 GENERIC: load-metainfo ( obj -- metainfo )
58
59 M: url load-metainfo http-get-bencode ;
60
61 M: pathname load-metainfo
62     binary [ read-bencode ] with-file-reader ;
63
64 M: string load-metainfo
65     dup "http" head? [ >url ] [ <pathname> ] if load-metainfo ;
66
67 : info-hash ( metainfo -- hash )
68     "info hash" swap dup '[
69         drop _ "info" of >bencode sha1 checksum-bytes
70     ] cache ;
71
72 : announce-url ( metainfo -- url )
73     dup "announce-list" of [ nip first random ] [ "announce" of ] if* ;
74
75 : scrape-url ( metainfo -- url/f )
76     announce-url dup path>>  "announce" subseq-index? [
77         [ "announce" "scrape" replace ] change-path
78     ] [ drop f ] if ;
79
80
81
82 ! tracker
83
84 : tracker-url ( metainfo -- url )
85     {
86         [ announce-url >url ]
87         [
88             info-hash "info_hash" set-query-param
89             torrent-peer-id get "peer_id" set-query-param
90             torrent-port get "port" set-query-param
91             0 "uploaded" set-query-param
92             0 "downloaded" set-query-param
93             1 "compact" set-query-param
94         ]
95         [
96             { "info" "length" } [ of ] each
97             "left" set-query-param
98         ]
99     } cleave ;
100
101 TUPLE: magnet display-name exact-length exact-topic
102 web-seed acceptable-source exact-source keyword-topic
103 manifest-topic address-tracker ;
104
105 : magnet-link>magnet ( url -- magnet-url )
106     [ magnet new ] dip
107     >url query>> {
108         [ "dn" of >>display-name ]
109         [ "xl" of >>exact-length ]
110         [ "xt" of >>exact-topic ]
111         [ "ws" of >>web-seed ]
112         [ "as" of >>acceptable-source ]
113         [ "xs" of >>exact-source ]
114         [ "kt" of >>keyword-topic ]
115         [ "mt" of >>manifest-topic ]
116         [ "tr" of >>address-tracker ]
117     } cleave ;
118
119 : parse-peer4 ( peerbin -- inet4 )
120     4 cut [
121         [ number>string ] { } map-as "." join
122     ] dip be> <inet4> ;
123
124 : parse-peer4s ( peersbin -- inet4s )
125     dup array? [
126         [ [ "ip" of ] [ "port" of ] bi <inet4> ] map
127     ] [
128         6 <groups> [ parse-peer4 ] map
129     ] if ;
130
131 : parse-peer6 ( peerbin -- inet6 )
132     16 cut [
133         2 <groups> [ be> number>string ] map ":" join
134     ] dip be> <inet6> ;
135
136 : parse-peer6s ( peersbin -- inet6s )
137     18 <groups> [ parse-peer6 ] map ;
138
139 : load-tracker ( torrent -- response )
140     tracker-url http-get-bencode
141     "peers" over [ parse-peer4s ] change-at ;
142
143 : send-event ( torrent event -- response )
144     [ tracker-url ] [ "event" set-query-param ] bi*
145     http-get-bencode ;
146
147
148
149 ! messages
150
151 TUPLE: handshake string reserved info-hash peer-id ;
152
153 : <handshake> ( info-hash peer-id -- handshake )
154     handshake new
155         "BitTorrent protocol" >byte-array >>string
156         8 <byte-array> >>reserved
157         swap >>peer-id
158         swap >>info-hash ;
159
160 : read-handshake ( -- handshake/f )
161     read1 [
162         [ 48 + read ] keep cut 8 cut 20 cut handshake boa
163     ] [ f ] if* ;
164
165 : write-handshake ( handshake -- )
166     {
167         [ string>> [ length write1 ] [ write ] bi ]
168         [ reserved>> write ]
169         [ info-hash>> write ]
170         [ peer-id>> write ]
171     } cleave flush ;
172
173 TUPLE: keep-alive ;
174 TUPLE: choke ;
175 TUPLE: unchoke ;
176 TUPLE: interested ;
177 TUPLE: not-interested ;
178 TUPLE: have index ;
179 TUPLE: bitfield bitfield ;
180 TUPLE: request index begin length ;
181 TUPLE: piece index begin block ;
182 TUPLE: cancel index begin length ;
183 TUPLE: port port ;
184 TUPLE: suggest-piece index ;
185 TUPLE: have-all ;
186 TUPLE: have-none ;
187 TUPLE: reject-request index begin length ;
188 TUPLE: allowed-fast index ;
189 TUPLE: extended id payload ;
190 TUPLE: unknown id payload ;
191
192 : read-int ( -- n/f ) 4 read [ be> ] [ f ] if* ;
193
194 : parse-message ( bytes -- message/f )
195     unclip {
196         ! Core Protocol
197         { 0 [ drop choke boa ] }
198         { 1 [ drop unchoke boa ] }
199         { 2 [ drop interested boa ] }
200         { 3 [ drop not-interested boa ] }
201         { 4 [ 4 head be> have boa ] }
202         { 5 [ bitfield boa ] }
203         { 6 [ 4 cut 4 cut 4 head [ be> ] tri@ request boa ] }
204         { 7 [ 4 cut 4 cut [ [ be> ] bi@ ] dip piece boa ] }
205         { 8 [ 4 cut 4 cut 4 head [ be> ] tri@ cancel boa ] }
206
207         ! DHT Extension
208         { 9 [ be> port boa ] }
209
210         ! Fast Extensions
211         { 0x0D [ 4 head be> suggest-piece boa ] }
212         { 0x0E [ drop have-all boa ] }
213         { 0x0F [ drop have-none boa ] }
214         { 0x10 [ 4 cut 4 cut 4 head [ be> ] tri@ reject-request boa ] }
215         { 0x11 [ 4 head be> allowed-fast boa ] }
216
217         ! Extension Protocol
218         { 0x14 [ unclip swap extended boa ] }
219
220         ! Hash Transfer Protocol
221         ! { 0x15 [ "HashRequest" ] }
222         ! { 0x16 [ "Hashes" ] }
223         ! { 0x17 [ "HashReject" ] }
224         [ swap unknown boa ]
225     } case ;
226
227 : read-message ( -- message )
228     read-int {
229         { f [ f ] }
230         { 0 [ keep-alive boa ] }
231         [ read [ parse-message ] [ f ] if* ]
232     } case ;
233
234 : write-int ( n -- ) 4 >be write ;
235
236 GENERIC: write-message ( message -- )
237
238 M: keep-alive write-message drop 0 write-int ;
239
240 M: choke write-message drop 1 write-int 0 write1 ;
241
242 M: unchoke write-message drop 1 write-int 1 write1 ;
243
244 M: interested write-message drop 1 write-int 2 write1 ;
245
246 M: not-interested write-message drop 1 write-int 3 write1 ;
247
248 M: have write-message
249     5 write-int 4 write1 index>> write-int ;
250
251 M: bitfield write-message
252     field>> dup length 1 + write-int 5 write1 write ;
253
254 M: request write-message
255     [ index>> ] [ begin>> ] [ length>> ] tri
256     13 write-int 6 write1 [ write-int ] tri@ ;
257
258 M: piece write-message
259     [ index>> ] [ offset>> ] [ block>> ] tri
260     dup length 9 + write-int 7 write1
261     [ write-int ] [ write-int ] [ write ] tri* ;
262
263 M: cancel write-message
264     [ index>> ] [ offset>> ] [ length>> ] tri
265     13 write-int 8 write1 [ write-int ] tri@ ;
266
267 M: port write-message
268     5 write-int 9 write1 port>> write-int ;
269
270 M: suggest-piece write-message
271     5 write-int 0x0D write1 index>> write-int ;
272
273 M: have-all write-message drop 1 write-int 0x0E write1 ;
274
275 M: have-none write-message drop 1 write-int 0x0F write1 ;
276
277 M: reject-request write-message
278     [ index>> ] [ begin>> ] [ length>> ] tri
279     13 write-int 0x10 write1 [ write-int ] tri@ ;
280
281 M: allowed-fast write-message
282     5 write-int 0x11 write1 index>> write-int ;
283
284 M: extended write-message
285     [ payload>> ] [ id>> ] bi
286     over length 2 + write-int 0x14 write1 write1 write ;
287
288 M: unknown write-message
289     [ payload>> ] [ id>> ] bi
290     over length 1 + write-int write1 write ;
291
292 : >message ( bytes -- message )
293     binary [ read-message ] with-byte-reader ;
294
295 : message> ( message -- bytes )
296     binary [ write-message ] with-byte-writer ;
297
298
299
300
301 SYMBOL: torrent-max-block-size
302 torrent-max-block-size [ 16384 ] initialize
303
304 SYMBOL: torrent-directory
305 torrent-directory [ "~/Desktop" ] initialize
306
307
308
309 : with-debug ( quot -- )
310     ui-running? [
311         get-listener output>> <pane-stream> swap
312         '[ @ flush ] with-output-stream*
313     ] [
314         '[ @ flush ] with-global
315     ] if ; inline
316
317
318 ! connection
319
320 TUPLE: torrent metainfo tracker peers ;
321
322 :: <torrent> ( metainfo -- torrent )
323     torrent new
324         metainfo >>metainfo
325         metainfo load-tracker >>tracker
326         V{ } clone >>peers
327     ;
328
329 : torrent-path ( torrent -- path )
330     metainfo>> { "info" "name" } [ of ] each
331     torrent-directory get prepend-path ;
332
333
334 DEFER: <peer>
335
336 : random-peer ( torrent -- peer )
337     dup tracker>> "peers" of random <peer> ;
338
339 : update-tracker ( client -- client )
340     dup metainfo>> load-tracker >>tracker ;
341
342
343 ! peers
344
345 TUPLE: peer < disposable torrent remote stream local handshake
346 self-choking self-interested peer-choking peer-interested timer
347 #pieces piece-length bitfield current-index current-piece ;
348
349 DEFER: with-peer
350
351 :: <peer> ( torrent remote -- peer )
352     peer new
353         torrent >>torrent
354         remote >>remote
355
356         t >>self-choking
357         f >>self-interested
358         t >>peer-choking
359         f >>peer-interested
360
361         dup '[
362             _ [ T{ keep-alive } write-message flush ] with-peer
363         ] 30 seconds dup <timer> >>timer
364
365         torrent metainfo>> "info" of
366         "pieces" of length 20 / [ >>#pieces ] keep
367
368         8 / ceiling <byte-array> >>bitfield
369
370         torrent metainfo>> "info" of
371         "piece length" of [ >>piece-length ] keep
372
373         <byte-vector> >>current-piece
374     ;
375
376 M: peer dispose
377     dup timer>> stop-timer
378     [ dispose f ] change-stream
379     f >>local f >>remote drop ;
380
381 :: with-peer ( peer quot -- )
382     [
383         peer remote>> remote-address set
384         peer local>> local-address set
385         peer stream>> quot with-stream*
386     ] with-scope ; inline
387
388 : connect-peer ( peer -- peer )
389     dup remote>> binary <client> [ >>stream ] [ >>local ] bi* ;
390
391 : handshake-peer ( peer -- peer )
392     dup torrent>> metainfo>> info-hash
393     torrent-peer-id get <handshake> write-handshake
394     read-handshake >>handshake ;
395
396 : fast-peer? ( peer -- ? )
397     handshake>> reserved>> 7 swap nth 3 swap bit? ;
398
399 : unchoke-peer ( peer -- peer )
400     T{ unchoke } write-message f >>self-choking
401     T{ interested } write-message t >>self-interested
402     flush ;
403
404 : choke-peer ( peer -- peer )
405     T{ choke } write-message t >>self-choking
406     T{ not-interested } write-message f >>self-interested
407     flush ;
408
409 :: verify-block ( peer -- peer )
410     peer current-piece>> sha1 checksum-bytes
411     peer current-index>> 20 * dup 20 +
412     peer torrent>> metainfo>>
413     { "info" "pieces" } [ of ] each
414     B{ } subseq-as assert=
415     peer ;
416
417 :: save-block ( peer -- peer )
418     peer torrent>> torrent-path binary [
419         peer current-index>>
420         peer piece-length>> *
421         seek-absolute seek-output
422         peer current-piece>> write
423     ] with-file-appender peer ;
424
425 :: next-block ( peer -- peer )
426     peer current-index>> [ 1 + ] [ 0 ] if*
427     peer #pieces>>
428     peer bitfield>> '[ _ check-bitfield ] find-integer-from
429     peer current-index<<
430     0 peer current-piece>> set-length
431     peer ;
432
433 :: request-piece ( peer -- peer )
434     peer current-index>>
435     [ peer next-block current-index>> ] unless*
436     peer current-piece>> length
437     peer piece-length>> over - torrent-max-block-size get min
438     [
439         2drop
440         peer
441         verify-block
442         save-block
443         next-block
444         request-piece
445     ] [
446         request boa write-message flush peer
447     ] if-zero ;
448
449 GENERIC: handle-message ( peer message -- peer )
450
451 M: object handle-message drop ;
452
453 M: choke handle-message
454     drop t >>peer-choking ;
455
456 M: unchoke handle-message
457     drop f >>peer-choking
458     dup self-choking>> [ next-block request-piece ] unless ;
459
460 M: interested handle-message
461     drop t >>peer-interested ;
462
463 M: not-interested handle-message
464     drop f >>peer-interested ;
465
466 M: have handle-message
467     t swap index>> pick bitfield>> set-nth ;
468
469 M: bitfield handle-message
470     2dup [ bitfield>> length ] bi@ assert=
471     bitfield>> >>bitfield ;
472
473 M: request handle-message
474     [ index>> ] [ begin>> ] [ length>> ] tri
475     reject-request boa write-message ;
476
477 M: have-all handle-message
478     drop [ length [ 255 ] B{ } replicate-as ] change-bitfield ;
479
480 M: have-none handle-message
481     drop [ length <byte-array> ] change-bitfield ;
482
483 M:: piece handle-message ( peer message -- peer )
484     peer current-index>> message index>> assert=
485     peer current-piece>> length message begin>> assert=
486     message [ block>> ] [ begin>> ] bi peer current-piece>> copy
487     peer request-piece ;
488
489 : read-messages ( peer -- peer )
490     [ read-message ] [ handle-message ] while* ;