File Coverage

blib/lib/HTTP/Tiny/SPDY.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package HTTP::Tiny::SPDY;
2              
3 28     28   1413405 use strict;
  28         71  
  28         1140  
4 28     28   143 use warnings;
  28         54  
  28         1362  
5              
6             # ABSTRACT: A subclass of HTTP::Tiny with SPDY support
7              
8             our $VERSION = '0.020'; # VERSION
9              
10 28     28   37436 use HTTP::Tiny;
  28         1510287  
  28         1584  
11 28     28   57739 use Net::SPDY::Session;
  0            
  0            
12              
13             use parent 'HTTP::Tiny';
14              
15             my @attributes;
16             BEGIN {
17             @attributes = qw(enable_SPDY);
18             ## no critic (NoStrict)
19             no strict 'refs';
20             for my $accessor (@attributes) {
21             *{$accessor} = sub {
22             @_ > 1 ? $_[0]->{$accessor} = $_[1] : $_[0]->{$accessor};
23             };
24             }
25             ## use critic
26             }
27              
28              
29             sub new {
30             my ($class, %args) = @_;
31              
32             my $self = $class->SUPER::new(%args);
33              
34             $self->{enable_SPDY} = exists $args{enable_SPDY} ? $args{enable_SPDY} : 1;
35              
36             return $self;
37             }
38              
39             my %DefaultPort = (
40             http => 80,
41             https => 443,
42             );
43            
44             sub _request {
45             my ($self, $method, $url, $args) = @_;
46            
47             my ($scheme, $host, $port, $path_query, $auth) = $self->_split_url($url);
48            
49             my $request = {
50             method => $method,
51             scheme => $scheme,
52             host => $host,
53             host_port => ($port == $DefaultPort{$scheme} ? $host : "$host:$port"),
54             uri => $path_query,
55             headers => {},
56             };
57            
58             # We remove the cached handle so it is not reused in the case of redirect.
59             # If all is well, it will be recached at the end of _request. We only
60             # reuse for the same scheme, host and port
61             my $handle = delete $self->{handle};
62             if ( $handle ) {
63             unless ( $handle->can_reuse( $scheme, $host, $port ) ) {
64             $handle->close;
65             undef $handle;
66             }
67             }
68             $handle ||= $self->_open_handle( $request, $scheme, $host, $port );
69              
70             $self->_prepare_headers_and_cb($request, $args, $url, $auth);
71              
72             $handle->write_request($request);
73              
74             my $response;
75              
76             if (defined $handle->{spdy}) {
77             # SPDY connection
78             my $framer = $handle->{spdy}->{session}->{framer};
79              
80             while (my %frame = $framer->read_frame) {
81             if (exists $frame{type} &&
82             $frame{type} == Net::SPDY::Framer::SYN_REPLY)
83             {
84             my %frame_headers = @{$frame{headers}};
85             my @http_headers = @{$frame{headers}};
86              
87             ($response->{status}, $response->{reason}) =
88             split /[\x09\x20]+/, delete($frame_headers{':status'}), 2;
89              
90             $response->{headers} = {};
91              
92             for (my $i = 0; $i < $#http_headers; $i += 2) {
93             if ($http_headers[$i] !~ /^:/) {
94             my $field_name = lc $http_headers[$i];
95              
96             if (exists $response->{headers}->{$field_name}) {
97             if (ref $response->{headers}->{$field_name} ne 'ARRAY') {
98             $response->{headers}->{$field_name} = [
99             $response->{headers}->{$field_name}
100             ];
101              
102             push @{$response->{headers}->{$field_name}}, $http_headers[$i+1];
103             }
104             }
105             else {
106             $response->{headers}->{$field_name} = $http_headers[$i+1];
107             }
108             }
109             }
110             }
111              
112             if (!$frame{control}) {
113             # TODO: Add support for max_size
114             $response->{content} .= $frame{data};
115             }
116              
117             last if ($frame{flags} & Net::SPDY::Framer::FLAG_FIN);
118              
119             # FIXME: Probably need to do better than just saying "throw another
120             # 64K on us" after each and every frame
121             $framer->write_frame(
122             control => 1,
123             type => Net::SPDY::Framer::WINDOW_UPDATE,
124             stream_id => $frame{stream_id},
125             delta_window_size => 0x00010000,
126             );
127             }
128              
129             $handle->close;
130             }
131             else {
132             # Traditional HTTP(S) connection
133             do { $response = $handle->read_response_header }
134             until (substr($response->{status},0,1) ne '1');
135            
136             $self->_update_cookie_jar( $url, $response ) if $self->{cookie_jar};
137            
138             if ( my @redir_args = $self->_maybe_redirect($request, $response, $args) ) {
139             $handle->close;
140             return $self->_request(@redir_args, $args);
141             }
142            
143             my $known_message_length;
144             if ($method eq 'HEAD' || $response->{status} =~ /^[23]04/) {
145             # response has no message body
146             $known_message_length = 1;
147             }
148             else {
149             my $data_cb = $self->_prepare_data_cb($response, $args);
150             $known_message_length = $handle->read_body($data_cb, $response);
151             }
152              
153             if ( $self->{keep_alive}
154             && $known_message_length
155             && $response->{protocol} eq 'HTTP/1.1'
156             && ($response->{headers}{connection} || '') ne 'close'
157             ) {
158             $self->{handle} = $handle;
159             }
160             else {
161             $handle->close;
162             }
163             }
164            
165             $response->{success} = substr($response->{status},0,1) eq '2';
166             $response->{url} = $url;
167             return $response;
168             }
169              
170             sub _open_handle {
171             my ($self, $request, $scheme, $host, $port) = @_;
172              
173             if ($self->{enable_SPDY}) {
174             my $handle = HTTP::Tiny::Handle::SPDY->new(
175             timeout => $self->{timeout},
176             SSL_options => $self->{SSL_options},
177             verify_SSL => $self->{verify_SSL},
178             local_address => $self->{local_address},
179             keep_alive => $self->{keep_alive},
180             );
181              
182             if ($self->{_has_proxy}{$scheme} && ! grep { $host =~ /\Q$_\E$/ } @{$self->{no_proxy}}) {
183             return $self->_proxy_connect( $request, $handle );
184             }
185             else {
186             return $handle->connect($scheme, $host, $port);
187             }
188             }
189             else {
190             return $self->SUPER::_open_handle($request, $scheme, $host, $port);
191             }
192             }
193              
194             package
195             HTTP::Tiny::Handle::SPDY;
196              
197             use strict;
198             use warnings;
199              
200             use IO::Socket qw(SOCK_STREAM);
201              
202             use parent -norequire, 'HTTP::Tiny::Handle';
203              
204             sub connect {
205             @_ == 4 || die(q/Usage: $handle->connect(scheme, host, port)/ . "\n");
206             my ($self, $scheme, $host, $port) = @_;
207            
208             if ( $scheme eq 'https' ) {
209             # Need IO::Socket::SSL 1.42 for SSL_create_ctx_callback
210             die(qq/IO::Socket::SSL 1.42 must be installed for https support\n/)
211             unless eval {require IO::Socket::SSL; IO::Socket::SSL->VERSION(1.42)};
212             # Need Net::SSLeay 1.49 for MODE_AUTO_RETRY
213             die(qq/Net::SSLeay 1.49 must be installed for https support\n/)
214             unless eval {require Net::SSLeay; Net::SSLeay->VERSION(1.49)};
215             }
216             elsif ( $scheme ne 'http' ) {
217             die(qq/Unsupported URL scheme '$scheme'\n/);
218             }
219             $self->{fh} = 'IO::Socket::INET'->new(
220             PeerHost => $host,
221             PeerPort => $port,
222             $self->{local_address} ?
223             ( LocalAddr => $self->{local_address} ) : (),
224             Proto => 'tcp',
225             Type => SOCK_STREAM,
226             Timeout => $self->{timeout}
227             ) or die(qq/Could not connect to '$host:$port': $@\n/);
228            
229             binmode($self->{fh})
230             or die(qq/Could not binmode() socket: '$!'\n/);
231              
232             if ($scheme eq 'https') {
233             $self->start_ssl($host);
234              
235             if ($self->{fh}->next_proto_negotiated &&
236             $self->{fh}->next_proto_negotiated eq 'spdy/3')
237             {
238             # SPDY negotiation succeeded
239             $self->{spdy} = {
240             session => Net::SPDY::Session->new($self->{fh}),
241             stream_id => 1,
242             };
243             }
244             }
245              
246             $self->{scheme} = $scheme;
247             $self->{host} = $host;
248             $self->{port} = $port;
249            
250             return $self;
251             }
252              
253             my $Printable = sub {
254             local $_ = shift;
255             s/\r/\\r/g;
256             s/\n/\\n/g;
257             s/\t/\\t/g;
258             s/([^\x20-\x7E])/sprintf('\\x%.2X', ord($1))/ge;
259             $_;
260             };
261              
262             # HTTP headers which must not be present in a SPDY request
263             my %invalid_headers;
264             undef @invalid_headers{qw( connection host )};
265            
266             sub write_request {
267             @_ == 2 || die(q/Usage: $handle->write_request(request)/ . "\n");
268             my ($self, $request) = @_;
269              
270             if (defined $self->{spdy}) {
271             my $framer = $self->{spdy}->{session}->{framer};
272              
273             my %frame = (
274             type => Net::SPDY::Framer::SYN_STREAM,
275             stream_id => $self->{spdy}->{stream_id},
276             associated_stream_id => 0,
277             priority => 2,
278             flags => $request->{cb} ? 0 : Net::SPDY::Framer::FLAG_FIN,
279             slot => 0,
280             headers => [
281             ':method' => $request->{method},
282             ':scheme' => $request->{scheme},
283             ':path' => $request->{uri},
284             ':version' => 'HTTP/1.1',
285             ':host' => $request->{host_port},
286             ]
287             );
288              
289             while (my ($k, $v) = each %{$request->{headers}}) {
290             my $field_name = lc $k;
291              
292             # Omit invalid headers
293             next if exists $invalid_headers{$field_name};
294              
295             for (ref $v eq 'ARRAY' ? @$v : $v) {
296             /[^\x0D\x0A]/
297             or die(qq/Invalid HTTP header field value ($field_name): / . $Printable->($_). "\n");
298             push @{$frame{headers}}, $field_name, $_;
299             }
300             }
301              
302             $framer->write_frame(%frame);
303              
304             if ($request->{cb}) {
305             if ($request->{headers}{'content-length'}) {
306             # write_content_body
307             my ($len, $content_length) = (0, $request->{headers}{'content-length'});
308              
309             my $data = $request->{cb}->();
310             my $last_frame = 0;
311              
312             do {
313             my %frame = (
314             control => 0,
315             stream_id => $self->{spdy}->{stream_id},
316             data => $data || '',
317             flags => 0,
318             );
319              
320             $last_frame = !defined $data || !length $data;
321            
322             if (!$last_frame) {
323             $data = $request->{cb}->();
324             $last_frame = !defined $data || !length $data;
325             }
326              
327             if ($last_frame) {
328             $frame{flags} |= Net::SPDY::Framer::FLAG_FIN;
329             }
330            
331             %frame = $framer->write_frame(%frame);
332            
333             $len += $frame{length};
334             }
335             while (!$last_frame);
336              
337             $len == $content_length
338             or die(qq/Content-Length mismatch (got: $len, expected: $content_length)\n/);
339             }
340             else {
341             # write_chunked_body
342             }
343             }
344              
345             $self->{spdy}->{stream_id} += 2;
346              
347             return;
348             }
349             else {
350             return $self->SUPER::write_request($request);
351             }
352             }
353              
354             sub _ssl_args {
355             my ($self, $host) = @_;
356              
357             my %ssl_args = %{$self->SUPER::_ssl_args($host)};
358              
359             $ssl_args{SSL_npn_protocols} = ['spdy/3'];
360              
361             return \%ssl_args;
362             }
363              
364             1;
365              
366             __END__