File Coverage

blib/lib/Protocol/HTTP2/Client.pm
Criterion Covered Total %
statement 80 99 80.8
branch 27 38 71.0
condition 3 8 37.5
subroutine 16 17 94.1
pod 5 6 83.3
total 131 168 77.9


line stmt bran cond sub pod time code
1             package Protocol::HTTP2::Client;
2 5     5   34038 use strict;
  5         11  
  5         164  
3 5     5   18 use warnings;
  5         7  
  5         112  
4 5     5   1547 use Protocol::HTTP2::Connection;
  5         11  
  5         167  
5 5         1231 use Protocol::HTTP2::Constants qw(:frame_types :flags :states :endpoints
6 5     5   25 :errors);
  5         7  
7 5     5   26 use Protocol::HTTP2::Trace qw(tracer);
  5         7  
  5         197  
8 5     5   31 use Carp;
  5         8  
  5         261  
9 5     5   24 use Scalar::Util ();
  5         6  
  5         4432  
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.
140              
141             See
142             L
143              
144             =item on_error => sub {...}
145              
146             Callback invoked on protocol errors
147              
148             on_error => sub {
149             my $error = shift;
150             ...
151             },
152              
153             =item on_change_state => sub {...}
154              
155             Callback invoked every time when http/2 streams change their state.
156             See
157             L
158              
159             on_change_state => sub {
160             my ( $stream_id, $previous_state, $current_state ) = @_;
161             ...
162             },
163              
164             =back
165              
166             =cut
167              
168             sub new {
169 15     15 1 28914 my ( $class, %opts ) = @_;
170 15 100       2134 my $self = {
171             con => undef,
172             input => '',
173             active_streams => 0,
174             settings => exists $opts{settings} ? $opts{settings} : {},
175             };
176              
177 15 50       2002 if ( exists $opts{on_push} ) {
178 0         0 Scalar::Util::weaken( my $self = $self );
179              
180 0         0 my $cb = delete $opts{on_push};
181             $opts{on_new_peer_stream} = sub {
182 0     0   0 my $stream_id = shift;
183 0         0 my $pp_headers;
184 0         0 $self->active_streams(+1);
185              
186             $self->{con}->stream_cb(
187             $stream_id,
188             RESERVED,
189             sub {
190 0         0 my $res =
191             $cb->( $self->{con}->stream_pp_headers($stream_id) );
192 0 0 0     0 if ( $res && ref $cb eq 'CODE' ) {
193             $self->{con}->stream_cb(
194             $stream_id,
195             CLOSED,
196             sub {
197 0         0 $res->(
198             $self->{con}->stream_headers($stream_id),
199             $self->{con}->stream_data($stream_id),
200             );
201 0         0 $self->active_streams(-1);
202             }
203 0         0 );
204             }
205             else {
206 0         0 $self->{con}
207             ->stream_error( $stream_id, REFUSED_STREAM );
208 0         0 $self->active_streams(-1);
209             }
210             }
211 0         0 );
212 0         0 };
213             }
214              
215 15         2135 $self->{con} = Protocol::HTTP2::Connection->new( CLIENT, %opts );
216 15         4018 bless $self, $class;
217             }
218              
219             sub active_streams {
220 22     22 0 2940 my $self = shift;
221 22   50     3154 my $add = shift || 0;
222 22         3158 $self->{active_streams} += $add;
223 22 100       5117 $self->{con}->finish unless $self->{active_streams} > 0;
224             }
225              
226             =head3 request
227              
228             Prepare HTTP/2 request.
229              
230             $client->request(
231              
232             # HTTP/2 headers
233             ':scheme' => 'http',
234             ':authority' => 'localhost:8000',
235             ':path' => '/',
236             ':method' => 'GET',
237              
238             # HTTP/1.1 headers
239             headers => [
240             'accept' => '*/*',
241             'user-agent' => 'perl-Protocol-HTTP2/0.06',
242             ],
243              
244             # Callback when receive server's response
245             on_done => sub {
246             my ( $headers, $data ) = @_;
247             ...
248             },
249             );
250              
251             You can chaining request one by one:
252              
253             $client->request( 1-st request )->request( 2-nd request );
254              
255             Available callbacks:
256              
257             =over
258              
259             =item on_done => sub {...}
260              
261             Invoked when full servers response is available
262              
263             on_done => sub {
264             my ( $headers, $data ) = @_;
265             ...
266             },
267              
268             =item on_headers => sub {...}
269              
270             Invoked as soon as headers have been successfully received from the server
271              
272             on_headers => sub {
273             my $headers = shift;
274             ...
275              
276             # if we want reject any data
277             # return undef
278              
279             # continue
280             return 1
281             }
282              
283             =item on_data => sub {...}
284              
285             If specified all data will be passed to this callback instead if on_done.
286             on_done will receive empty string.
287              
288             on_data => sub {
289             my ( $partial_data, $headers ) = @_;
290             ...
291              
292             # if we want cancel download
293             # return undef
294              
295             # continue downloading
296             return 1
297             }
298              
299             =back
300              
301             =cut
302              
303             my @must = (qw(:authority :method :path :scheme));
304              
305             sub request {
306 15     15 1 4239 my ( $self, %h ) = @_;
307 15         1985 my @miss = grep { !exists $h{$_} } @must;
  60         15730  
308 15 50       2107 croak "Missing fields in request: @miss" if @miss;
309              
310 15         2018 $self->active_streams(+1);
311              
312 15         2038 my $con = $self->{con};
313              
314 15         2282 my $stream_id = $con->new_stream;
315              
316 15 100 66     2053 if ( $con->upgrade && !exists $self->{sent_upgrade} ) {
317 4         65 $con->enqueue_raw(
318             $con->upgrade_request(
319 1 50       12 ( map { $_ => $h{$_} } @must ),
320             headers => exists $h{headers} ? $h{headers} : []
321             )
322             );
323 1         6 $self->{sent_upgrade} = 1;
324 1         15 $con->stream_state( $stream_id, HALF_CLOSED );
325             }
326             else {
327 14 50       1995 if ( !$con->preface ) {
328 14         1974 $con->enqueue_raw( $con->preface_encode ),
329             $con->enqueue( SETTINGS, 0, 0, $self->{settings} );
330 14         2064 $con->preface(1);
331             }
332              
333             $con->send_headers(
334 56         15851 $stream_id,
335             [
336 12         3920 ( map { $_ => $h{$_} } @must ),
337 14 100       1983 exists $h{headers} ? @{ $h{headers} } : ()
338             ],
339             1
340             );
341             }
342              
343 15         2130 Scalar::Util::weaken $self;
344 15         2196 Scalar::Util::weaken $con;
345              
346             $con->stream_cb(
347             $stream_id,
348             CLOSED,
349             sub {
350 7     7   1005 $h{on_done}->(
351             $con->stream_headers($stream_id),
352             $con->stream_data($stream_id),
353             );
354 7         6089 $self->active_streams(-1);
355             }
356 15 100       2222 ) if exists $h{on_done};
357              
358             $con->stream_frame_cb(
359             $stream_id,
360             HEADERS,
361             sub {
362 5     5   1101 my $res = $h{on_headers}->( $_[0] );
363 5 50       7083 return if $res;
364 0         0 $con->stream_error( $stream_id, REFUSED_STREAM );
365             }
366 15 100       2173 ) if exists $h{on_headers};
367              
368             $con->stream_frame_cb(
369             $stream_id,
370             DATA,
371             sub {
372 5     5   1157 my $res = $h{on_data}->( $_[0], $con->stream_headers($stream_id), );
373 5 50       5879 return if $res;
374 5         1488 $con->stream_error( $stream_id, REFUSED_STREAM );
375             }
376 15 100       2128 ) if exists $h{on_data};
377              
378 15         3939 return $self;
379             }
380              
381             =head3 shutdown
382              
383             Get connection status:
384              
385             =over
386              
387             =item 0 - active
388              
389             =item 1 - closed (you can terminate connection)
390              
391             =back
392              
393             =cut
394              
395             sub shutdown {
396 7     7 1 43 shift->{con}->shutdown;
397             }
398              
399             =head3 next_frame
400              
401             get next frame to send over connection to server.
402             Returns:
403              
404             =over
405              
406             =item undef - on error
407              
408             =item 0 - nothing to send
409              
410             =item binary string - encoded frame
411              
412             =back
413              
414             # Example
415             while ( my $frame = $client->next_frame ) {
416             syswrite $fh, $frame;
417             }
418              
419             =cut
420              
421             sub next_frame {
422 109     109 1 62497 my $self = shift;
423 109         16488 my $frame = $self->{con}->dequeue;
424 109 100       16728 tracer->debug("send one frame to wire\n") if $frame;
425 109         31961 return $frame;
426             }
427              
428             =head3 feed
429              
430             Feed decoder with chunks of server's response
431              
432             sysread $fh, $binary_data, 4096;
433             $client->feed($binary_data);
434              
435             =cut
436              
437             sub feed {
438 60     60 1 65036 my ( $self, $chunk ) = @_;
439 60         10380 $self->{input} .= $chunk;
440 60         10316 my $offset = 0;
441 60         10223 my $len;
442 60         10493 my $con = $self->{con};
443 60         10413 tracer->debug( "got " . length($chunk) . " bytes on a wire\n" );
444 60 100       10497 if ( $con->upgrade ) {
445 1         23 $len = $con->decode_upgrade_response( \$self->{input}, $offset );
446 1 50       9 $con->shutdown(1) unless defined $len;
447 1 50       9 return unless $len;
448 0         0 $offset += $len;
449 0         0 $con->upgrade(0);
450 0         0 $con->enqueue_raw( $con->preface_encode );
451 0         0 $con->preface(1);
452             }
453 59         10896 while ( $len = $con->frame_decode( \$self->{input}, $offset ) ) {
454 61         9897 tracer->debug("decoded frame at $offset, length $len\n");
455 61         10034 $offset += $len;
456             }
457 59 50       30038 substr( $self->{input}, 0, $offset ) = '' if $offset;
458             }
459              
460             1;