File Coverage

blib/lib/Protocol/HTTP2/Client.pm
Criterion Covered Total %
statement 91 111 81.9
branch 40 52 76.9
condition 6 14 42.8
subroutine 19 20 95.0
pod 8 9 88.8
total 164 206 79.6


line stmt bran cond sub pod time code
1             package Protocol::HTTP2::Client;
2 7     7   55101 use strict;
  7         10  
  7         164  
3 7     7   22 use warnings;
  7         8  
  7         137  
4 7     7   2098 use Protocol::HTTP2::Connection;
  7         14  
  7         196  
5 7         1517 use Protocol::HTTP2::Constants qw(:frame_types :flags :states :endpoints
6 7     7   31 :errors);
  7         6  
7 7     7   34 use Protocol::HTTP2::Trace qw(tracer);
  7         8  
  7         235  
8 7     7   23 use Carp;
  7         10  
  7         304  
9 7     7   26 use Scalar::Util ();
  7         6  
  7         6448  
10              
11             =encoding utf-8
12              
13             =head1 NAME
14              
15             Protocol::HTTP2::Client - HTTP/2 client
16              
17             =head1 SYNOPSIS
18              
19             use Protocol::HTTP2::Client;
20              
21             # Create client object
22             my $client = Protocol::HTTP2::Client->new;
23              
24             # Prepare first request
25             $client->request(
26              
27             # HTTP/2 headers
28             ':scheme' => 'http',
29             ':authority' => 'localhost:8000',
30             ':path' => '/',
31             ':method' => 'GET',
32              
33             # HTTP/1.1 headers
34             headers => [
35             'accept' => '*/*',
36             'user-agent' => 'perl-Protocol-HTTP2/0.13',
37             ],
38              
39             # Callback when receive server's response
40             on_done => sub {
41             my ( $headers, $data ) = @_;
42             ...
43             },
44             );
45              
46             # Protocol::HTTP2 is just HTTP/2 protocol decoder/encoder
47             # so you must create connection yourself
48              
49             use AnyEvent;
50             use AnyEvent::Socket;
51             use AnyEvent::Handle;
52             my $w = AnyEvent->condvar;
53              
54             # Plain-text HTTP/2 connection
55             tcp_connect 'localhost', 8000, sub {
56             my ($fh) = @_ or die "connection failed: $!\n";
57            
58             my $handle;
59             $handle = AnyEvent::Handle->new(
60             fh => $fh,
61             autocork => 1,
62             on_error => sub {
63             $_[0]->destroy;
64             print "connection error\n";
65             $w->send;
66             },
67             on_eof => sub {
68             $handle->destroy;
69             $w->send;
70             }
71             );
72              
73             # First write preface to peer
74             while ( my $frame = $client->next_frame ) {
75             $handle->push_write($frame);
76             }
77              
78             # Receive servers frames
79             # Reply to server
80             $handle->on_read(
81             sub {
82             my $handle = shift;
83              
84             $client->feed( $handle->{rbuf} );
85              
86             $handle->{rbuf} = undef;
87             while ( my $frame = $client->next_frame ) {
88             $handle->push_write($frame);
89             }
90              
91             # Terminate connection if all done
92             $handle->push_shutdown if $client->shutdown;
93             }
94             );
95             };
96              
97             $w->recv;
98              
99             =head1 DESCRIPTION
100              
101             Protocol::HTTP2::Client is HTTP/2 client library. It's intended to make
102             http2-client implementations on top of your favorite event-loop.
103              
104             =head2 METHODS
105              
106             =head3 new
107              
108             Initialize new client object
109              
110             my $client = Procotol::HTTP2::Client->new( %options );
111              
112             Availiable options:
113              
114             =over
115              
116             =item on_push => sub {...}
117              
118             If server send push promise this callback will be invoked
119              
120             on_push => sub {
121             # received PUSH PROMISE headers
122             my $pp_header = shift;
123             ...
124            
125             # if we want reject this push
126             # return undef
127            
128             # if we want to accept pushed resource
129             # return callback to receive data
130             return sub {
131             my ( $headers, $data ) = @_;
132             ...
133             }
134             },
135              
136             =item upgrade => 0|1
137              
138             Use HTTP/1.1 Upgrade to upgrade protocol from HTTP/1.1 to HTTP/2. Upgrade
139             possible only on plain (non-tls) connection. Default value is 0.
140              
141             See
142             L
143              
144             =item keepalive => 0|1
145              
146             Keep connection alive after requests. Default value is 0. Don't forget to
147             explicitly call close method if set this to true.
148              
149             =item on_error => sub {...}
150              
151             Callback invoked on protocol errors
152              
153             on_error => sub {
154             my $error = shift;
155             ...
156             },
157              
158             =item on_change_state => sub {...}
159              
160             Callback invoked every time when http/2 streams change their state.
161             See
162             L
163              
164             on_change_state => sub {
165             my ( $stream_id, $previous_state, $current_state ) = @_;
166             ...
167             },
168              
169             =back
170              
171             =cut
172              
173             sub new {
174 19     19 1 24026 my ( $class, %opts ) = @_;
175             my $self = {
176             con => undef,
177             input => '',
178             active_streams => 0,
179             keepalive => exists $opts{keepalive}
180             ? delete $opts{keepalive}
181             : 0,
182             settings => exists $opts{settings} ? $opts{settings} : {},
183 19 100       1847 };
    100          
184              
185 19 50       1631 if ( exists $opts{on_push} ) {
186 0         0 Scalar::Util::weaken( my $self = $self );
187              
188 0         0 my $cb = delete $opts{on_push};
189             $opts{on_new_peer_stream} = sub {
190 0     0   0 my $stream_id = shift;
191 0         0 my $pp_headers;
192 0         0 $self->active_streams(+1);
193              
194             $self->{con}->stream_cb(
195             $stream_id,
196             RESERVED,
197             sub {
198             my $res =
199 0         0 $cb->( $self->{con}->stream_pp_headers($stream_id) );
200 0 0 0     0 if ( $res && ref $cb eq 'CODE' ) {
201             $self->{con}->stream_cb(
202             $stream_id,
203             CLOSED,
204             sub {
205             $res->(
206             $self->{con}->stream_headers($stream_id),
207 0         0 $self->{con}->stream_data($stream_id),
208             );
209 0         0 $self->active_streams(-1);
210             }
211 0         0 );
212             }
213             else {
214             $self->{con}
215 0         0 ->stream_error( $stream_id, REFUSED_STREAM );
216 0         0 $self->active_streams(-1);
217             }
218             }
219 0         0 );
220 0         0 };
221             }
222              
223 19         1676 $self->{con} = Protocol::HTTP2::Connection->new( CLIENT, %opts );
224 19         3158 bless $self, $class;
225             }
226              
227             sub active_streams {
228 70     70 0 2420 my $self = shift;
229 70   50     2481 my $add = shift || 0;
230 70         2455 $self->{active_streams} += $add;
231             $self->{con}->finish
232             unless $self->{active_streams} > 0
233 70 100 66     4154 || $self->{keepalive};
234             }
235              
236             =head3 request
237              
238             Prepare HTTP/2 request.
239              
240             $client->request(
241              
242             # HTTP/2 headers
243             ':scheme' => 'http',
244             ':authority' => 'localhost:8000',
245             ':path' => '/items',
246             ':method' => 'POST',
247              
248             # HTTP/1.1 headers
249             headers => [
250             'content-type' => 'application/x-www-form-urlencoded',
251             'user-agent' => 'perl-Protocol-HTTP2/0.06',
252             ],
253              
254             # Callback when receive server's response
255             on_done => sub {
256             my ( $headers, $data ) = @_;
257             ...
258             },
259              
260             # Callback when receive stream reset
261             on_error => sub {
262             my $error_code = shift;
263             },
264              
265             # Body of POST request
266             data => "hello=world&test=done",
267             );
268              
269             You can chaining request one by one:
270              
271             $client->request( 1-st request )->request( 2-nd request );
272              
273             Available callbacks:
274              
275             =over
276              
277             =item on_done => sub {...}
278              
279             Invoked when full servers response is available
280              
281             on_done => sub {
282             my ( $headers, $data ) = @_;
283             ...
284             },
285              
286             =item on_headers => sub {...}
287              
288             Invoked as soon as headers have been successfully received from the server
289              
290             on_headers => sub {
291             my $headers = shift;
292             ...
293              
294             # if we want reject any data
295             # return undef
296              
297             # continue
298             return 1
299             }
300              
301             =item on_data => sub {...}
302              
303             If specified all data will be passed to this callback instead if on_done.
304             on_done will receive empty string.
305              
306             on_data => sub {
307             my ( $partial_data, $headers ) = @_;
308             ...
309              
310             # if we want cancel download
311             # return undef
312              
313             # continue downloading
314             return 1
315             }
316              
317             =item on_error => sub {...}
318              
319             Callback invoked on stream errors
320              
321             on_error => sub {
322             my $error = shift;
323             ...
324             }
325              
326             =back
327              
328             =cut
329              
330             my @must = (qw(:authority :method :path :scheme));
331              
332             sub request {
333 41     41 1 5954 my ( $self, %h ) = @_;
334 41         1642 my @miss = grep { !exists $h{$_} } @must;
  164         12655  
335 41 50       1666 croak "Missing fields in request: @miss" if @miss;
336              
337 41         1638 my $con = $self->{con};
338              
339 41         1720 my $stream_id = $con->new_stream;
340 41 100       1634 unless ( defined $stream_id ) {
341 2 100       4 if ( exists $con->{on_error} ) {
342 1         3 $con->{on_error}->(PROTOCOL_ERROR);
343 1         356 return $self;
344             }
345             else {
346 1         157 croak "Can't create new stream, connection is closed";
347             }
348             }
349              
350 39         1651 $self->active_streams(+1);
351              
352 39 100 66     1662 if ( $con->upgrade && !exists $self->{sent_upgrade} ) {
353             $con->enqueue_raw(
354             $con->upgrade_request(
355 4         71 ( map { $_ => $h{$_} } @must ),
356 1 50       8 headers => exists $h{headers} ? $h{headers} : []
357             )
358             );
359 1         7 $self->{sent_upgrade} = 1;
360 1         7 $con->stream_state( $stream_id, HALF_CLOSED );
361             }
362             else {
363 38 100       1943 if ( !$con->preface ) {
364             $con->enqueue_raw( $con->preface_encode ),
365 18         1645 $con->enqueue( SETTINGS, 0, 0, $self->{settings} );
366 18         1592 $con->preface(1);
367             }
368              
369             $con->send_headers(
370             $stream_id,
371             [
372 152         12760 ( map { $_ => $h{$_} } @must ),
373 36         3280 exists $h{headers} ? @{ $h{headers} } : ()
374             ],
375 38 100       1640 exists $h{data} ? 0 : 1
    100          
376             );
377 38 100       3275 $con->send_data( $stream_id, $h{data}, 1 ) if exists $h{data};
378             }
379              
380 39         1673 Scalar::Util::weaken $self;
381 39         1612 Scalar::Util::weaken $con;
382              
383             $con->stream_cb(
384             $stream_id,
385             CLOSED,
386             sub {
387 31 50 33 31   860 if ( exists $h{on_error} && $con->stream_reset($stream_id) ) {
388 0         0 $h{on_error}->( $con->stream_reset($stream_id) );
389             }
390             else {
391 31         861 $h{on_done}->(
392             $con->stream_headers($stream_id),
393             $con->stream_data($stream_id),
394             );
395             }
396 31         9993 $self->active_streams(-1);
397             }
398 39 100       1814 ) if exists $h{on_done};
399              
400             $con->stream_frame_cb(
401             $stream_id,
402             HEADERS,
403             sub {
404 5     5   810 my $res = $h{on_headers}->( $_[0] );
405 5 50       5291 return if $res;
406 0         0 $con->stream_error( $stream_id, REFUSED_STREAM );
407             }
408 39 100       1690 ) if exists $h{on_headers};
409              
410             $con->stream_frame_cb(
411             $stream_id,
412             DATA,
413             sub {
414 5     5   811 my $res = $h{on_data}->( $_[0], $con->stream_headers($stream_id), );
415 5 50       3649 return if $res;
416 5         836 $con->stream_error( $stream_id, REFUSED_STREAM );
417             }
418 39 100       1651 ) if exists $h{on_data};
419              
420 39         3210 return $self;
421             }
422              
423             =head3 keepalive
424              
425             Keep connection alive after requests
426              
427             my $bool = $client->keepalive;
428             $client = $client->keepalive($bool);
429              
430             =cut
431              
432             sub keepalive {
433 1     1 1 2 my $self = shift;
434             return @_
435             ? scalar( $self->{keepalive} = shift, $self )
436 1 50       132 : $self->{keepalive};
437             }
438              
439             =head3 shutdown
440              
441             Get connection status:
442              
443             =over
444              
445             =item 0 - active
446              
447             =item 1 - closed (you can terminate connection)
448              
449             =back
450              
451             =cut
452              
453             sub shutdown {
454 7     7 1 37 shift->{con}->shutdown;
455             }
456              
457             =head3 close
458              
459             Explicitly close connection (send GOAWAY frame). This is requred if client
460             has keepalive option enabled.
461              
462             =cut
463              
464             sub close {
465 1     1 1 11 shift->{con}->finish;
466             }
467              
468             =head3 next_frame
469              
470             get next frame to send over connection to server.
471             Returns:
472              
473             =over
474              
475             =item undef - on error
476              
477             =item 0 - nothing to send
478              
479             =item binary string - encoded frame
480              
481             =back
482              
483             # Example
484             while ( my $frame = $client->next_frame ) {
485             syswrite $fh, $frame;
486             }
487              
488             =cut
489              
490             sub next_frame {
491 212     212 1 44702 my $self = shift;
492 212         13100 my $frame = $self->{con}->dequeue;
493 212 100       13137 tracer->debug("send one frame to wire\n") if $frame;
494 212         26009 return $frame;
495             }
496              
497             =head3 feed
498              
499             Feed decoder with chunks of server's response
500              
501             sysread $fh, $binary_data, 4096;
502             $client->feed($binary_data);
503              
504             =cut
505              
506             sub feed {
507 117     117 1 48805 my ( $self, $chunk ) = @_;
508 117         8090 $self->{input} .= $chunk;
509 117         7958 my $offset = 0;
510 117         8321 my $len;
511 117         8046 my $con = $self->{con};
512 117         8090 tracer->debug( "got " . length($chunk) . " bytes on a wire\n" );
513 117 100       8188 if ( $con->upgrade ) {
514 1         18 $len = $con->decode_upgrade_response( \$self->{input}, $offset );
515 1 50       7 $con->shutdown(1) unless defined $len;
516 1 50       4 return unless $len;
517 0         0 $offset += $len;
518 0         0 $con->upgrade(0);
519 0         0 $con->enqueue_raw( $con->preface_encode );
520 0         0 $con->preface(1);
521             }
522 116         8212 while ( $len = $con->frame_decode( \$self->{input}, $offset ) ) {
523 118         8160 tracer->debug("decoded frame at $offset, length $len\n");
524 118         8222 $offset += $len;
525             }
526 116 50       24271 substr( $self->{input}, 0, $offset ) = '' if $offset;
527             }
528              
529             =head3 ping
530              
531             Send ping frame to server (to keep connection alive)
532              
533             $client->ping
534              
535             or
536              
537             $client->ping($payload);
538              
539             Payload can be arbitrary binary string and must contain 8 octets. If payload argument
540             is omitted client will send random data.
541              
542             =cut
543              
544             sub ping {
545 1     1 1 8 shift->{con}->send_ping(@_);
546             }
547              
548             1;