File Coverage

blib/lib/Net/BitTorrent/Protocol/BEP03.pm
Criterion Covered Total %
statement 17 17 100.0
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 23 23 100.0


line stmt bran cond sub pod time code
1 26     26   285624 use v5.40;
  26         116  
2 26     26   190 use feature 'class';
  26         67  
  26         3796  
3 26     26   174 no warnings 'experimental::class';
  26         64  
  26         1445  
4             #
5 26     26   3725 use Net::BitTorrent::Emitter;
  26         66  
  26         10899  
6             class Net::BitTorrent::Protocol::BEP03 v2.0.0 : isa(Net::BitTorrent::Emitter) {
7             #
8             field $infohash : param = undef;
9             field $peer_id : param : reader;
10             field $reserved : param : reader : writer = "\0" x 8;
11             field $debug : param : reader = 0;
12             field $state : reader = 'HANDSHAKE'; # HANDSHAKE, OPEN, CLOSED
13             field $buffer_in = '';
14             field $buffer_out = '';
15             field $handshake_sent = 0;
16             field $detected_ih : reader = undef;
17              
18             # Message IDs
19             use constant {
20 26         5740 CHOKE => 0,
21             UNCHOKE => 1,
22             INTERESTED => 2,
23             NOT_INTERESTED => 3,
24             HAVE => 4,
25             BITFIELD => 5,
26             REQUEST => 6,
27             PIECE => 7,
28             CANCEL => 8,
29              
30             # BEP 52
31             HASH_REQUEST => 21,
32             HASHES => 22,
33             HASH_REJECT => 23,
34 26     26   209 };
  26         61  
35              
36             method set_reserved_bit ( $byte, $mask ) {
37 26     26   195 no warnings 'numeric';
  26         74  
  26         56953  
38             my $val = ord( substr( $reserved, $byte, 1 ) );
39             $val |= $mask;
40             $reserved = substr( $reserved, 0, $byte ) . chr($val) . substr( $reserved, $byte + 1 );
41             }
42              
43             method send_handshake () {
44             if ( !defined $infohash ) {
45             $self->_emit( log => 'infohash required to send handshake', level => 'fatal' );
46             return;
47             }
48             my $ih_len = CORE::length($infohash);
49             if ( $ih_len != 20 && $ih_len != 32 ) {
50             $self->_emit( log => 'Info hash must be 20 or 32 bytes', level => 'fatal' );
51             return;
52             }
53             $self->_emit( log => " [DEBUG] Sending handshake (" . unpack( 'H*', $infohash ) . ")\n", level => 'debug' ) if $debug;
54             my $raw = pack( 'C A19 a8', 19, 'BitTorrent protocol', $reserved ) . $infohash . $peer_id;
55             $self->_emit( log => " [DEBUG] Handshake hex: " . unpack( 'H*', $raw ) . "\n", level => 'debug' ) if $debug;
56             $buffer_out .= $raw;
57             $handshake_sent = 1;
58             }
59              
60             method write_buffer () {
61             my $tmp = $buffer_out;
62             $buffer_out = '';
63             return $tmp;
64             }
65             field $processing = 0;
66              
67             method receive_data ($data) {
68             $buffer_in .= $data;
69             return if $processing;
70             $processing = 1;
71             $self->_process_buffer();
72             $processing = 0;
73             }
74              
75             method _process_buffer () {
76             if ( $state eq 'OPEN' ) {
77             $self->_process_messages();
78             return;
79             }
80             my $old_state;
81             while ( $state ne 'CLOSED' && ( !$old_state || $old_state ne $state ) ) {
82             $old_state = $state;
83             if ( $state eq 'HANDSHAKE' ) {
84             $self->_process_handshake();
85             }
86             if ( $state eq 'OPEN' ) {
87             $self->_process_messages();
88             }
89             }
90             }
91              
92             method _process_handshake () {
93             return if length($buffer_in) < 1;
94             my $pstrlen = ord( substr( $buffer_in, 0, 1 ) );
95             if ( $pstrlen != 19 ) {
96             $state = 'CLOSED';
97             $self->_emit(
98             log => 'Invalid protocol string (expected 19, got ' . $pstrlen . ') hex: ' . unpack( 'H*', substr( $buffer_in, 0, 20 ) ),
99             level => 'fatal'
100             );
101             return;
102             }
103             return if length($buffer_in) < 1 + $pstrlen + 8 + 20 + 20; # Min v1 handshake (68 bytes)
104             my $pstr = substr( $buffer_in, 1, $pstrlen );
105             if ( $pstr ne 'BitTorrent protocol' ) {
106             $state = 'CLOSED';
107             $self->_emit( log => 'Invalid protocol string', level => 'fatal' );
108             return;
109             }
110              
111             # We have at least 68 bytes.
112             my $ih_len;
113             if ( defined $infohash ) {
114             $ih_len = length($infohash);
115             }
116             else {
117             # If we have exactly 80 bytes, or more than 80, it MIGHT be a v2 handshake.
118             # But it could also be a v1 handshake (68) followed by a small message.
119             # A v2 handshake MUST be exactly 80 bytes if nothing else follows it.
120             # For now, let's look at the actual length of the buffer.
121             # If it's 68-79, it must be v1.
122             # If it's exactly 80, we assume v2 if we don't know yet?
123             # Actually, most swarms are still v1.
124             if ( length($buffer_in) == 80 ) {
125             $ih_len = 32;
126             }
127             elsif ( length($buffer_in) > 80 ) {
128              
129             # Could be v2 + messages OR v1 + messages.
130             # This is ambiguous without knowing which IH the peer is using.
131             # Standard practice: if we didn't specify, assume v1 first.
132             $ih_len = 20;
133             }
134             else {
135             $ih_len = 20;
136             }
137             }
138             my $handshake_len = 1 + $pstrlen + 8 + $ih_len + 20;
139             return if length($buffer_in) < $handshake_len;
140             my $remote_res = substr( $buffer_in, 1 + $pstrlen, 8 );
141             my $remote_ih = substr( $buffer_in, 1 + $pstrlen + 8, $ih_len );
142             my $remote_id = substr( $buffer_in, 1 + $pstrlen + 8 + $ih_len, 20 );
143             if ( defined $infohash && $remote_ih ne $infohash ) {
144              
145             # If we were expecting v2 but got v1, it might fail here.
146             $state = 'CLOSED';
147             $self->_emit( log => 'Info hash mismatch', level => 'fatal' );
148             return;
149             }
150             substr( $buffer_in, 0, $handshake_len, '' );
151             $state = 'OPEN';
152             $detected_ih = $remote_ih;
153             $reserved = $remote_res;
154             $self->_emit( log => " [DEBUG] Received handshake from " . unpack( 'H*', $remote_id ) . "\n", level => 'debug' ) if $debug;
155             $self->_emit( handshake => $remote_ih, $remote_id );
156             }
157              
158             method _process_messages () {
159             while ( length($buffer_in) >= 4 ) {
160             my $msg_len = unpack( 'N', substr( $buffer_in, 0, 4 ) );
161             if ( $msg_len == 0 ) {
162             substr( $buffer_in, 0, 4, '' ); # Keep-alive
163             next;
164             }
165             if ( length($buffer_in) < 4 + $msg_len ) {
166             return;
167             }
168             my $raw_msg = substr( $buffer_in, 0, 4 + $msg_len, '' );
169             my $id = unpack( 'C', substr( $raw_msg, 4, 1 ) );
170             my $payload = substr( $raw_msg, 5 );
171             $self->_emit( log => " [DEBUG] Received message ID $id (len " . length($payload) . ")\n", level => 'debug' ) if $debug;
172             $self->_handle_message( $id, $payload );
173             }
174             }
175             method _handle_message ( $id, $payload ) { }
176              
177             method send_message ( $id, $payload = '' ) {
178             $self->_emit( log => " [DEBUG] Sending message ID $id (len " . length($payload) . ")\n", level => 'debug' ) if $debug;
179             $buffer_out .= pack( 'N C a*', 1 + length($payload), $id, $payload );
180             }
181              
182             method send_keepalive () {
183             $buffer_out .= pack( 'N', 0 );
184             }
185             method send_choke () { $self->send_message(CHOKE) }
186             method send_unchoke () { $self->send_message(UNCHOKE) }
187             method send_interested () { $self->send_message(INTERESTED) }
188             method send_not_interested () { $self->send_message(NOT_INTERESTED) }
189              
190             method send_have ($index) {
191             $self->send_message( HAVE, pack( 'N', $index ) );
192             }
193              
194             method send_bitfield ($data) {
195             $self->send_message( BITFIELD, $data );
196             }
197             } 1;