| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
1
|
|
|
1
|
|
238401
|
use v5.40; |
|
|
1
|
|
|
|
|
4
|
|
|
2
|
1
|
|
|
1
|
|
10
|
use feature 'class'; |
|
|
1
|
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
207
|
|
|
3
|
1
|
|
|
1
|
|
10
|
no warnings 'experimental::class'; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
160
|
|
|
4
|
|
|
|
|
|
|
# |
|
5
|
|
|
|
|
|
|
class Net::Multicast::PeerDiscovery v1.0.0 { |
|
6
|
1
|
|
|
|
|
373
|
use Socket qw[ |
|
7
|
|
|
|
|
|
|
pack_sockaddr_in unpack_sockaddr_in inet_aton inet_ntoa |
|
8
|
|
|
|
|
|
|
pack_sockaddr_in6 unpack_sockaddr_in6 inet_pton inet_ntop |
|
9
|
|
|
|
|
|
|
AF_INET AF_INET6 sockaddr_family |
|
10
|
1
|
|
|
1
|
|
8
|
]; |
|
|
1
|
|
|
|
|
2
|
|
|
11
|
1
|
|
|
1
|
|
629
|
use IO::Select; |
|
|
1
|
|
|
|
|
2087
|
|
|
|
1
|
|
|
|
|
69
|
|
|
12
|
1
|
|
|
1
|
|
9
|
use Carp qw[carp croak]; |
|
|
1
|
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
205
|
|
|
13
|
|
|
|
|
|
|
# |
|
14
|
|
|
|
|
|
|
BEGIN { |
|
15
|
|
|
|
|
|
|
# Polyfill for systems where Socket::pack_sockaddr_in6 is "not implemented" (e.g. some Windows runners) |
|
16
|
|
|
|
|
|
|
# or missing entirely. |
|
17
|
1
|
50
|
33
|
1
|
|
11
|
if ( !defined &pack_sockaddr_in6 || !eval { pack_sockaddr_in6( 0, "\0" x 16 ); 1 } ) { |
|
18
|
1
|
|
|
1
|
|
8
|
no warnings 'redefine'; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
327
|
|
|
19
|
|
|
|
|
|
|
*pack_sockaddr_in6 = sub ( $port, $ip, $scope_id = 0, $flowinfo = 0 ) { |
|
20
|
0
|
|
0
|
|
|
|
my $family = eval { AF_INET6() } // 23; # Default to 23 (Win32) if missing, though risky. |
|
|
0
|
|
|
|
|
|
|
|
21
|
0
|
|
|
|
|
|
return pack( 'S n N a16 I', $family, $port, $flowinfo, $ip, $scope_id ); |
|
22
|
0
|
|
|
|
|
|
}; |
|
23
|
|
|
|
|
|
|
*unpack_sockaddr_in6 = sub ($packed) { |
|
24
|
0
|
|
|
|
|
|
my ( $family, $port, $flowinfo, $ip, $scope_id ) = unpack( 'S n N a16 I', $packed ); |
|
25
|
0
|
|
|
|
|
|
return ( $port, $ip, $scope_id, $flowinfo ); |
|
26
|
0
|
|
|
|
|
|
}; |
|
27
|
|
|
|
|
|
|
} |
|
28
|
|
|
|
|
|
|
} |
|
29
|
|
|
|
|
|
|
# |
|
30
|
|
|
|
|
|
|
field $port : param //= 6771; |
|
31
|
|
|
|
|
|
|
field $domain : param //= undef; |
|
32
|
|
|
|
|
|
|
field $socket; |
|
33
|
|
|
|
|
|
|
field %on; |
|
34
|
|
|
|
|
|
|
field $available : reader(is_available) = 0; |
|
35
|
|
|
|
|
|
|
# |
|
36
|
|
|
|
|
|
|
# BEP 14 Multicast group and port |
|
37
|
|
|
|
|
|
|
my $MCAST_ADDR4 = '239.192.152.143'; |
|
38
|
|
|
|
|
|
|
my $MCAST_ADDR6 = 'ff15::efc0:988f'; |
|
39
|
|
|
|
|
|
|
my $MCAST_PORT = 6771; |
|
40
|
|
|
|
|
|
|
# |
|
41
|
|
|
|
|
|
|
ADJUST { |
|
42
|
|
|
|
|
|
|
try { |
|
43
|
|
|
|
|
|
|
require IO::Socket::Multicast; |
|
44
|
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
# Use provided domain or try to auto-select |
|
46
|
|
|
|
|
|
|
if ( defined $domain ) { |
|
47
|
|
|
|
|
|
|
$socket = IO::Socket::Multicast->new( LocalPort => $MCAST_PORT, Proto => 'udp', ReuseAddr => 1, Domain => $domain ) or die $!; |
|
48
|
|
|
|
|
|
|
} |
|
49
|
|
|
|
|
|
|
else { |
|
50
|
|
|
|
|
|
|
$socket = IO::Socket::Multicast->new( LocalPort => $MCAST_PORT, Proto => 'udp', ReuseAddr => 1, Domain => AF_INET6 ) |
|
51
|
|
|
|
|
|
|
// IO::Socket::Multicast->new( LocalPort => $MCAST_PORT, Proto => 'udp', ReuseAddr => 1, Domain => AF_INET ); |
|
52
|
|
|
|
|
|
|
} |
|
53
|
|
|
|
|
|
|
die "Could not create discovery socket: $!" unless $socket; |
|
54
|
|
|
|
|
|
|
$socket->mcast_add($MCAST_ADDR4) if $socket->sockdomain == AF_INET // $socket->sockdomain == AF_INET6; |
|
55
|
|
|
|
|
|
|
$socket->mcast_add($MCAST_ADDR6) if $socket->sockdomain == AF_INET6; |
|
56
|
|
|
|
|
|
|
$available = 1; |
|
57
|
|
|
|
|
|
|
} |
|
58
|
|
|
|
|
|
|
catch ($e) { |
|
59
|
|
|
|
|
|
|
carp "IO::Socket::Multicast not available. Disabled: $e"; |
|
60
|
|
|
|
|
|
|
require IO::Socket::IP; |
|
61
|
|
|
|
|
|
|
$socket = IO::Socket::IP->new( Proto => 'udp', LocalPort => 0 ); |
|
62
|
|
|
|
|
|
|
} |
|
63
|
|
|
|
|
|
|
} |
|
64
|
|
|
|
|
|
|
method on ( $event, $cb ) { push $on{$event}->@*, $cb } |
|
65
|
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
|
method _emit ( $event, @args ) { |
|
67
|
|
|
|
|
|
|
for my $cb ( $on{$event}->@* ) { |
|
68
|
|
|
|
|
|
|
try { $cb->(@args) } catch ($e) { |
|
69
|
|
|
|
|
|
|
carp "Discovery callback for $event failed: $e" |
|
70
|
|
|
|
|
|
|
} |
|
71
|
|
|
|
|
|
|
} |
|
72
|
|
|
|
|
|
|
} |
|
73
|
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
method announce ( $info_hash, $bt_port ) { |
|
75
|
|
|
|
|
|
|
return unless $available; |
|
76
|
|
|
|
|
|
|
my $ih_hex = unpack( 'H*', $info_hash ); |
|
77
|
|
|
|
|
|
|
my $msg = "BT-SEARCH * HTTP/1.1\r\n" . "Host: %s:%d\r\n" . "Port: $bt_port\r\n" . "Infohash: $ih_hex\r\n" . "\r\n\r\n"; |
|
78
|
|
|
|
|
|
|
|
|
79
|
|
|
|
|
|
|
# Announce to IPv4 |
|
80
|
|
|
|
|
|
|
$socket->mcast_send( sprintf( $msg, $MCAST_ADDR4, $MCAST_PORT ), "$MCAST_ADDR4:$MCAST_PORT" ); |
|
81
|
|
|
|
|
|
|
|
|
82
|
|
|
|
|
|
|
# Announce to IPv6 |
|
83
|
|
|
|
|
|
|
$socket->mcast_send( sprintf( $msg, "[$MCAST_ADDR6]", $MCAST_PORT ), "[$MCAST_ADDR6]:$MCAST_PORT" ) if $socket->sockdomain == AF_INET6; |
|
84
|
|
|
|
|
|
|
} |
|
85
|
|
|
|
|
|
|
|
|
86
|
|
|
|
|
|
|
method tick ( $timeout //= 0 ) { |
|
87
|
|
|
|
|
|
|
return unless $available; |
|
88
|
|
|
|
|
|
|
my $sel = IO::Select->new($socket); |
|
89
|
|
|
|
|
|
|
while ( $sel->can_read($timeout) ) { |
|
90
|
|
|
|
|
|
|
my $sender = $socket->recv( my $data, 1024 ); |
|
91
|
|
|
|
|
|
|
$self->_handle_packet( $data, $sender ) if defined $data; |
|
92
|
|
|
|
|
|
|
last if $timeout == 0; |
|
93
|
|
|
|
|
|
|
} |
|
94
|
|
|
|
|
|
|
} |
|
95
|
|
|
|
|
|
|
|
|
96
|
|
|
|
|
|
|
method _handle_packet ( $data, $sender ) { |
|
97
|
|
|
|
|
|
|
if ( $data =~ /^BT-SEARCH/i ) { |
|
98
|
|
|
|
|
|
|
my ($port) = $data =~ /^Port:\s*(\d+)/mi; |
|
99
|
|
|
|
|
|
|
my ($ih_hex) = $data =~ /^Infohash:\s*([a-fA-F0-9]+)/mi; |
|
100
|
|
|
|
|
|
|
if ( $port && $ih_hex ) { |
|
101
|
|
|
|
|
|
|
my $family = sockaddr_family($sender); |
|
102
|
|
|
|
|
|
|
my ( $ip, $scope_id ); |
|
103
|
|
|
|
|
|
|
if ( $family == AF_INET ) { |
|
104
|
|
|
|
|
|
|
( undef, my $ip_bin ) = unpack_sockaddr_in($sender); |
|
105
|
|
|
|
|
|
|
$ip = inet_ntoa($ip_bin); |
|
106
|
|
|
|
|
|
|
} |
|
107
|
|
|
|
|
|
|
elsif ( $family == AF_INET6 ) { |
|
108
|
|
|
|
|
|
|
( undef, my $ip_bin, $scope_id ) = unpack_sockaddr_in6($sender); |
|
109
|
|
|
|
|
|
|
$ip = inet_ntop( AF_INET6, $ip_bin ); |
|
110
|
|
|
|
|
|
|
|
|
111
|
|
|
|
|
|
|
# RFC 6724 / IPv6 Link-Local scope handling |
|
112
|
|
|
|
|
|
|
$ip .= '%' . $scope_id if $ip =~ /^fe80:/i && $scope_id; |
|
113
|
|
|
|
|
|
|
} |
|
114
|
|
|
|
|
|
|
$self->_emit( 'peer_found', { ip => $ip, port => $port, info_hash => pack( 'H*', $ih_hex ) } ) if $ip; |
|
115
|
|
|
|
|
|
|
} |
|
116
|
|
|
|
|
|
|
} |
|
117
|
|
|
|
|
|
|
} |
|
118
|
|
|
|
|
|
|
}; |
|
119
|
|
|
|
|
|
|
1; |