File Coverage

lib/Net/BitTorrent/Protocol/BEP15.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package Net::BitTorrent::Protocol::BEP15;
2             our $VERSION = "1.5.1";
3 1     1   993 use strictures;
  0            
  0            
4             use Type::Utils;
5             use Type::Params qw[compile];
6             use Types::Standard qw[slurpy Dict ArrayRef Optional Maybe Int Str Enum];
7             use Carp qw[carp];
8             use vars qw[@EXPORT_OK %EXPORT_TAGS];
9             use Exporter qw[];
10             *import = *import = *Exporter::import;
11             %EXPORT_TAGS = (
12             build => [
13             qw[ build_connect_request build_connect_reply
14             build_announce_request build_announce_reply
15             build_scrape_request build_scrape_reply
16             build_error_reply
17             ]
18             ],
19             parse => [
20             qw[ parse_connect_request parse_connect_reply
21             parse_announce_request parse_announce_reply
22             parse_scrape_request parse_scrape_reply
23             parse_error_reply
24             parse_request parse_reply
25             ]
26             ],
27             types => [
28             qw[ $CONNECT $ANNOUNCE $SCRAPE $ERROR
29             $NONE $COMPLETED $STARTED $STOPPED ]
30             ]
31             );
32             @EXPORT_OK = sort map { @$_ = sort @$_; @$_ } values %EXPORT_TAGS;
33             $EXPORT_TAGS{'all'} = \@EXPORT_OK;
34             use Digest::SHA qw[sha1];
35             use Net::BitTorrent::Protocol::BEP23 qw[compact_ipv4 uncompact_ipv4];
36             #
37             our $CONNECTION_ID = 4497486125440; # 0x41727101980
38              
39             # Actions
40             our $CONNECT = 0;
41             our $ANNOUNCE = 1;
42             our $SCRAPE = 2;
43             our $ERROR = 3;
44              
45             # Events
46             our $NONE = 0;
47             our $COMPLETED = 1;
48             our $STARTED = 2;
49             our $STOPPED = 3;
50              
51             # Build functions
52             sub build_connect_request {
53             CORE::state $check = compile(slurpy Dict [transaction_id => Int]);
54             my ($args) = $check->(@_);
55             return pack 'Q>NN', $CONNECTION_ID, $CONNECT, $args->{transaction_id};
56             }
57              
58             sub build_connect_reply {
59             CORE::state $check
60             = compile(slurpy Dict [transaction_id => Int, connection_id => Int]);
61             my ($args) = $check->(@_);
62             return pack 'NNQ>', $CONNECT, $args->{transaction_id},
63             $args->{connection_id};
64             }
65              
66             sub build_announce_request {
67             CORE::state $check = compile(
68             slurpy Dict [
69             connection_id => Int,
70             transaction_id => Int,
71             info_hash => Str,
72             peer_id => Str,
73             downloaded => Int,
74             left => Int,
75             uploaded => Int,
76             event => Enum [$NONE, $COMPLETED, $STARTED, $STOPPED],
77             ip => Optional [Str], # Default: 0
78             key => Int,
79             num_want => Optional [Int], # Default: -1
80             port => Int,
81             request_string => Optional [Str],
82             authentication => Optional [ArrayRef]
83             ]
84             );
85             my ($args) = $check->(@_);
86             my $data = pack 'Q>NN a20a20 Q>Q>Q>N a4 Nl>n',
87             $args->{connection_id}, $ANNOUNCE, $args->{transaction_id},
88             $args->{info_hash}, $args->{peer_id},
89             $args->{downloaded}, $args->{left}, $args->{uploaded}, $args->{event},
90             (defined $args->{ip} ?
91             $args->{ip} =~ m[\.] ?
92             (pack("C4", split(/\./, $args->{ip})))
93             : pack 'N',
94             0
95             : pack 'N',
96             0
97             ),
98             $args->{key}, ($args->{num_want} // -1), $args->{port};
99             my $ext = 0;
100             $ext ^= 1 if defined $args->{authentication};
101             $ext ^= 2 if defined $args->{request_string};
102             $data .= pack 'n', $ext;
103             if (defined $args->{authentication}) {
104             $data .= pack('ca*',
105             length($args->{authentication}[0]),
106             $args->{authentication}[0]);
107             $data .= pack('a8', sha1($data, sha1($args->{authentication}[1])));
108             }
109             $data
110             .= pack('ca*', length($args->{request_string}),
111             $args->{request_string})
112             if defined $args->{request_string};
113             $data;
114             }
115              
116             sub build_announce_reply {
117             CORE::state $check = compile(slurpy Dict [
118             transaction_id => Int,
119             interval => Int,
120             leechers => Int,
121             seeders => Int,
122             peers => ArrayRef [Maybe [ArrayRef]]
123             ]
124             );
125             my ($args) = $check->(@_);
126             pack 'NNNNNa*',
127             $ANNOUNCE,
128             (map { $args->{$_} } qw[transaction_id interval leechers seeders]),
129             (compact_ipv4(@{$args->{peers}}) // '');
130             }
131              
132             sub build_scrape_request {
133             CORE::state $check = compile(slurpy Dict [connection_id => Int,
134             transaction_id => Int,
135             info_hash => ArrayRef [Str]
136             ]
137             );
138             my ($args) = $check->(@_);
139             return pack 'Q>NN(a20)*',
140             $args->{connection_id}, $SCRAPE, $args->{transaction_id},
141             @{$args->{info_hash}};
142             }
143              
144             sub build_scrape_reply {
145             CORE::state $check = compile(
146             slurpy Dict [
147             transaction_id => Int,
148             scrape =>
149             ArrayRef [
150             Dict [downloaded => Int, incomplete => Int, complete => Int]
151             ]
152             ]
153             );
154             my ($args) = $check->(@_);
155             CORE::state $keys = [qw[complete downloaded incomplete]];
156             my $data = pack 'NN', $SCRAPE, $args->{transaction_id};
157             for my $scrape (@{$args->{scrape}}) {
158             for my $key (@$keys) {
159             $data .= pack 'N', $scrape->{$key};
160             }
161             }
162             $data;
163             }
164              
165             sub build_error_reply {
166             CORE::state $check = compile(slurpy Dict [transaction_id => Int,
167             'failure reason' => Str
168             ]
169             );
170             my ($args) = $check->(@_);
171             return pack 'NNa*', $ERROR,
172             map { $args->{$_} } qw[transaction_id], 'failure reason';
173             }
174              
175             # Parse functions
176             sub parse_connect_request {
177             my ($data) = @_;
178             if (length $data < 16) {
179             return {fatal => 0, error => 'Not enough data'};
180             }
181             my ($cid, $action, $tid) = unpack 'Q>NN', $data;
182             if ($cid != $CONNECTION_ID) {
183             return {fatal => 1, error => 'Incorrect connection id'};
184             }
185             if ($action != $CONNECT) {
186             return {fatal => 1,
187             error => 'Incorrect action for connect request'
188             };
189             }
190             return {transaction_id => $tid, action => $action, connection_id => $cid};
191             }
192              
193             sub parse_connect_reply {
194             my ($data) = @_;
195             if (length $data < 16) {
196             return {fatal => 0, error => 'Not enough data'};
197             }
198             my ($action, $tid, $cid) = unpack 'NNQ>', $data;
199             if ($action != $CONNECT) {
200             return {fatal => 1,
201             error => 'Incorrect action for connect request'
202             };
203             }
204             return {transaction_id => $tid, action => $action, connection_id => $cid};
205             }
206              
207             sub parse_announce_request {
208             my ($data) = @_;
209             if (length $data < 16) {
210             return {fatal => 0, error => 'Not enough data'};
211             }
212             my ($cid, $action, $tid,
213             #
214             $info_hash, $peer_id,
215             #
216             $downloaded, $left, $uploaded, $event,
217             #
218             $ip,
219             #
220             $key, $num_want, $port, $ext, $ext_data
221             )
222             = unpack 'Q>NN a20a20 Q>Q>Q>N a4 Nl>nna*',
223             $data;
224             if ($action != $ANNOUNCE) {
225             return {fatal => 1,
226             error => 'Incorrect action for announce request'
227             };
228             }
229             my $retval = {connection_id => $cid,
230             action => $action,
231             transaction_id => $tid,
232             info_hash => $info_hash,
233             peer_id => $peer_id,
234             downloaded => $downloaded,
235             left => $left,
236             uploaded => $uploaded,
237             event => $event,
238             ip => $ip,
239             key => $key,
240             num_want => $num_want,
241             port => $port,
242             ip => (join(".", unpack("C4", $ip)))
243             };
244             ($retval->{authentication}[0], $retval->{authentication}[1], $ext_data)
245             = unpack 'c/aa8a*', $ext_data
246             if $ext & 1;
247             $retval->{request_string} = unpack 'c/a', $ext_data if $ext & 2;
248             $retval;
249             }
250              
251             sub parse_announce_reply {
252             my ($data) = @_;
253             my ($action, $transaction_id, $interval, $leechers, $seeders, $peers)
254             = unpack 'NNNNNa*', $data;
255             return if $action != $ANNOUNCE;
256             return {action => $action,
257             transaction_id => $transaction_id,
258             interval => $interval,
259             leechers => $leechers,
260             seeders => $seeders,
261             peers => [uncompact_ipv4 $peers]
262             };
263             }
264              
265             sub parse_scrape_request {
266             my ($data) = @_;
267             my ($connection_id, $action, $transaction_id, $infohash)
268             = unpack 'Q>NNa*', $data;
269             return if $action != $SCRAPE;
270             return {action => $action,
271             connection_id => $connection_id,
272             transaction_id => $transaction_id,
273             info_hash => [unpack '(a20)*', $infohash]
274             };
275             }
276              
277             sub parse_scrape_reply {
278             my ($data) = @_;
279             my ($action, $transaction_id, @etc) = unpack 'NN(NNN)*', $data;
280             return if $action != $SCRAPE;
281             CORE::state $keys = [qw[complete downloaded incomplete]];
282             my @scrape;
283             while (my @next_n = splice @etc, 0, 3) {
284             push @scrape, {map { $keys->[$_] => $next_n[$_] } 0 .. $#next_n};
285             }
286             return {action => $action,
287             transaction_id => $transaction_id,
288             scrape => [@scrape]
289             };
290             }
291              
292             sub parse_error_reply {
293             my ($data) = @_;
294             my ($action, $transaction_id, $failure_reason) = unpack 'NNa*', $data;
295             return if $action != $ERROR;
296             return {transaction_id => $transaction_id,
297             'failure reason' => $failure_reason
298             };
299             }
300              
301             sub parse_request {
302             CORE::state $check = compile(Str);
303             my ($data) = $check->(@_);
304             my ($connection_id, $action) = unpack 'Q>N', $data;
305             return parse_connect_request($data) if $action == $CONNECT;
306             return parse_announce_request($data) if $action == $ANNOUNCE;
307             return parse_scrape_request($data) if $action == $SCRAPE;
308             return;
309             }
310              
311             sub parse_reply {
312             CORE::state $check = compile(Str);
313             my ($data) = $check->(@_);
314             my ($action) = unpack 'NN', $data;
315             return parse_connect_reply($data) if $action == $CONNECT;
316             return parse_announce_reply($data) if $action == $ANNOUNCE;
317             return parse_scrape_reply($data) if $action == $SCRAPE;
318             return parse_error_reply($data) if $action == $ERROR;
319             return;
320             }
321             1;
322              
323             =pod
324              
325             =head1 NAME
326              
327             Net::BitTorrent::Protocol::BEP15 - Packet Utilities for BEP15, the UDP Tracker Protocol
328              
329             =head1 Synopsis
330              
331             use Net::BitTorrent::Protocol::BEP15 qw[:all];
332              
333             # Tell them we want to connect...
334             my $handshake = build_connect_request(255);
335              
336             # ...send to tracker and get reply...
337             my ($transaction_id, $connection_id) = parse_connect_reply( $reply );
338              
339             =head1 Description
340              
341             What would BitTorrent be without packets? TCP noise, mostly.
342              
343             For similar work and the specifications behind these packets, move on down to
344             the L section.
345              
346             =head1 Importing from Net::BitTorrent::Protocol::BEP15
347              
348             There are two tags available for import. To get them both in one go, use the
349             C<:all> tag.
350              
351             =over
352              
353             =item C<:build>
354              
355             These create packets ready-to-send to trackers. See
356             L.
357              
358             =item C<:parse>
359              
360             These are used to parse unknown data into sensible packets. The same packet
361             types we can build, we can also parse. You may want to use this to write your
362             own UDP tracker. See L.
363              
364             =back
365              
366             =head2 Building Functions
367              
368             =over
369              
370             =item C
371              
372             Creates a request for a connection id. The provided C should
373             be a random 32-bit integer.
374              
375             =item C
376              
377             Creates a reply for a connection request. The C should match
378             the value sent from the client. The C is sent with every packet
379             to identify the client.
380              
381             =item C
382              
383             Creates a packet suited to announce with the tracker. The following keys are
384             required:
385              
386             =over
387              
388             =item C
389              
390             This is the same C returned by the tracker when you sent a
391             connection request.
392              
393             =item C
394              
395             This is defined by you. It's a random integer which will be returned by the
396             tracker in response to this packet.
397              
398             =item C
399              
400             This is the packed info hash of the torrent.
401              
402             =item C
403              
404             This is your client's peer id.
405              
406             =item C
407              
408             The amount of data you have downloaded so far this session.
409              
410             =item C
411              
412             The amount of data you have left to download before complete.
413              
414             =item C
415              
416             The amount of data you have uploaded to other peers in this session.
417              
418             =item C
419              
420             This value is either C<$NONE>, C<$COMPLETED>, C<$STARTED>, or C<$STOPPED>.
421             C<$NONE> is sent when you're simply reannouncing after a certain interval.
422              
423             All of these are imported with the C<:types> or C<:all> tags.
424              
425             =item C
426              
427             A unique key that is randomized by the client. Unlike the C
428             which is generated for every packet, this value should be kept per-session.
429              
430             =item C
431              
432             The port you're listening on.
433              
434             =back
435              
436             ...and the following are optional. Some have default values:
437              
438             =over
439              
440             =item C
441              
442             The request string extension is meant to allow torrent creators pass along
443             cookies back to the tracker. This can be useful for authenticating that a
444             torrent is allowed to be tracked by a tracker for instance. It could also be
445             used to authenticate users by generating torrents with unique tokens in the
446             tracker URL for each user.
447              
448             Typically this starts with "/announce" The bittorrent client is not expected
449             to append query string arguments for stats reporting, like "uploaded" and
450             "downloaded" since this is already reported in the udp tracker protocol.
451             However, the client is free to add arguments as extensions.
452              
453             =item C
454              
455             This is a list which contains a username and password. This function then
456             correctly hashes the password to sent over the wire.
457              
458             =item C
459              
460             Your ip address. By default, this is C<0> which tells the tracker to use the
461             sender of this udp packet.
462              
463             =item C
464              
465             The maximum number of peers you want in the reply. The default is C<-1> which
466             lets the tracker decide.
467              
468             =back
469              
470             =item C
471              
472             Creates a packet a UDP tracker would sent in reply to an announce packet from
473             a client. The following are required: C, the C at
474             which the client should reannounce, the number of C and C,
475             as well as a list of C for the given infohash.
476              
477             =item C
478              
479             Creates a packet for a client to request basic data about a number of
480             torrents. Up to about 74 torrents can be scraped at once. A full scrape can't
481             be done with this protocol.
482              
483             You must provide: the tracker provided C, a C,
484             and a list in C.
485              
486             =item C
487              
488             Creates a packet for a tracker to sent in reply to a scrape request. You must
489             provide the client defined C and a list of hashes as C
490             data. The hashes contain integers for the following: C,
491             C, and C.
492              
493             =item C
494              
495             Creates a packet to be sent to the client in case of an error. You must
496             provide a C and C.
497              
498             =back
499              
500             =head2 Parsing Functions
501              
502             These are the parsing counterparts for the C functions.
503              
504             When the packet is invalid, a hash reference is returned with C and
505             C keys. The value in C is a string describing what went wrong.
506              
507             Return values for valid packets are explained below.
508              
509             =over
510              
511             =item C
512              
513             This will automatically call the correct parsing function for you. When you
514             aren't exactly sure what the data is.
515              
516             =item C
517              
518             This will automatically call the correct parsing function for you. When you
519             aren't exactly sure what the data is. This would be use in you're writing a
520             UDP tracker yourself.
521              
522             =item C
523              
524             Returns the parsed transaction id.
525              
526             =item C
527              
528             Parses the reply for a connect request. Returns the original transaction id
529             and the new connection id.
530              
531             =item C
532              
533             Returns C, C, C, C,
534             C, C, C, C, C, C, C,
535             C, C.
536              
537             Optionally, this packet might also contian C and
538             C values.
539              
540             =item C
541              
542             Returns the C, the C at which you should
543             re-announce, the current number of C and C, and an inflated
544             list of C.
545              
546             =item C
547              
548             Returns the C, C, and an C which may
549             contain multiple infohashes depending on the request.
550              
551             =item C
552              
553             Returns C and list of hashes in C. The scrape hashes
554             contain C, C and C keys.
555              
556             =item C
557              
558             Returns C and C.
559              
560             =back
561              
562             =head1 See Also
563              
564             http://bittorrent.org/beps/bep_0015.html - UDP Tracker Protocol for BitTorrent
565              
566             =head1 Author
567              
568             Sanko Robinson - http://sankorobinson.com/
569              
570             CPAN ID: SANKO
571              
572             =head1 License and Legal
573              
574             Copyright (C) 2016 by Sanko Robinson
575              
576             This program is free software; you can redistribute it and/or modify it under
577             the terms of
578             L.
579             See the F file included with this distribution or
580             L
581             for clarification.
582              
583             When separated from the distribution, all original POD documentation is
584             covered by the
585             L.
586             See the
587             L.
588              
589             Neither this module nor the L is affiliated with BitTorrent,
590             Inc.
591              
592             =cut