File Coverage

lib/Net/BitTorrent/Tracker/HTTP.pm
Criterion Covered Total %
statement 30 32 93.7
branch 2 4 50.0
condition n/a
subroutine 9 9 100.0
pod n/a
total 41 45 91.1


line stmt bran cond sub pod time code
1 22     22   473775 use v5.40;
  22         80  
2 22     22   139 use feature 'class', 'try';
  22         41  
  22         3112  
3 22     22   134 no warnings 'experimental::class', 'experimental::try';
  22         38  
  22         2365  
4 22     22   12300 class Net::BitTorrent::Tracker::HTTP v2.0.0 : isa(Net::BitTorrent::Tracker::Base) {
  22         63  
  22         2828  
5 22     22   1245 use Net::BitTorrent::Protocol::BEP03::Bencode qw[bdecode];
  22         41  
  22         1548  
6 22     22   10571 use Net::BitTorrent::Protocol::BEP23;
  22         80  
  22         1094  
7 22     22   19033 use HTTP::Tiny;
  22         1284228  
  22         1387  
8 22     22   15650 use URI::Escape qw[uri_escape];
  22         45192  
  22         44351  
9              
10             method build_announce_url ($params) {
11             my $full_url = $self->url;
12             $full_url .= ( $full_url =~ /\?/ ? '&' : '?' );
13             my @query;
14             for my $key ( sort keys %$params ) {
15             next if $key eq 'ua';
16             my $val = $params->{$key} // '';
17             if ( $key eq 'info_hash' || $key eq 'peer_id' ) {
18             $val = join( '', map { sprintf( '%%%02x', ord($_) ) } split( '', $val ) );
19             }
20             else {
21             $val = uri_escape($val);
22             }
23             push @query, "$key=$val";
24             }
25             return $full_url . join( '&', @query );
26             }
27              
28             method build_scrape_url ($infohashes) {
29             my $scrape_url = $self->url;
30             if ( $scrape_url =~ /\/announce$/ ) {
31             $scrape_url =~ s/\/announce$/\/scrape/;
32             }
33             my $full_url = $scrape_url;
34             $full_url .= ( $scrape_url =~ /\?/ ? '&' : '?' );
35             my @query;
36             for my $ih (@$infohashes) {
37             my $val = join( '', map { sprintf( '%%%02x', ord($_) ) } split( '', $ih ) );
38             push @query, "info_hash=$val";
39             }
40             return $full_url . join( '&', @query );
41             }
42              
43             method parse_response ($data) {
44             my $dict = bdecode($data);
45             if ( $dict->{failure_reason} ) {
46             $self->_emit( log => "Tracker failure: $dict->{failure_reason}", level => 'error' );
47             return $dict;
48             }
49             if ( defined $dict->{peers} && !ref $dict->{peers} ) {
50             $dict->{peers} = Net::BitTorrent::Protocol::BEP23::unpack_peers_ipv4( $dict->{peers} );
51             }
52             if ( defined $dict->{peers6} && !ref $dict->{peers6} ) {
53             my $p6 = Net::BitTorrent::Protocol::BEP23::unpack_peers_ipv6( $dict->{peers6} );
54             $dict->{peers} = [ @{ $dict->{peers} // [] }, @$p6 ];
55             }
56             $dict->{peers} //= []; # Ensure it is an array ref
57             return $dict;
58             }
59              
60             method perform_announce ( $params, $cb = undef ) {
61             my $target = $self->build_announce_url($params);
62             if ( $params->{ua} && $params->{ua}->can('get') ) {
63             $params->{ua}->get(
64 1         3 $target,
65 1     1   134 sub ( $res, @ ) {
  1         2  
66 1 50       5 if ( $res->{success} ) {
67 1         3 try {
68 1 50       4 if ($cb) {
69 1         5 $cb->( $self->parse_response( $res->{content} ) );
70             }
71             }
72             catch ($e) {
73 0           $self->_emit( log => "Error in HTTP announce callback: $e", level => 'error' );
74             }
75             }
76             else {
77 0           $self->_emit( log => "Async HTTP error during announce: $res->{status} $res->{reason}\n", level => 'error' );
78             }
79             }
80             );
81             return;
82             }
83             my $http = HTTP::Tiny->new();
84             my $response = $http->get($target);
85             if ( $response->{success} ) {
86             my $parsed = $self->parse_response( $response->{content} );
87             $cb->($parsed) if $cb;
88             return $parsed;
89             }
90             else {
91             $self->_emit( log => "HTTP error during announce: $response->{status} $response->{reason}", level => 'error' );
92             return undef;
93             }
94             }
95              
96             method perform_scrape ( $infohashes, $cb = undef ) {
97             my $target = $self->build_scrape_url($infohashes);
98              
99             # Note: Scrape might not have a 'ua' in $infohashes params,
100             # usually client passes it or we should store it in $self.
101             # For now, if we don't have it, we block.
102             # Real fix: Tracker objects should have a 'ua' field.
103             my $http = HTTP::Tiny->new();
104             my $response = $http->get($target);
105             if ( $response->{success} ) {
106             my $parsed = bdecode( $response->{content} );
107             $cb->($parsed) if $cb;
108             return $parsed;
109             }
110             else {
111             $self->_emit( log => "HTTP scrape error: $response->{status} $response->{reason}", level => 'error' );
112             return undef;
113             }
114             }
115             } 1;