File Coverage

blib/lib/Net/HTTP2/Client/Connection.pm
Criterion Covered Total %
statement 106 148 71.6
branch 10 32 31.2
condition 3 11 27.2
subroutine 26 31 83.8
pod 2 2 100.0
total 147 224 65.6


line stmt bran cond sub pod time code
1             package Net::HTTP2::Client::Connection;
2              
3 1     1   374 use strict;
  1         2  
  1         20  
4 1     1   4 use warnings;
  1         1  
  1         22  
5              
6             =encoding utf-8
7              
8             =head1 NAME
9              
10             Net::HTTP2::Client::Connection - Base class for individual HTTP/2
11             client connection
12              
13             =head1 SYNOPSIS
14              
15             See:
16              
17             =over
18              
19             =item * L
20              
21             =item * L
22              
23             =item * L
24              
25             =back
26              
27             =head1 DESCRIPTION
28              
29             This implements a non-blocking, B HTTP/2 client.
30             It’s a base class; the actual class you’ll use will depend on your
31             event loop interface (see L).
32              
33             If you want a full-featured client (that honors redirections),
34             see L.
35              
36             =head1 CONNECTION LONGEVITY
37              
38             TCP connections are kept open as long as instances of this class live.
39              
40             =head1 HTTP REDIRECTION
41              
42             This class’s design would facilitate HTTP redirects only in the case
43             where the target of the redirect is the same server that answers the
44             initial request. (e.g., we could honor
45             C or
46             C, but not
47             C.)
48              
49             To avoid that inconsistency, this class purposely omits HTTP redirection.
50             See L for an interface that implements redirection.
51              
52             =cut
53              
54             #----------------------------------------------------------------------
55              
56 1     1   3 use Carp ();
  1         2  
  1         9  
57 1     1   3 use Scalar::Util ();
  1         1  
  1         9  
58              
59 1     1   378 use Protocol::HTTP2::Client ();
  1         28110  
  1         16  
60 1     1   5 use Protocol::HTTP2::Constants ();
  1         1  
  1         10  
61              
62 1     1   5 use Net::HTTP2::Constants ();
  1         1  
  1         11  
63 1     1   338 use Net::HTTP2::X ();
  1         3  
  1         16  
64 1     1   290 use Net::HTTP2::Response ();
  1         2  
  1         15  
65 1     1   300 use Net::HTTP2::PartialResponse ();
  1         2  
  1         15  
66 1     1   318 use Net::HTTP2::RejectorRegistry ();
  1         2  
  1         18  
67              
68             # Lazy-load so that Mojo doesn’t load Promise::ES6.
69 1     1   4 use constant _PROMISE_CLASS => 'Promise::ES6';
  1         2  
  1         45  
70              
71             use constant {
72 1         41 _DEBUG => 1,
73              
74             _ALPN_PROTOS => ['h2'],
75 1     1   4 };
  1         2  
76              
77 1         190 use constant _INIT_OPTS => (
78             'port',
79             'tls_verify',
80 1     1   5 );
  1         1  
81              
82             my %TLS_VERIFY = (
83             none => 0,
84             peer => 1,
85             );
86              
87             sub _parse_args {
88 2     2   3 my $class = shift;
89 2         2 my $peer = shift;
90              
91 2         4 my %raw_opts = @_;
92              
93 2         13 my %opts = map { $_ => delete $raw_opts{$_} } $class->_INIT_OPTS();
  4         12  
94              
95 2 50       8 if (my @extra = sort keys %raw_opts) {
96 0         0 Carp::croak "Unknown: @extra";
97             }
98              
99 2         3 my @extra_args;
100              
101 2         3 my $port = delete $opts{'port'};
102              
103 2 50       5 if (my $tls_verify = delete $opts{'tls_verify'}) {
104 0         0 my $val = $TLS_VERIFY{$tls_verify};
105 0 0       0 if (!defined $val) {
106 0         0 Carp::croak "Bad `tls_verify`: $tls_verify";
107             }
108              
109 0         0 push @extra_args, tls_verify_yn => $val;
110             }
111              
112 2         10 push @extra_args, $class->_parse_event_args(%opts);
113              
114 2         7 return ( $peer, $port, @extra_args );
115             }
116              
117 1     1   6 use constant _parse_event_args => ();
  1         2  
  1         1021  
118              
119             #----------------------------------------------------------------------
120              
121             =head1 METHODS
122              
123             =head2 $obj = I->new( $HOSTNAME_OR_IP, %OPTS )
124              
125             Instantiates I.
126              
127             %OPTS will depend on the I, but can always include:
128              
129             =over
130              
131             =item * C - The TCP port to connect to. Defaults to 443.
132              
133             =item * C - Either C (default) or C.
134              
135             =back
136              
137             =cut
138              
139             sub new {
140 2     2 1 7 my ($class, @args) = @_;
141              
142             # TODO: Allow special options to be passed for TLS,
143             # e.g., verification options.
144              
145 2         9 my ($host, $port, @extra_args) = $class->_parse_args(@args);
146              
147 2   50     9 $port ||= Net::HTTP2::Constants::HTTPS_PORT;
148              
149 2         9 my $rejectors_obj = Net::HTTP2::RejectorRegistry->new();
150              
151 2         12 my $self = bless {
152             host => $host,
153             port => $port,
154              
155             pid => $$,
156              
157             rejectors => $rejectors_obj,
158              
159             @extra_args,
160             }, $class;
161              
162             my @client_args = (
163             keepalive => 1,
164              
165             # H2 server push is, as of 2022, rarely used,
166             # so we won’t support it here.
167              
168             on_error => sub {
169 0     0   0 my $errnum = shift;
170              
171 0         0 $rejectors_obj->reject_all(
172             Net::HTTP2::X->create('HTTP2', $errnum),
173             );
174             },
175 2         9 );
176              
177 2         4 if (_DEBUG) {
178             push @client_args, (
179             on_change_state => sub {
180 4     4   1237 my ( $stream_id, $previous_state, $current_state ) = @_;
181 4         13 _debug( change_State => @_ );
182             },
183 2         5 );
184             }
185              
186 2         11 $self->{'h2'} = Protocol::HTTP2::Client->new(@client_args);
187              
188 2         139 return $self;
189             }
190              
191             =head2 promise($result) = I->request( $METHOD, $PATH_AND_QUERY, %OPTS )
192              
193             Sends an HTTP/2 request.
194              
195             Returns a promise (an instance of , unless
196             otherwise noted in the subclass) that resolves to a L
197             instance.
198              
199             %OPTS can be:
200              
201             =over
202              
203             =item * C - Request headers, as in L’s C.
204              
205             =item * C - Request content, as in L’s C.
206              
207             =item * C - A code reference that fires as each chunk of the
208             HTTP response arrives. The code reference always receives a
209             L instance.
210              
211             =back
212              
213             On failure (I including valid HTTP responses!), the promise rejects
214             with an instance of an appropriate L class,
215             e.g., L.
216              
217             =cut
218              
219             sub request {
220 2     2 1 19 my ($self, $method, $path_query, %opts) = @_;
221              
222 2         4 my @headers;
223              
224 2 50       5 if ($opts{'headers'}) {
225 0         0 for my $name (keys %{$opts{'headers'}}) {
  0         0  
226 0         0 my $val = $opts{'headers'}{$name};
227              
228 0 0       0 if ('ARRAY' eq ref $val) {
229 0         0 push @headers, $name => $_ for @$val;
230             }
231             else {
232 0         0 push @headers, $name => $val;
233             }
234             }
235             }
236              
237 2         3 my @extra_args;
238              
239 2 50       4 if (defined $opts{'content'}) {
240 0         0 push @extra_args, data => $opts{'content'};
241             }
242              
243 2 50       5 if (my $data_cr = $opts{'on_data'}) {
244             push @extra_args, (
245             on_headers => sub {
246 0     0   0 my $canceled;
247              
248 0         0 $data_cr->( Net::HTTP2::PartialResponse->new(\$canceled, $_[0], q<>) );
249              
250 0   0     0 return !$canceled || undef;
251             },
252             on_data => sub {
253 0     0   0 my $canceled;
254              
255 0         0 $data_cr->( Net::HTTP2::PartialResponse->new(\$canceled, @_[1, 0]) );
256              
257 0   0     0 return !$canceled || undef;
258             },
259 0         0 );
260             }
261              
262 2         3 my $rejectors_obj = $self->{'rejectors'};
263              
264 2         3 my $rejector_str;
265              
266             return $self->_get_promise_class()->new( sub {
267 2     2   42 my ($res, $rej) = @_;
268              
269 2         6 $rejector_str = $rejectors_obj->add($rej);
270              
271 2         4 $self->_start_io_if_needed(@{$self}{'host', 'port', 'h2'});
  2         8  
272              
273 2         15460 my $weak_h2 = $self->{'h2'};
274 2         7 Scalar::Util::weaken($weak_h2);
275              
276 2         4 my $authty = $self->{'host'};
277              
278             # Some services act differently if :443 is postfixed unnecessarily.
279             # For example, as of this writing perl.org will give a 404 rather
280             # than the redirect to www.perl.org that it should give.
281             #
282 2 50       4 $authty .= ":$self->{'port'}" if $self->{'port'} != Net::HTTP2::Constants::HTTPS_PORT;
283              
284             $self->{'h2'}->request(
285             ':scheme' => 'https',
286             ':authority' => $authty,
287             ':path' => $path_query,
288             ':method' => $method,
289              
290             headers => \@headers,
291              
292             on_done => sub {
293 2         98 $res->( Net::HTTP2::Response->new(@_) );
294             },
295              
296             on_error => sub {
297 0         0 my $errnum = shift;
298 0         0 $rej->( Net::HTTP2::X->create('HTTP2', $errnum) );
299             },
300              
301 2         16 @extra_args,
302             );
303              
304 2         104 $self->_send_pending_frames();
305             } )->finally( sub {
306 2     2   111 $rejectors_obj->remove($rejector_str);
307 2         19 } );
308             }
309              
310             # =head2 $obj = I->close()
311             #
312             # Tells the server we are ending the connection. Automatically called
313             # when I is DESTROY()ed, so you usually don’t need to call this.
314             #
315             # Returns a promise that resolves when the HTTP/2 GOAWAY frame is sent.
316             #
317             # =cut
318             #
319             # sub close {
320             # my ($self) = @_;
321             #
322             # if ($self->{'_close_called'}) {
323             # Carp::croak "Duplicate close() call\n";
324             # }
325             #
326             # $self->{'_close_called'} = 1;
327             #
328             # my $close_cr = sub {
329             # my $res = shift;
330             #
331             # $self->{'h2'}->close();
332             #
333             # $self->_send_pending_frames($res || ());
334             # };
335             #
336             # if (defined wantarray) {
337             # my $rejector_str;
338             #
339             # my $rejectors_obj = $self->{'rejectors'};
340             #
341             # return $self->{'_close_p'} ||= $self->_get_promise_class()->new( sub {
342             # my ($res, $rej) = @_;
343             #
344             # $rejector_str = $rejectors_obj->add($rej);
345             #
346             # $close_cr->($res);
347             # } )->finally( sub {
348             # $rejectors_obj->remove($rejector_str);
349             # } );
350             # }
351             # else {
352             # $close_cr->();
353             # }
354             # }
355              
356             #----------------------------------------------------------------------
357              
358             sub DESTROY {
359 2     2   1514 my $self = shift;
360              
361 2         8 $self->_h2_close();
362              
363 2 50 33     22 if (${^GLOBAL_PHASE} && ${^GLOBAL_PHASE} eq 'DESTRUCT') {
364 0 0       0 if ($self->{'pid'} == $$) {
365 0         0 warn "$self: DESTROY() at global destruction; memory leak likely!\n";
366             }
367             }
368             }
369              
370             sub _h2_close {
371 2     2   3 my $self = shift;
372              
373 2         9 $self->{'h2'}->close();
374 2         141 $self->_send_pending_frames();
375             }
376              
377             sub _on_stream_error {
378 0     0   0 my ($self, $err) = @_;
379              
380 0         0 my $rejectors = $self->{'rejectors'};
381              
382 0 0       0 if ($rejectors->count()) {
383 0         0 $rejectors->reject_all($err);
384             }
385             else {
386 0         0 warn $err;
387             }
388             }
389              
390             sub _on_stream_close {
391 0     0   0 my $self = shift;
392              
393 0         0 my $rejectors = $self->{'rejectors'};
394              
395 0 0       0 if ($rejectors->count()) {
    0          
396 0         0 $rejectors->reject_all("Unexpected close of TCP stream!");
397             }
398             elsif (!$self->{'_close_called'}) {
399 0         0 print "TCP peer closed\n";
400             }
401             }
402              
403             sub _send_pending_frames {
404 4     4   8 my ($self, $cb) = @_;
405              
406 4 50       9 if ($cb) {
407 0         0 my @frames;
408              
409 0         0 while (my $frame = $self->{'h2'}->next_frame()) {
410 0         0 push @frames, $frame;
411             }
412              
413 0 0       0 if (@frames) {
414 0         0 my $last_frame = pop @frames;
415              
416 0         0 $self->_write_frame($_) for @frames;
417 0         0 $self->_write_frame($last_frame, $cb);
418             }
419             }
420             else {
421 4         11 while (my $frame = $self->{'h2'}->next_frame()) {
422 12         647 $self->_write_frame($frame);
423             }
424             }
425              
426 4         163 return;
427             }
428              
429             sub _debug {
430 4     4   170 print STDERR "DEBUG: @_\n" if _DEBUG;
431             }
432              
433             sub _get_promise_class {
434 2     2   3 my ($self) = @_;
435              
436 2         9 my $promise_class = $self->_PROMISE_CLASS();
437              
438 2         9 local ($!, $@);
439 2 100 50     64 eval "require $promise_class; 1" || die if !$promise_class->can('new');
440              
441 2         20 return $promise_class;
442             }
443              
444             1;