File Coverage

blib/lib/App/MatrixTool/HTTPClient.pm
Criterion Covered Total %
statement 36 111 32.4
branch 0 26 0.0
condition 0 17 0.0
subroutine 12 24 50.0
pod 4 6 66.6
total 52 184 28.2


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2015 -- leonerd@leonerd.org.uk
5              
6             package App::MatrixTool::HTTPClient;
7              
8 1     1   931 use strict;
  1         1  
  1         23  
9 1     1   3 use warnings;
  1         1  
  1         34  
10              
11             our $VERSION = '0.07';
12              
13 1     1   4 use Carp;
  1         1  
  1         57  
14 1     1   4 use Future 0.33; # ->catch
  1         25  
  1         23  
15 1     1   459 use Future::Utils qw( repeat_until_success );
  1         1478  
  1         51  
16 1     1   761 use IO::Async::Loop;
  1         34182  
  1         44  
17 1     1   577 use IO::Async::Resolver 0.68; # failure details
  1         30816  
  1         35  
18 1     1   864 use IO::Async::Resolver::DNS 0.06 qw( ERR_NO_HOST ERR_NO_ADDRESS ); # Future return with failure details
  1         39358  
  1         63  
19 1     1   8 use JSON qw( encode_json decode_json );
  1         1  
  1         8  
20 1     1   110 use URI;
  1         1  
  1         27  
21              
22 1     1   4 use Socket qw( getnameinfo NI_NUMERICHOST SOCK_RAW );
  1         1  
  1         46  
23              
24 1     1   3 use constant DEFAULT_MATRIX_PORT => 8448;
  1         1  
  1         1311  
25              
26             =head1 NAME
27              
28             C - HTTP client helper for L
29              
30             =head1 DESCRIPTION
31              
32             Provides helper methods to perform HTTP client operations that may be required
33             by commands of L.
34              
35             =cut
36              
37             sub new
38             {
39 0     0 0   my $class = shift;
40              
41 0           my $loop = IO::Async::Loop->new; # magic constructor
42              
43 0           return bless {
44             @_,
45             resolver => $loop->resolver,
46             loop => $loop,
47             }, $class;
48             }
49              
50             =head1 METHODS
51              
52             =cut
53              
54             sub ua
55             {
56 0     0 0   my $self = shift;
57 0   0       return $self->{ua} ||= do {
58 0           require IO::Async::SSL;
59 0           require Net::Async::HTTP;
60 0           require HTTP::Request;
61              
62 0           my $ua = Net::Async::HTTP->new(
63             SSL_verify_mode => 0,
64             fail_on_error => 1, # turn 4xx/5xx errors into Future failures
65             );
66              
67 0           $self->{loop}->add( $ua );
68 0           $ua;
69             };
70             }
71              
72             =head2 resolve_matrix
73              
74             @res = $client->resolve_matrix( $server_name )->get
75              
76             Returns a list of C references. Each has at least the keys C
77             and C. These should be tried in order until one succeeds.
78              
79             =cut
80              
81             sub resolve_matrix
82             {
83 0     0 1   my $self = shift;
84 0           my ( $server_name ) = @_;
85              
86 0 0         if( my ( $host, $port ) = $server_name =~ m/^(\S+):(\d+)$/ ) {
87 0           return Future->done( { target => $host, port => $port } );
88             }
89              
90             $self->{resolver}->res_query(
91             dname => "_matrix._tcp.$server_name",
92             type => "SRV",
93             )->then( sub {
94 0     0     my ( undef, @srvs ) = @_;
95 0           Future->done( @srvs );
96             })->catch_with_f(
97             resolve => sub {
98 0     0     my ( $f, $message, undef, undef, $errnum ) = @_;
99 0 0 0       return $f unless $errnum == ERR_NO_HOST or $errnum == ERR_NO_ADDRESS;
100              
101 0           Future->done( { target => $server_name, port => DEFAULT_MATRIX_PORT } );
102             }
103 0           );
104             }
105              
106             =head2 resolve_addr
107              
108             @addrs = $client->resolve_addr( $hostname )->get
109              
110             Returns a list of human-readable string representations of the IP addresses
111             resolved by the given hostname.
112              
113             =cut
114              
115             sub resolve_addr
116             {
117 0     0 1   my $self = shift;
118 0           my ( $host ) = @_;
119              
120             $self->{resolver}->getaddrinfo(
121             host => $host,
122             service => "",
123             family => $self->{family},
124             socktype => SOCK_RAW,
125             )->then( sub {
126 0     0     my @res = @_;
127             return Future->done(
128 0           map { ( getnameinfo( $_->{addr}, NI_NUMERICHOST ) )[1] } @res
  0            
129             );
130 0           });
131             }
132              
133             =head2 request
134              
135             $response = $client->request( server => $name, method => $method, path => $path, ... )->get
136              
137             Performs an HTTPS request to the given server, by resolving the server name
138             using the C method first, thus obeying its published C
139             records.
140              
141             =cut
142              
143             sub request
144             {
145 0     0 1   my $self = shift;
146 0           my %params = @_;
147              
148 0           my $uri = URI->new;
149 0           $uri->path( $params{path} );
150 0 0         $uri->query_form( %{ $params{params} } ) if $params{params};
  0            
151              
152 0           my $ua = $self->ua;
153             my $req = $params{request} // HTTP::Request->new( $params{method} => $uri,
154 0   0       [ Host => $params{server} ],
155             );
156 0           $req->protocol( "HTTP/1.1" );
157              
158 0 0         if( defined $params{content} ) {
159 0 0         if( ref $params{content} ) {
160 0           $req->content( encode_json( delete $params{content} ) );
161 0           $req->header( Content_type => "application/json" );
162             }
163             else {
164 0           $req->content( delete $params{content} );
165             $req->header( Content_type => delete $params{content_type} //
166 0   0       croak "Non-reference content needs 'content_type'" );
167             }
168 0           $req->header( Content_length => length $req->content );
169             }
170              
171 0 0         if( $self->{print_request} ) {
172 0           print STDERR "Sending HTTP request to $params{server}\n";
173 0           print STDERR " $_\n" for split m/\n/, $req->as_string( "\n" );
174             }
175              
176 0           my $path = $req->uri->path;
177              
178             # Different kinds of request need resolving either as a client or as a
179             # federated server
180 0           my $resolve_f;
181 0 0         if( $path =~ m{^/_matrix/key/} ) {
    0          
182             $resolve_f = $self->resolve_matrix( $params{server} )->then( sub {
183 0     0     my @res = @_;
184             Future->done( map {
185 0           { SSL => 1, host => $_->{target}, port => $_->{port}, family => $self->{family} }
186 0           } @res );
187 0           });
188             }
189             elsif( $path =~ m{^/_matrix/(?:client|media)/} ) {
190 0 0         my ( $server, $port ) = $params{server} =~ m/^([^:]+)(?::(\d+))?$/ or
191             die "Unable to parse server '$params{server}'\n";
192             $resolve_f = Future->done(
193             { SSL => 1, port => $port // 443, host => $server, family => $self->{family} }
194 0   0       );
195             }
196             else {
197 0           die "Unsure how to resolve server for path $path\n";
198             }
199              
200             $resolve_f->then( sub {
201 0     0     my @res = @_;
202              
203             repeat_until_success {
204 0           my $res = shift;
205 0 0         print STDERR "Using target $res->{host} port $res->{port}\n" if $self->{print_request};
206              
207             $ua->do_request(
208             %params,
209             %$res,
210             request => $req,
211             )->on_done( sub {
212 0           my ( $response ) = @_;
213 0 0         if( $self->{print_response} ) {
214 0           print STDERR "Received HTTP response:\n";
215 0           print STDERR " $_\n" for split m/\n/, $response->as_string( "\n" );
216             }
217             })->on_fail( sub {
218 0           my ( undef, $name, $response ) = @_;
219 0 0 0       if( $name eq "http" and $self->{print_response} ) {
220 0           print STDERR "Received HTTP response:\n";
221 0           print STDERR " $_\n" for split m/\n/, $response->as_string( "\n" );
222             }
223 0           });
224 0           } foreach => \@res;
225 0           });
226             }
227              
228             =head2 request_json
229              
230             ( $body, $response ) = $client->request_json( ... )
231              
232             A small wrapper around C that decodes the returned body as JSON.
233              
234             =cut
235              
236             sub request_json
237             {
238 0     0 1   my $self = shift;
239             $self->request( @_ )->then( sub {
240 0     0     my ( $response ) = @_;
241              
242 0 0         $response->content_type eq "application/json" or
243             return Future->fail( "Expected an application/json response body", matrix => );
244              
245 0           Future->done( decode_json( $response->decoded_content ), $response );
246 0           });
247             }
248              
249             =head1 AUTHOR
250              
251             Paul Evans
252              
253             =cut
254              
255             0x55AA;