File Coverage

blib/lib/HTTP/Daemon.pm
Criterion Covered Total %
statement 61 389 15.6
branch 5 202 2.4
condition 3 55 5.4
subroutine 18 39 46.1
pod 4 4 100.0
total 91 689 13.2


line stmt bran cond sub pod time code
1             package HTTP::Daemon; # git description: v6.15-4-gbab5825
2              
3             # ABSTRACT: A simple http server class
4              
5 1     1   68629 use strict;
  1         12  
  1         28  
6 1     1   6 use warnings;
  1         1  
  1         37  
7              
8             our $VERSION = '6.16';
9              
10 1     1   601 use Socket ();
  1         3779  
  1         37  
11 1     1   536 use IO::Socket::IP;
  1         32373  
  1         6  
12             our @ISA = qw(IO::Socket::IP);
13              
14             our $PROTO = "HTTP/1.1";
15              
16             our $DEBUG;
17              
18             sub new {
19 1     1 1 137 my ($class, %args) = @_;
20 1   50     9 $args{Listen} ||= 5;
21 1   50     6 $args{Proto} ||= 'tcp';
22              
23             # Handle undefined or empty local address the same way as
24             # IO::Socket::INET -- use unspecified address
25 1         3 for my $key (qw(LocalAddr LocalHost)) {
26 2 0 0     7 if (exists $args{$key} && (!defined $args{$key} || $args{$key} eq '')) {
      33        
27 0         0 delete $args{$key};
28             }
29             }
30 1         10 return $class->SUPER::new(%args);
31             }
32              
33             sub accept {
34 0     0 1 0 my $self = shift;
35 0   0     0 my $pkg = shift || "HTTP::Daemon::ClientConn";
36 0         0 my ($sock, $peer) = $self->SUPER::accept($pkg);
37 0 0       0 if ($sock) {
38 0         0 ${*$sock}{'httpd_daemon'} = $self;
  0         0  
39 0 0       0 return wantarray ? ($sock, $peer) : $sock;
40             }
41             else {
42 0         0 return;
43             }
44             }
45              
46             sub url {
47 1     1 1 1951 my $self = shift;
48              
49 1         5 my $host = $self->sockhost;
50 1         7 $host =~ s/%/%25/g;
51 1 50       5 $host = "127.0.0.1" if $host eq "0.0.0.0";
52 1 50       3 $host = "::1" if $host eq "::";
53 1 50       4 $host = "[$host]" if $self->sockdomain == Socket::AF_INET6;
54              
55 1         9 my $url = $self->_default_scheme . "://" . $host;
56 1         9 my $port = $self->sockport;
57 1 50       74 $url .= ":$port" if $port != $self->_default_port;
58 1         2 $url .= "/";
59 1         8 $url;
60             }
61              
62             sub _default_port {
63 1     1   6 80;
64             }
65              
66             sub _default_scheme {
67 1     1   3 "http";
68             }
69              
70             sub product_tokens {
71 0     0 1 0 "libwww-perl-daemon/$HTTP::Daemon::VERSION";
72             }
73              
74             package # hide from PAUSE
75             HTTP::Daemon::ClientConn;
76              
77 1     1   948 use strict;
  1         2  
  1         31  
78 1     1   5 use warnings;
  1         2  
  1         49  
79              
80 1     1   6 use IO::Socket::IP ();
  1         2  
  1         67  
81             our @ISA = qw(IO::Socket::IP);
82             our $DEBUG;
83             *DEBUG = \$HTTP::Daemon::DEBUG;
84              
85 1     1   470 use HTTP::Request ();
  1         23731  
  1         24  
86 1     1   461 use HTTP::Response ();
  1         7328  
  1         26  
87 1     1   7 use HTTP::Status;
  1         8  
  1         260  
88 1     1   448 use HTTP::Date qw(time2str);
  1         3830  
  1         65  
89 1     1   475 use LWP::MediaTypes qw(guess_media_type);
  1         17171  
  1         91  
90 1     1   7 use Carp ();
  1         2  
  1         3914  
91              
92             # "\r\n" is not portable
93             my $CRLF = "\015\012";
94             my $HTTP_1_0 = _http_version("HTTP/1.0");
95             my $HTTP_1_1 = _http_version("HTTP/1.1");
96              
97              
98             sub get_request {
99 0     0   0 my ($self, $only_headers) = @_;
100 0 0       0 if (${*$self}{'httpd_nomore'}) {
  0         0  
101 0         0 $self->reason("No more requests from this connection");
102 0         0 return;
103             }
104              
105 0         0 $self->reason("");
106 0         0 my $buf = ${*$self}{'httpd_rbuf'};
  0         0  
107 0 0       0 $buf = "" unless defined $buf;
108              
109 0         0 my $timeout = ${*$self}{'io_socket_timeout'};
  0         0  
110 0         0 my $fdset = "";
111 0         0 vec($fdset, $self->fileno, 1) = 1;
112 0         0 local ($_);
113              
114             READ_HEADER:
115 0         0 while (1) {
116              
117             # loop until we have the whole header in $buf
118 0         0 $buf =~ s/^(?:\015?\012)+//; # ignore leading blank lines
119 0 0       0 if ($buf =~ /\012/) { # potential, has at least one line
    0          
120 0 0       0 if ($buf =~ /^\w+[^\012]+HTTP\/\d+\.\d+\015?\012/) {
121 0 0       0 if ($buf =~ /\015?\012\015?\012/) {
    0          
122 0         0 last READ_HEADER; # we have it
123             }
124             elsif (length($buf) > 16 * 1024) {
125 0         0 $self->send_error(413); # REQUEST_ENTITY_TOO_LARGE
126 0         0 $self->reason("Very long header");
127 0         0 return;
128             }
129             }
130             else {
131 0         0 last READ_HEADER; # HTTP/0.9 client
132             }
133             }
134             elsif (length($buf) > 16 * 1024) {
135 0         0 $self->send_error(414); # REQUEST_URI_TOO_LARGE
136 0         0 $self->reason("Very long first line");
137 0         0 return;
138             }
139 0 0       0 print STDERR "Need more data for complete header\n" if $DEBUG;
140 0 0       0 return unless $self->_need_more($buf, $timeout, $fdset);
141             }
142 0 0       0 if ($buf !~ s/^(\S+)[ \t]+(\S+)(?:[ \t]+(HTTP\/\d+\.\d+))?[^\012]*\012//) {
143 0         0 ${*$self}{'httpd_client_proto'} = _http_version("HTTP/1.0");
  0         0  
144 0         0 $self->send_error(400); # BAD_REQUEST
145 0         0 $self->reason("Bad request line: $buf");
146 0         0 return;
147             }
148 0         0 my $method = $1;
149 0         0 my $uri = $2;
150 0   0     0 my $proto = $3 || "HTTP/0.9";
151 0 0       0 $uri = "http://$uri" if $method eq "CONNECT";
152 0         0 $uri = $HTTP::URI_CLASS->new($uri, $self->daemon->url);
153 0         0 my $r = HTTP::Request->new($method, $uri);
154 0         0 $r->protocol($proto);
155 0         0 ${*$self}{'httpd_client_proto'} = $proto = _http_version($proto);
  0         0  
156 0         0 ${*$self}{'httpd_head'} = ($method eq "HEAD");
  0         0  
157              
158 0 0       0 if ($proto >= $HTTP_1_0) {
159              
160             # we expect to find some headers
161 0         0 my ($key, $val);
162             HEADER:
163 0         0 while ($buf =~ s/^([^\012]*)\012//) {
164 0         0 $_ = $1;
165 0         0 s/\015$//;
166 0 0       0 if (/^([^:\s]+)\s*:\s*(.*)/) {
    0          
167 0 0       0 $r->push_header($key, $val) if $key;
168 0         0 ($key, $val) = ($1, $2);
169             }
170             elsif (/^\s+(.*)/) {
171 0         0 $val .= " $1";
172             }
173             else {
174 0         0 last HEADER;
175             }
176             }
177 0 0       0 $r->push_header($key, $val) if $key;
178             }
179              
180 0         0 my $conn = $r->header('Connection');
181 0 0       0 if ($proto >= $HTTP_1_1) {
182 0 0 0     0 ${*$self}{'httpd_nomore'}++ if $conn && lc($conn) =~ /\bclose\b/;
  0         0  
183             }
184             else {
185 0 0 0     0 ${*$self}{'httpd_nomore'}++
  0         0  
186             unless $conn && lc($conn) =~ /\bkeep-alive\b/;
187             }
188              
189 0 0       0 if ($only_headers) {
190 0         0 ${*$self}{'httpd_rbuf'} = $buf;
  0         0  
191 0         0 return $r;
192             }
193              
194             # Find out how much content to read
195 0         0 my $tr_enc = $r->header('Transfer-Encoding');
196 0         0 my $ct_type = $r->header('Content-Type');
197 0         0 my $ct_len = $r->header('Content-Length');
198              
199             # Act on the Expect header, if it's there
200 0         0 for my $e ($r->header('Expect')) {
201 0 0       0 if (lc($e) eq '100-continue') {
202 0         0 $self->send_status_line(100);
203 0         0 $self->send_crlf;
204             }
205             else {
206 0         0 $self->send_error(417);
207 0         0 $self->reason("Unsupported Expect header value");
208 0         0 return;
209             }
210             }
211              
212 0 0 0     0 if ($tr_enc && lc($tr_enc) eq 'chunked') {
    0 0        
    0          
    0          
213              
214             # Handle chunked transfer encoding
215 0         0 my $body = "";
216             CHUNK:
217 0         0 while (1) {
218 0 0       0 print STDERR "Chunked\n" if $DEBUG;
219 0 0       0 if ($buf =~ s/^([^\012]*)\012//) {
220 0         0 my $chunk_head = $1;
221 0 0       0 unless ($chunk_head =~ /^([0-9A-Fa-f]+)/) {
222 0         0 $self->send_error(400);
223 0         0 $self->reason("Bad chunk header $chunk_head");
224 0         0 return;
225             }
226 0         0 my $size = hex($1);
227 0 0       0 last CHUNK if $size == 0;
228              
229 0         0 my $missing = $size - length($buf) + 2; # 2=CRLF at chunk end
230             # must read until we have a complete chunk
231 0         0 while ($missing > 0) {
232 0 0       0 print STDERR "Need $missing more bytes\n" if $DEBUG;
233 0         0 my $n = $self->_need_more($buf, $timeout, $fdset);
234 0 0       0 return unless $n;
235 0         0 $missing -= $n;
236             }
237 0         0 $body .= substr($buf, 0, $size);
238 0         0 substr($buf, 0, $size + 2) = '';
239              
240             }
241             else {
242             # need more data in order to have a complete chunk header
243 0 0       0 return unless $self->_need_more($buf, $timeout, $fdset);
244             }
245             }
246 0         0 $r->content($body);
247              
248             # pretend it was a normal entity body
249 0         0 $r->remove_header('Transfer-Encoding');
250 0         0 $r->header('Content-Length', length($body));
251              
252 0         0 my ($key, $val);
253             FOOTER:
254 0         0 while (1) {
255 0 0       0 if ($buf !~ /\012/) {
256              
257             # need at least one line to look at
258 0 0       0 return unless $self->_need_more($buf, $timeout, $fdset);
259             }
260             else {
261 0         0 $buf =~ s/^([^\012]*)\012//;
262 0         0 $_ = $1;
263 0         0 s/\015$//;
264 0 0       0 if (/^([\w\-]+)\s*:\s*(.*)/) {
    0          
    0          
265 0 0       0 $r->push_header($key, $val) if $key;
266 0         0 ($key, $val) = ($1, $2);
267             }
268             elsif (/^\s+(.*)/) {
269 0         0 $val .= " $1";
270             }
271             elsif (!length) {
272 0         0 last FOOTER;
273             }
274             else {
275 0         0 $self->reason("Bad footer syntax");
276 0         0 return;
277             }
278             }
279             }
280 0 0       0 $r->push_header($key, $val) if $key;
281              
282             }
283             elsif ($tr_enc) {
284 0         0 $self->send_error(501); # Unknown transfer encoding
285 0         0 $self->reason("Unknown transfer encoding '$tr_enc'");
286 0         0 return;
287              
288             }
289             elsif ($ct_len) {
290              
291             # After a security issue, we ensure we comply to
292             # RFC-7230 -- HTTP/1.1 Message Syntax and Routing
293             # section 3.3.2 -- Content-Length
294             # section 3.3.3 -- Message Body Length
295              
296             # split and clean up Content-Length ', ' separated string
297 0         0 my @vals = map {my $str = $_; $str =~ s/^\s+//; $str =~ s/\s+$//; $str }
  0         0  
  0         0  
  0         0  
  0         0  
298             split ',', $ct_len;
299             # check that they are all numbers (RFC: Content-Length = 1*DIGIT)
300 0         0 my @nums = grep { /^[0-9]+$/} @vals;
  0         0  
301 0 0       0 unless (@vals == @nums) {
302 0         0 my $reason = "Content-Length value must be an unsigned integer";
303 0         0 $self->send_error(400, $reason);
304 0         0 $self->reason($reason);
305 0         0 return;
306             }
307             # check they are all the same
308 0         0 my $ct_len = shift @nums;
309 0         0 foreach (@nums) {
310 0 0       0 next if $_ == $ct_len;
311 0         0 my $reason = "Content-Length values are not the same";
312 0         0 $self->send_error(400, $reason);
313 0         0 $self->reason($reason);
314 0         0 return;
315             }
316             # ensure we have now a fixed header, with only 1 value
317 0         0 $r->header('Content-Length' => $ct_len);
318              
319             # Plain body specified by "Content-Length"
320 0         0 my $missing = $ct_len - length($buf);
321 0         0 while ($missing > 0) {
322 0 0       0 print "Need $missing more bytes of content\n" if $DEBUG;
323 0         0 my $n = $self->_need_more($buf, $timeout, $fdset);
324 0 0       0 return unless $n;
325 0         0 $missing -= $n;
326             }
327 0 0       0 if (length($buf) > $ct_len) {
328 0         0 $r->content(substr($buf, 0, $ct_len));
329 0         0 substr($buf, 0, $ct_len) = '';
330             }
331             else {
332 0         0 $r->content($buf);
333 0         0 $buf = '';
334             }
335             }
336             elsif ($ct_type && $ct_type =~ m/^multipart\/\w+\s*;.*boundary\s*=\s*("?)(\w+)\1/i) {
337              
338             # Handle multipart content type
339 0         0 my $boundary = "$CRLF--$2--";
340 0         0 my $index;
341 0         0 while (1) {
342 0         0 $index = index($buf, $boundary);
343 0 0       0 last if $index >= 0;
344              
345             # end marker not yet found
346 0 0       0 return unless $self->_need_more($buf, $timeout, $fdset);
347             }
348 0         0 $index += length($boundary);
349 0         0 $r->content(substr($buf, 0, $index));
350 0         0 substr($buf, 0, $index) = '';
351              
352             }
353 0         0 ${*$self}{'httpd_rbuf'} = $buf;
  0         0  
354              
355 0         0 $r;
356             }
357              
358             sub _need_more {
359 0     0   0 my $self = shift;
360              
361             #my($buf,$timeout,$fdset) = @_;
362 0 0       0 if ($_[1]) {
363 0         0 my ($timeout, $fdset) = @_[1, 2];
364 0 0       0 print STDERR "select(,,,$timeout)\n" if $DEBUG;
365 0         0 my $n = select($fdset, undef, undef, $timeout);
366 0 0       0 unless ($n) {
367 0 0       0 $self->reason(defined($n) ? "Timeout" : "select: $!");
368 0         0 return;
369             }
370             }
371 0 0       0 print STDERR "sysread()\n" if $DEBUG;
372 0         0 my $n = sysread($self, $_[0], 2048, length($_[0]));
373 0 0       0 $self->reason(defined($n) ? "Client closed" : "sysread: $!") unless $n;
    0          
374 0         0 $n;
375             }
376              
377             sub read_buffer {
378 0     0   0 my $self = shift;
379 0         0 my $old = ${*$self}{'httpd_rbuf'};
  0         0  
380 0 0       0 if (@_) {
381 0         0 ${*$self}{'httpd_rbuf'} = shift;
  0         0  
382             }
383 0         0 $old;
384             }
385              
386             sub reason {
387 0     0   0 my $self = shift;
388 0         0 my $old = ${*$self}{'httpd_reason'};
  0         0  
389 0 0       0 if (@_) {
390 0         0 ${*$self}{'httpd_reason'} = shift;
  0         0  
391             }
392 0         0 $old;
393             }
394              
395             sub proto_ge {
396 0     0   0 my $self = shift;
397 0         0 ${*$self}{'httpd_client_proto'} >= _http_version(shift);
  0         0  
398             }
399              
400             sub _http_version {
401 2     2   4 local ($_) = shift;
402 2 50       13 return 0 unless m,^(?:HTTP/)?(\d+)\.(\d+)$,i;
403 2         9 $1 * 1000 + $2;
404             }
405              
406             sub antique_client {
407 0     0     my $self = shift;
408 0           ${*$self}{'httpd_client_proto'} < $HTTP_1_0;
  0            
409             }
410              
411             sub force_last_request {
412 0     0     my $self = shift;
413 0           ${*$self}{'httpd_nomore'}++;
  0            
414             }
415              
416             sub head_request {
417 0     0     my $self = shift;
418 0           ${*$self}{'httpd_head'};
  0            
419             }
420              
421              
422             sub send_status_line {
423 0     0     my ($self, $status, $message, $proto) = @_;
424 0 0         return if $self->antique_client;
425 0   0       $status ||= RC_OK;
426 0   0       $message ||= status_message($status) || "";
      0        
427 0   0       $proto ||= $HTTP::Daemon::PROTO || "HTTP/1.1";
      0        
428 0           print $self "$proto $status $message$CRLF";
429             }
430              
431             sub send_crlf {
432 0     0     my $self = shift;
433 0           print $self $CRLF;
434             }
435              
436             sub send_basic_header {
437 0     0     my $self = shift;
438 0 0         return if $self->antique_client;
439 0           $self->send_status_line(@_);
440 0           print $self "Date: ", time2str(time), $CRLF;
441 0           my $product = $self->daemon->product_tokens;
442 0 0         print $self "Server: $product$CRLF" if $product;
443             }
444              
445             sub send_header {
446 0     0     my $self = shift;
447 0           while (@_) {
448 0           my ($k, $v) = splice(@_, 0, 2);
449 0 0         $v = "" unless defined($v);
450 0           print $self "$k: $v$CRLF";
451             }
452             }
453              
454             sub send_response {
455 0     0     my $self = shift;
456 0           my $res = shift;
457 0 0         if (!ref $res) {
458 0   0       $res ||= RC_OK;
459 0           $res = HTTP::Response->new($res, @_);
460             }
461 0           my $content = $res->content;
462 0           my $chunked;
463 0 0         unless ($self->antique_client) {
464 0           my $code = $res->code;
465 0           $self->send_basic_header($code, $res->message, $res->protocol);
466 0 0 0       if ($code =~ /^(1\d\d|[23]04)$/) {
    0          
    0          
    0          
467              
468             # make sure content is empty
469 0           $res->remove_header("Content-Length");
470 0           $content = "";
471             }
472             elsif ($res->request && $res->request->method eq "HEAD") {
473              
474             # probably OK
475             }
476             elsif (ref($content) eq "CODE") {
477 0 0         if ($self->proto_ge("HTTP/1.1")) {
478 0           $res->push_header("Transfer-Encoding" => "chunked");
479 0           $chunked++;
480             }
481             else {
482 0           $self->force_last_request;
483             }
484             }
485             elsif (length($content)) {
486 0           $res->header("Content-Length" => length($content));
487             }
488             else {
489 0           $self->force_last_request;
490 0           $res->header('connection', 'close');
491             }
492 0           print $self $res->headers_as_string($CRLF);
493 0           print $self $CRLF; # separates headers and content
494             }
495 0 0         if ($self->head_request) {
    0          
    0          
496              
497             # no content
498             }
499             elsif (ref($content) eq "CODE") {
500 0           while (1) {
501 0           my $chunk = &$content();
502 0 0 0       last unless defined($chunk) && length($chunk);
503 0 0         if ($chunked) {
504 0           printf $self "%x%s%s%s", length($chunk), $CRLF, $chunk, $CRLF;
505             }
506             else {
507 0           print $self $chunk;
508             }
509             }
510 0 0         print $self "0$CRLF$CRLF" if $chunked; # no trailers either
511             }
512             elsif (length $content) {
513 0           print $self $content;
514             }
515             }
516              
517             sub send_redirect {
518 0     0     my ($self, $loc, $status, $content) = @_;
519 0   0       $status ||= RC_MOVED_PERMANENTLY;
520 0 0         Carp::croak("Status '$status' is not redirect") unless is_redirect($status);
521 0           $self->send_basic_header($status);
522 0           my $base = $self->daemon->url;
523 0 0         $loc = $HTTP::URI_CLASS->new($loc, $base) unless ref($loc);
524 0           $loc = $loc->abs($base);
525 0           print $self "Location: $loc$CRLF";
526              
527 0 0         if ($content) {
528 0 0         my $ct_type = $content =~ /^\s*
529 0           print $self "Content-Type: $ct_type$CRLF";
530             }
531 0           print $self $CRLF;
532 0 0 0       print $self $content if $content && !$self->head_request;
533 0           $self->force_last_request; # no use keeping the connection open
534             }
535              
536             sub send_error {
537 0     0     my ($self, $status, $error) = @_;
538 0   0       $status ||= RC_BAD_REQUEST;
539 0 0         Carp::croak("Status '$status' is not an error") unless is_error($status);
540 0           my $mess = status_message($status);
541 0   0       $error ||= "";
542 0           $mess = <
543             $status $mess
544            

$status $mess

545             $error
546             EOT
547 0 0         unless ($self->antique_client) {
548 0           $self->send_basic_header($status);
549 0           print $self "Content-Type: text/html$CRLF";
550 0           print $self "Content-Length: " . length($mess) . $CRLF;
551 0           print $self $CRLF;
552             }
553 0 0         print $self $mess unless $self->head_request;
554 0           $status;
555             }
556              
557             sub send_file_response {
558 0     0     my ($self, $file) = @_;
559 0 0         if (-d $file) {
    0          
560 0           $self->send_dir($file);
561             }
562             elsif (-f _) {
563              
564             # plain file
565 0           local (*F);
566 0 0         sysopen(F, $file, 0) or return $self->send_error(RC_FORBIDDEN);
567 0           binmode(F);
568 0           my ($mime_type, $file_enc) = guess_media_type($file);
569 0           my ($size, $mtime) = (stat _)[7, 9];
570 0 0         unless ($self->antique_client) {
571 0           $self->send_basic_header;
572 0           print $self "Content-Type: $mime_type$CRLF";
573 0 0         print $self "Content-Encoding: $file_enc$CRLF" if $file_enc;
574 0 0         print $self "Content-Length: $size$CRLF" if $size;
575 0 0         print $self "Last-Modified: ", time2str($mtime), "$CRLF" if $mtime;
576 0           print $self $CRLF;
577             }
578 0 0         $self->send_file(\*F) unless $self->head_request;
579 0           return RC_OK;
580             }
581             else {
582 0           $self->send_error(RC_NOT_FOUND);
583             }
584             }
585              
586             sub send_dir {
587 0     0     my ($self, $dir) = @_;
588 0 0         $self->send_error(RC_NOT_FOUND) unless -d $dir;
589 0           $self->send_error(RC_NOT_IMPLEMENTED);
590             }
591              
592             sub send_file {
593 0     0     my ($self, $file) = @_;
594 0           my $opened = 0;
595 0           local (*FILE);
596 0 0         if (!ref($file)) {
597 0 0         open(FILE, $file) || return undef;
598 0           binmode(FILE);
599 0           $file = \*FILE;
600 0           $opened++;
601             }
602 0           my $cnt = 0;
603 0           my $buf = "";
604 0           my $n;
605 0           while ($n = sysread($file, $buf, 8 * 1024)) {
606 0 0         last if !$n;
607 0           $cnt += $n;
608 0           print $self $buf;
609             }
610 0 0         close($file) if $opened;
611 0           $cnt;
612             }
613              
614             sub daemon {
615 0     0     my $self = shift;
616 0           ${*$self}{'httpd_daemon'};
  0            
617             }
618              
619              
620             1;
621              
622             __END__