File Coverage

blib/lib/Hijk.pm
Criterion Covered Total %
statement 111 186 59.6
branch 63 146 43.1
condition 37 101 36.6
subroutine 11 13 84.6
pod 0 1 0.0
total 222 447 49.6


line stmt bran cond sub pod time code
1             package Hijk;
2 18     18   550976 use strict;
  18         75  
  18         476  
3 18     18   96 use warnings;
  18         33  
  18         492  
4 18     18   872170 use Time::HiRes;
  18         27869  
  18         80  
5 18     18   16600 use POSIX qw(:errno_h);
  18         132405  
  18         133  
6 18     18   48372 use Socket qw(PF_INET SOCK_STREAM pack_sockaddr_in inet_ntoa $CRLF SOL_SOCKET SO_ERROR);
  18         69696  
  18         5557  
7 18     18   196 use Fcntl qw(F_GETFL F_SETFL O_NONBLOCK);
  18         30  
  18         6071  
8              
9             our $VERSION = "0.26";
10              
11             sub Hijk::Error::CONNECT_TIMEOUT () { 1 << 0 } # 1
12             sub Hijk::Error::READ_TIMEOUT () { 1 << 1 } # 2
13             sub Hijk::Error::TIMEOUT () { Hijk::Error::READ_TIMEOUT | Hijk::Error::CONNECT_TIMEOUT } # 3
14             sub Hijk::Error::CANNOT_RESOLVE () { 1 << 2 } # 4
15             sub Hijk::Error::REQUEST_SELECT_ERROR () { 1 << 3 } # 8
16             sub Hijk::Error::REQUEST_WRITE_ERROR () { 1 << 4 } # 16
17             sub Hijk::Error::REQUEST_ERROR () { Hijk::Error::REQUEST_SELECT_ERROR | Hijk::Error::REQUEST_WRITE_ERROR } # 24
18             sub Hijk::Error::RESPONSE_READ_ERROR () { 1 << 5 } # 32
19             sub Hijk::Error::RESPONSE_BAD_READ_VALUE () { 1 << 6 } # 64
20             sub Hijk::Error::RESPONSE_ERROR () { Hijk::Error::RESPONSE_READ_ERROR | Hijk::Error::RESPONSE_BAD_READ_VALUE } # 96
21              
22             sub _read_http_message {
23 12     12   17184 my ($fd, $read_length, $read_timeout, $parse_chunked, $head_as_array, $method) = @_;
24 12 100 66     72 $read_timeout = undef if defined($read_timeout) && $read_timeout <= 0;
25              
26 12         25 my ($body,$buf,$decapitated,$nbytes,$proto);
27 12         21 my $status_code = 0;
28 12 100       43 my $header = $head_as_array ? [] : {};
29 12         25 my $no_content_len = 0;
30 12         24 my $head = "";
31 18     18   97 my $method_has_no_content = do { no warnings qw(uninitialized); $method eq "HEAD" };
  18         33  
  18         41363  
  12         19  
  12         36  
32 12         22 my $close_connection;
33 12         54 vec(my $rin = '', $fd, 1) = 1;
34 12   66     24 do {
      33        
      66        
35 26 50 33     70 return ($close_connection,undef,0,undef,undef, Hijk::Error::READ_TIMEOUT)
      33        
36             if ((_select($rin, undef, undef, $read_timeout) != 1) || (defined($read_timeout) && $read_timeout <= 0));
37              
38 26         330 my $nbytes = POSIX::read($fd, $buf, $read_length);
39 26 100 66     285 return ($close_connection, $proto, $status_code, $header, $body)
      66        
      66        
40             if $no_content_len && $decapitated && (!defined($nbytes) || $nbytes == 0);
41 24 50       65 if (!defined($nbytes)) {
42 0 0 0     0 next if ($! == EWOULDBLOCK || $! == EAGAIN || $! == EINTR);
      0        
43             return (
44 0 0       0 $close_connection, undef, 0, undef, undef,
45             Hijk::Error::RESPONSE_READ_ERROR,
46             "Failed to read http " . ($decapitated ? "body": "head") . " from socket",
47             $!+0,
48             "$!",
49             );
50             }
51              
52 24 100       69 if ($nbytes == 0) {
53             return (
54 5 50       52 $close_connection, undef, 0, undef, undef,
55             Hijk::Error::RESPONSE_BAD_READ_VALUE,
56             "Wasn't expecting a 0 byte response for http " . ($decapitated ? "body": "head" ) . ". This shouldn't happen",
57             );
58             }
59              
60 19 100       73 if ($decapitated) {
61 9         151 $body .= $buf;
62 9 50       52 if (!$no_content_len) {
63 0         0 $read_length -= $nbytes;
64             }
65             }
66             else {
67 10         142 $head .= $buf;
68 10         188 my $neck_pos = index($head, "${CRLF}${CRLF}");
69 10 100       80 if ($neck_pos > 0) {
70 7         18 $decapitated = 1;
71 7         35 $body = substr($head, $neck_pos+4);
72 7         88 $head = substr($head, 0, $neck_pos);
73 7         19 $proto = substr($head, 0, 8);
74 7         22 $status_code = substr($head, 9, 3);
75 7 50       39 $method_has_no_content = 1 if $status_code == 204; # 204 NO CONTENT, see http://tools.ietf.org/html/rfc2616#page-60
76 7         27 substr($head, 0, index($head, $CRLF) + 2, ""); # 2 = length($CRLF)
77              
78 7         130 my ($doing_chunked, $content_length, $trailer_mode, $trailer_value_is_true);
79 7         231 for (split /${CRLF}/o, $head) {
80 45         210 my ($key, $value) = split /: /, $_, 2;
81              
82             # Figure this out now so we don't need to scan the
83             # list later under $head_as_array, and just for
84             # simplicity and to avoid duplicating code later
85             # when !$head_as_array.
86 45 100 66     365 if ($key eq 'Transfer-Encoding' and $value eq 'chunked') {
    100 66        
    100 66        
    100          
87 3         7 $doing_chunked = 1;
88             } elsif ($key eq 'Content-Length') {
89 2         4 $content_length = $value;
90             } elsif ($key eq 'Connection' and $value eq 'close') {
91 2         8 $close_connection = 1;
92             } elsif ($key eq 'Trailer' and $value) {
93 2         4 $trailer_value_is_true = 1;
94             }
95              
96 45 100       84 if ($head_as_array) {
97 13         40 push @$header => $key, $value;
98             } else {
99 32         95 $header->{$key} = $value;
100             }
101             }
102              
103             # We're processing the headers as a stream, and we
104             # only want to turn on $trailer_mode if
105             # Transfer-Encoding=chunked && Trailer=TRUE. However I
106             # don't think there's any guarantee that
107             # Transfer-Encoding comes before Trailer, so we're
108             # effectively doing a second-pass here.
109 7 100 100     55 if ($doing_chunked and $trailer_value_is_true) {
110 2         2 $trailer_mode = 1;
111             }
112              
113 7 100       25 if ($doing_chunked) {
114 3 50       8 die "PANIC: The experimental Hijk support for chunked transfer encoding needs to be explicitly enabled with parse_chunked => 1"
115             unless $parse_chunked;
116              
117             # if there is chunked encoding we have to ignore content length even if we have it
118             return (
119             $close_connection, $proto, $status_code, $header,
120             _read_chunked_body(
121             $body, $fd, $read_length, $read_timeout,
122             $head_as_array
123             ? $trailer_mode
124 3 100       28 : ($header->{Trailer} ? 1 : 0),
    100          
125             ),
126             );
127             }
128              
129 4 100       22 if (defined $content_length) {
130 2 50       7 if ($content_length == 0) {
131 0         0 $read_length = 0;
132             } else {
133 2         44 $read_length = $content_length - length($body);
134             }
135             } else {
136 2         3 $read_length = 10204;
137 2         31 $no_content_len = 1;
138             }
139             }
140             }
141             } while( !$decapitated || (!$method_has_no_content && ($read_length > 0 || $no_content_len)) );
142 2         18 return ($close_connection, $proto, $status_code, $header, $body);
143             }
144              
145             sub _read_chunked_body {
146 3     3   7 my ($buf,$fd,$read_length,$read_timeout,$true_trailer_header) = @_;
147 3         6 my $chunk_size = 0;
148 3         6 my $body = "";
149 3         5 my $trailer_mode = 0;
150              
151 3         11 vec(my $rin = '', $fd, 1) = 1;
152 3         9 while(1) {
153             # just read a 10k block and process it until it is consumed
154 23 50 33     105 if (length($buf) == 0 || length($buf) < $chunk_size) {
155 0 0 0     0 return (undef, Hijk::Error::READ_TIMEOUT)
      0        
156             if ((_select($rin, undef, undef, $read_timeout) != 1) || (defined($read_timeout) && $read_timeout <= 0));
157 0         0 my $current_buf = "";
158 0         0 my $nbytes = POSIX::read($fd, $current_buf, $read_length);
159 0 0       0 if (!defined($nbytes)) {
160 0 0 0     0 next if ($! == EWOULDBLOCK || $! == EAGAIN || $! == EINTR);
      0        
161             return (
162             undef,
163 0         0 Hijk::Error::RESPONSE_READ_ERROR,
164             "Failed to chunked http body from socket",
165             $!+0,
166             "$!",
167             );
168             }
169              
170 0 0       0 if ($nbytes == 0) {
171             return (
172             undef,
173 0         0 Hijk::Error::RESPONSE_BAD_READ_VALUE,
174             "Wasn't expecting a 0 byte response for chunked http body. This shouldn't happen, buf:<$buf>, current_buf:<$current_buf>",
175             );
176             }
177              
178 0         0 $buf .= $current_buf;
179             }
180 23 100       42 if ($trailer_mode) {
181             # http://tools.ietf.org/html/rfc2616#section-14.40
182             # http://tools.ietf.org/html/rfc2616#section-3.6.1
183             # A server using chunked transfer-coding in a response MUST NOT use the
184             # trailer for any header fields unless at least one of the following is
185             # true:
186              
187             # a)the request included a TE header field that indicates "trailers" is
188             # acceptable in the transfer-coding of the response, as described in
189             # section 14.39; or,
190              
191             # b)the server is the origin server for the response, the trailer
192             # fields consist entirely of optional metadata, and the recipient
193             # could use the message (in a manner acceptable to the origin server)
194             # without receiving this metadata. In other words, the origin server
195             # is willing to accept the possibility that the trailer fields might
196             # be silently discarded along the path to the client.
197              
198             # in case of trailer mode, we just read everything until the next CRLFCRLF
199 2         5 my $neck_pos = index($buf, "${CRLF}${CRLF}");
200 2 50       6 if ($neck_pos > 0) {
201 2         25 return $body;
202             }
203             } else {
204 21 100 66     70 if ($chunk_size > 0 && length($buf) >= $chunk_size) {
205 9         20 $body .= substr($buf, 0, $chunk_size - 2); # our chunk size includes the CRLF
206 9         12 $buf = substr($buf, $chunk_size);
207 9         12 $chunk_size = 0;
208             } else {
209 12         19 my $neck_pos = index($buf, ${CRLF});
210 12 50       27 if ($neck_pos > 0) {
211 12         24 $chunk_size = hex(substr($buf, 0, $neck_pos));
212 12 100       33 if ($chunk_size == 0) {
213 3 100       10 if ($true_trailer_header) {
214 2         4 $trailer_mode = 1;
215             } else {
216 1         12 return $body;
217             }
218             } else {
219 9         12 $chunk_size += 2; # include the final CRLF
220 9         21 $buf = substr($buf, $neck_pos + 2);
221             }
222             }
223             }
224             }
225             }
226             }
227              
228             sub _construct_socket {
229 0     0   0 my ($host, $port, $connect_timeout) = @_;
230              
231             # If we can't find the IP address there'll be no point in even
232             # setting up a socket.
233 0         0 my $addr;
234             {
235 0         0 my $inet_aton = gethostbyname($host);
  0         0  
236 0 0       0 return (undef, {error => Hijk::Error::CANNOT_RESOLVE}) unless defined $inet_aton;
237 0         0 $addr = pack_sockaddr_in($port, $inet_aton);
238             }
239              
240 0         0 my $tcp_proto = getprotobyname("tcp");
241 0         0 my $soc;
242 0 0       0 socket($soc, PF_INET, SOCK_STREAM, $tcp_proto) || die "Failed to construct TCP socket: $!";
243 0 0       0 my $flags = fcntl($soc, F_GETFL, 0) or die "Failed to set fcntl F_GETFL flag: $!";
244 0 0       0 fcntl($soc, F_SETFL, $flags | O_NONBLOCK) or die "Failed to set fcntl O_NONBLOCK flag: $!";
245              
246 0 0 0     0 if (!connect($soc, $addr) && $! != EINPROGRESS) {
247 0         0 die "Failed to connect $!";
248             }
249              
250 0 0 0     0 $connect_timeout = undef if defined($connect_timeout) && $connect_timeout <= 0;
251 0         0 vec(my $rout = '', fileno($soc), 1) = 1;
252 0 0       0 if (_select(undef, $rout, undef, $connect_timeout) != 1) {
253 0 0       0 if (defined($connect_timeout)) {
254 0         0 return (undef, {error => Hijk::Error::CONNECT_TIMEOUT});
255             } else {
256             return (
257             undef,
258             {
259 0         0 error => Hijk::Error::REQUEST_SELECT_ERROR,
260             error_message => "select() error on constructing the socket",
261             errno_number => $!+0,
262             errno_string => "$!",
263             },
264             );
265             }
266             }
267              
268 0 0       0 if ($! = unpack("L", getsockopt($soc, SOL_SOCKET, SO_ERROR))) {
269 0         0 die $!;
270             }
271              
272 0         0 return $soc;
273             }
274              
275             sub _build_http_message {
276 36     36   486 my $args = $_[0];
277 36 100 100     188 my $path_and_qs = ($args->{path} || "/") . ( defined($args->{query_string}) ? ("?".$args->{query_string}) : "" );
278             return join(
279             $CRLF,
280             ($args->{method} || "GET")." $path_and_qs " . ($args->{protocol} || "HTTP/1.1"),
281             ($args->{no_default_host_header}
282             ? ()
283             : ("Host: $args->{host}")),
284             defined($args->{body}) ? ("Content-Length: " . length($args->{body})) : (),
285             ($args->{head} and @{$args->{head}}) ? (
286             map {
287 14         116 $args->{head}[2*$_] . ": " . $args->{head}[2*$_+1]
288 10         32 } 0..$#{$args->{head}}/2
289             ) : (),
290             ""
291 36 100 100     448 ) . $CRLF . (defined($args->{body}) ? $args->{body} : "");
    100 50        
    100 100        
    100          
292             }
293              
294             our $SOCKET_CACHE = {};
295              
296             sub request {
297 0     0 0 0 my $args = $_[0];
298              
299             # Backwards compatibility for code that provided the old timeout
300             # argument.
301 0 0       0 $args->{connect_timeout} = $args->{read_timeout} = $args->{timeout} if exists $args->{timeout};
302              
303             # Ditto for providing a default socket cache, allow for setting it
304             # to "socket_cache => undef" to disable the cache.
305 0 0       0 $args->{socket_cache} = $SOCKET_CACHE unless exists $args->{socket_cache};
306              
307             # Provide a default for the read_length option
308 0 0       0 $args->{read_length} = 10 * 2 ** 10 unless exists $args->{read_length};
309              
310             # Use $; so we can use the $socket_cache->{$$, $host, $port}
311             # idiom to access the cache.
312 0 0       0 my $cache_key; $cache_key = join($;, $$, @$args{qw(host port)}) if defined $args->{socket_cache};
  0         0  
313              
314 0         0 my $soc;
315 0 0 0     0 if (defined $cache_key and exists $args->{socket_cache}->{$cache_key}) {
316 0         0 $soc = $args->{socket_cache}->{$cache_key};
317             } else {
318 0         0 ($soc, my $error) = _construct_socket(@$args{qw(host port connect_timeout)});
319 0 0       0 return $error if $error;
320 0 0       0 $args->{socket_cache}->{$cache_key} = $soc if defined $cache_key;
321 0 0       0 $args->{on_connect}->() if exists $args->{on_connect};
322             }
323              
324 0         0 my $r = _build_http_message($args);
325 0         0 my $total = length($r);
326 0         0 my $left = $total;
327              
328 0         0 vec(my $rout = '', fileno($soc), 1) = 1;
329 0         0 while ($left > 0) {
330 0 0       0 if (_select(undef, $rout, undef, undef) != 1) {
331 0 0       0 delete $args->{socket_cache}->{$cache_key} if defined $cache_key;
332             return {
333 0         0 error => Hijk::Error::REQUEST_SELECT_ERROR,
334             error_message => "Got error on select() before the write() when while writing the HTTP request the socket",
335             errno_number => $!+0,
336             errno_string => "$!",
337             };
338             }
339              
340 0         0 my $rc = syswrite($soc,$r,$left, $total - $left);
341 0 0       0 if (!defined($rc)) {
342 0 0 0     0 next if ($! == EWOULDBLOCK || $! == EAGAIN || $! == EINTR);
      0        
343 0 0       0 delete $args->{socket_cache}->{$cache_key} if defined $cache_key;
344 0         0 shutdown($soc, 2);
345             return {
346 0         0 error => Hijk::Error::REQUEST_WRITE_ERROR,
347             error_message => "Got error trying to write the HTTP request with write() to the socket",
348             errno_number => $!+0,
349             errno_string => "$!",
350             };
351             }
352 0         0 $left -= $rc;
353             }
354              
355 0         0 my ($close_connection,$proto,$status,$head,$body,$error,$error_message,$errno_number,$errno_string);
356             eval {
357             ($close_connection,$proto,$status,$head,$body,$error,$error_message,$errno_number,$errno_string) =
358 0         0 _read_http_message(fileno($soc), @$args{qw(read_length read_timeout parse_chunked head_as_array method)});
359 0         0 1;
360 0 0       0 } or do {
361 0   0     0 my $err = $@ || "zombie error";
362 0 0       0 delete $args->{socket_cache}->{$cache_key} if defined $cache_key;
363 0         0 shutdown($soc, 2);
364 0         0 die $err;
365             };
366              
367 0 0 0     0 if ($status == 0
      0        
      0        
368             # We always close connections for 1.0 because some servers LIE
369             # and say that they're 1.0 but don't close the connection on
370             # us! An example of this. Test::HTTP::Server (used by the
371             # ShardedKV::Storage::Rest tests) is an example of such a
372             # server. In either case we can't cache a connection for a 1.0
373             # server anyway, so BEGONE!
374             or $close_connection
375             or (defined $proto and $proto eq 'HTTP/1.0')) {
376 0 0       0 delete $args->{socket_cache}->{$cache_key} if defined $cache_key;
377 0         0 shutdown($soc, 2);
378             }
379             return {
380 0 0       0 proto => $proto,
    0          
    0          
    0          
381             status => $status,
382             head => $head,
383             body => $body,
384             defined($error) ? ( error => $error ) : (),
385             defined($error_message) ? ( error_message => $error_message ) : (),
386             defined($errno_number) ? ( errno_number => $errno_number ) : (),
387             defined($errno_string) ? ( errno_string => $errno_string ) : (),
388             };
389             }
390              
391             sub _select {
392 27     27   1590 my ($rbits, $wbits, $ebits, $timeout) = @_;
393 27         57 while (1) {
394 37         151 my $start = Time::HiRes::time();
395 37         1999172 my $nfound = select($rbits, $wbits, $ebits, $timeout);
396 37 100 66     500 if ($nfound == -1 && $! == EINTR) {
397 10 50       1329 $timeout -= Time::HiRes::time() - $start if $timeout;
398 10         43 next;
399             }
400 27         176 return $nfound;
401             }
402             }
403              
404             1;
405              
406             __END__