| blib/lib/Net/Server/HTTP.pm | |||
|---|---|---|---|
| Criterion | Covered | Total | % |
| statement | 214 | 399 | 53.6 |
| branch | 42 | 204 | 20.5 |
| condition | 22 | 88 | 25.0 |
| subroutine | 33 | 57 | 57.8 |
| pod | 30 | 39 | 76.9 |
| total | 341 | 787 | 43.3 |
| line | stmt | bran | cond | sub | pod | time | code |
|---|---|---|---|---|---|---|---|
| 1 | # -*- perl -*- | ||||||
| 2 | # | ||||||
| 3 | # Net::Server::HTTP - Extensible Perl HTTP base server | ||||||
| 4 | # | ||||||
| 5 | # Copyright (C) 2010-2017 | ||||||
| 6 | # | ||||||
| 7 | # Paul Seamons |
||||||
| 8 | # | ||||||
| 9 | # This package may be distributed under the terms of either the | ||||||
| 10 | # GNU General Public License | ||||||
| 11 | # or the | ||||||
| 12 | # Perl Artistic License | ||||||
| 13 | # | ||||||
| 14 | ################################################################ | ||||||
| 15 | |||||||
| 16 | package Net::Server::HTTP; | ||||||
| 17 | |||||||
| 18 | 4 | 4 | 9688 | use strict; | |||
| 4 | 10 | ||||||
| 4 | 116 | ||||||
| 19 | 4 | 4 | 18 | use base qw(Net::Server::MultiType); | |||
| 4 | 8 | ||||||
| 4 | 1220 | ||||||
| 20 | 4 | 4 | 22 | use Scalar::Util qw(weaken blessed); | |||
| 4 | 6 | ||||||
| 4 | 388 | ||||||
| 21 | 4 | 4 | 24 | use IO::Handle (); | |||
| 4 | 4 | ||||||
| 4 | 68 | ||||||
| 22 | 4 | 4 | 16 | use re 'taint'; # most of our regular expressions setting ENV should not be clearing taint | |||
| 4 | 8 | ||||||
| 4 | 168 | ||||||
| 23 | 4 | 4 | 18 | use POSIX (); | |||
| 4 | 6 | ||||||
| 4 | 56 | ||||||
| 24 | 4 | 4 | 1258 | use Time::HiRes qw(time); | |||
| 4 | 4036 | ||||||
| 4 | 16 | ||||||
| 25 | my $has_xs_parser; | ||||||
| 26 | 4 | 33 | 4 | 18552 | BEGIN {$has_xs_parser = $ENV{'USE_XS_PARSER'} && eval { require HTTP::Parser::XS } }; | ||
| 27 | |||||||
| 28 | 1 | 1 | 0 | 4 | sub net_server_type { __PACKAGE__ } | ||
| 29 | |||||||
| 30 | sub options { | ||||||
| 31 | 4 | 4 | 0 | 12 | my $self = shift; | ||
| 32 | 4 | 29 | my $ref = $self->SUPER::options(@_); | ||||
| 33 | 4 | 8 | my $prop = $self->{'server'}; | ||||
| 34 | 4 | 53 | $ref->{$_} = \$prop->{$_} for qw(timeout_header timeout_idle server_revision max_header_size | ||||
| 35 | access_log_format access_log_file enable_dispatch); | ||||||
| 36 | 4 | 7 | return $ref; | ||||
| 37 | } | ||||||
| 38 | |||||||
| 39 | 2 | 2 | 1 | 17 | sub timeout_header { shift->{'server'}->{'timeout_header'} } | ||
| 40 | 5 | 5 | 1 | 30 | sub timeout_idle { shift->{'server'}->{'timeout_idle'} } | ||
| 41 | 4 | 4 | 1 | 21 | sub server_revision { shift->{'server'}->{'server_revision'} } | ||
| 42 | 2 | 2 | 1 | 25 | sub max_header_size { shift->{'server'}->{'max_header_size'} } | ||
| 43 | |||||||
| 44 | 0 | 0 | 0 | 0 | sub default_port { 80 } | ||
| 45 | |||||||
| 46 | 0 | 0 | 0 | 0 | sub default_server_type { 'PreFork' } | ||
| 47 | |||||||
| 48 | sub post_configure { | ||||||
| 49 | 2 | 2 | 1 | 4 | my $self = shift; | ||
| 50 | 2 | 12 | $self->SUPER::post_configure(@_); | ||||
| 51 | 2 | 5 | my $prop = $self->{'server'}; | ||||
| 52 | |||||||
| 53 | # set other defaults | ||||||
| 54 | 2 | 22 | my $d = { | ||||
| 55 | timeout_header => 15, | ||||||
| 56 | timeout_idle => 60, | ||||||
| 57 | server_revision => __PACKAGE__."/$Net::Server::VERSION", | ||||||
| 58 | max_header_size => 100_000, | ||||||
| 59 | access_log_format => '%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"', | ||||||
| 60 | }; | ||||||
| 61 | 2 | 8 | $prop->{$_} = $d->{$_} foreach grep {!defined($prop->{$_})} keys %$d; | ||||
| 10 | 25 | ||||||
| 62 | |||||||
| 63 | 2 | 11 | $self->_init_access_log; | ||||
| 64 | |||||||
| 65 | 2 | 10 | $self->_tie_client_stdout; | ||||
| 66 | } | ||||||
| 67 | |||||||
| 68 | sub post_bind { | ||||||
| 69 | 2 | 2 | 1 | 4 | my $self = shift; | ||
| 70 | 2 | 22 | $self->SUPER::post_bind(@_); | ||||
| 71 | |||||||
| 72 | 2 | 24 | $self->_check_dispatch; | ||||
| 73 | } | ||||||
| 74 | |||||||
| 75 | sub _init_access_log { | ||||||
| 76 | 2 | 2 | 5 | my $self = shift; | |||
| 77 | 2 | 4 | my $prop = $self->{'server'}; | ||||
| 78 | 2 | 3 | my $log = $prop->{'access_log_file'}; | ||||
| 79 | 2 | 50 | 33 | 10 | return if ! $log || $log eq '/dev/null'; | ||
| 80 | 0 | 0 | 0 | return if ! $prop->{'access_log_format'}; | |||
| 81 | 0 | 0 | 0 | $prop->{'access_log_format'} =~ s/\\([\\\"nt])/$1 eq 'n' ? "\n" : $1 eq 't' ? "\t" : $1/eg; | |||
| 0 | 0 | 0 | |||||
| 82 | 0 | 0 | 0 | if ($log eq 'STDERR') { | |||
| 83 | 0 | 0 | 0 | $prop->{'access_log_function'} = sub { print STDERR @_,"\n" }; | |||
| 0 | 0 | ||||||
| 84 | } else { | ||||||
| 85 | 0 | 0 | 0 | open my $fh, '>>', $log or die "Could not open access_log_file \"$log\": $!"; | |||
| 86 | 0 | 0 | $fh->autoflush(1); | ||||
| 87 | 0 | 0 | push @{ $prop->{'chown_files'} }, $log; | ||||
| 0 | 0 | ||||||
| 88 | 0 | 0 | 0 | $prop->{'access_log_function'} = sub { print $fh @_,"\n" }; | |||
| 0 | 0 | ||||||
| 89 | } | ||||||
| 90 | } | ||||||
| 91 | |||||||
| 92 | sub _tie_client_stdout { | ||||||
| 93 | 1 | 1 | 3 | my $self = shift; | |||
| 94 | 1 | 2 | my $prop = $self->{'server'}; | ||||
| 95 | |||||||
| 96 | # install a callback that will handle our outbound header negotiation for the clients similar to what apache does for us | ||||||
| 97 | 1 | 2 | my $copy = $self; | ||||
| 98 | 1 | 2 | $prop->{'tie_client_stdout'} = 1; | ||||
| 99 | $prop->{'tied_stdout_callback'} = sub { | ||||||
| 100 | 3 | 3 | 7 | my $client = shift; | |||
| 101 | 3 | 11 | my $method = shift; | ||||
| 102 | 3 | 12 | alarm($copy->timeout_idle); # reset timeout | ||||
| 103 | |||||||
| 104 | 3 | 10 | my $request_info = $copy->{'request_info'}; | ||||
| 105 | 3 | 100 | 14 | if ($request_info->{'headers_sent'}) { # keep track of how much has been printed | |||
| 106 | 2 | 7 | my ($resp, $len); | ||||
| 107 | 2 | 50 | 7 | if ($method eq 'print') { | |||
| 0 | |||||||
| 0 | |||||||
| 0 | |||||||
| 0 | |||||||
| 108 | 2 | 15 | $resp = $client->print(my $str = join '', @_); | ||||
| 109 | 2 | 175 | $len = length $str; | ||||
| 110 | } elsif ($method eq 'printf') { | ||||||
| 111 | 0 | 0 | $resp = $client->print(my $str = sprintf(shift, @_)); | ||||
| 112 | 0 | 0 | $len = length $str; | ||||
| 113 | } elsif ($method eq 'say') { | ||||||
| 114 | 0 | 0 | $resp = $client->print(my $str = join '', @_, "\n"); | ||||
| 115 | 0 | 0 | $len = length $str; | ||||
| 116 | } elsif ($method eq 'write') { | ||||||
| 117 | 0 | 0 | my $buf = shift; | ||||
| 118 | 0 | 0 | 0 | 0 | $buf = substr($buf, $_[1] || 0, $_[0]) if @_; | ||
| 119 | 0 | 0 | $resp = $client->print($buf); | ||||
| 120 | 0 | 0 | $len = length $buf; | ||||
| 121 | } elsif ($method eq 'syswrite') { | ||||||
| 122 | 0 | 0 | $len = $resp = $client->syswrite(@_); | ||||
| 123 | } else { | ||||||
| 124 | 0 | 0 | return $client->$method(@_); | ||||
| 125 | } | ||||||
| 126 | 2 | 50 | 100 | 20 | $request_info->{'response_size'} = ($request_info->{'response_size'} || 0) + $len if defined $len; | ||
| 127 | 2 | 12 | return $resp; | ||||
| 128 | } | ||||||
| 129 | |||||||
| 130 | 1 | 50 | 5 | die "All headers must only be sent via print ($method)\n" if $method ne 'print'; | |||
| 131 | |||||||
| 132 | 1 | 50 | 2 | my $headers = ${*$client}{'headers'} ||= {unparsed => '', parsed => ''}; | |||
| 1 | 33 | ||||||
| 133 | 1 | 11 | $headers->{'unparsed'} .= join('', @_); | ||||
| 134 | 1 | 16 | while ($headers->{'unparsed'} =~ s/^(.*?)\015?\012//) { | ||||
| 135 | 2 | 6 | my $line = $1; | ||||
| 136 | |||||||
| 137 | 2 | 50 | 66 | 25 | if (!$headers->{'parsed'} && $line =~ m{^HTTP/(1.[01]) \s+ (\d+) (?: | \s+ .+)$ }x) { | ||
| 100 | |||||||
| 50 | |||||||
| 138 | 0 | 0 | $headers->{'status'} = []; | ||||
| 139 | 0 | 0 | $headers->{'parsed'} .= "$line\015\012"; | ||||
| 140 | 0 | 0 | $prop->{'request_info'}->{'http_version'} = $1; | ||||
| 141 | 0 | 0 | $prop->{'request_info'}->{'response_status'} = $2; | ||||
| 142 | } | ||||||
| 143 | elsif (! length $line) { | ||||||
| 144 | 1 | 50 | 3 | my $s = $headers->{'status'} || die "Premature end of script headers\n"; | |||
| 145 | 1 | 2 | delete ${*$client}{'headers'}; | ||||
| 1 | 4 | ||||||
| 146 | 1 | 50 | 12 | $copy->send_status(@$s) if @$s; | |||
| 147 | 1 | 5 | $client->print($headers->{'parsed'}."\015\012"); | ||||
| 148 | 1 | 35 | $request_info->{'headers_sent'} = 1; | ||||
| 149 | 1 | 3 | $request_info->{'response_header_size'} += length($headers->{'parsed'})+2; | ||||
| 150 | 1 | 2 | $request_info->{'response_size'} = length($headers->{'unparsed'}); | ||||
| 151 | 1 | 4 | return $client->print($headers->{'unparsed'}); | ||||
| 152 | } elsif ($line !~ s/^(\w+(?:-(?:\w+))*):\s*//) { | ||||||
| 153 | 0 | 0 | 0 | my $invalid = ($line =~ /(.{0,120})/) ? "$1..." : ''; | |||
| 154 | 0 | 0 | $invalid =~ s/</g; | ||||
| 155 | 0 | 0 | die "Premature end of script headers: $invalid \n"; |
||||
| 156 | } else { | ||||||
| 157 | 1 | 5 | my $key = "\u\L$1"; | ||||
| 158 | 1 | 3 | $key =~ y/_/-/; | ||||
| 159 | 1 | 3 | push @{ $request_info->{'response_headers'} }, [$key, $line]; | ||||
| 1 | 10 | ||||||
| 160 | 1 | 50 | 33 | 6 | if ($key eq 'Status' && $line =~ /^(\d+) (?:|\s+(.+?))$/ix) { | ||
| 50 | |||||||
| 50 | |||||||
| 161 | 0 | 0 | 0 | $headers->{'status'} = [$1, $2 || '-']; | |||
| 162 | } | ||||||
| 163 | elsif ($key eq 'Location') { | ||||||
| 164 | 0 | 0 | $headers->{'status'} = [302, 'bouncing']; | ||||
| 165 | } | ||||||
| 166 | elsif ($key eq 'Content-type') { | ||||||
| 167 | 1 | 50 | 13 | $headers->{'status'} ||= [200, 'OK']; | |||
| 168 | } | ||||||
| 169 | 1 | 7 | $headers->{'parsed'} .= "$key: $line\015\012"; | ||||
| 170 | } | ||||||
| 171 | } | ||||||
| 172 | 1 | 27 | }; | ||||
| 173 | 1 | 15 | weaken $copy; | ||||
| 174 | } | ||||||
| 175 | |||||||
| 176 | sub _check_dispatch { | ||||||
| 177 | 2 | 2 | 49 | my $self = shift; | |||
| 178 | 2 | 50 | 10 | if (! $self->{'server'}->{'enable_dispatch'}) { | |||
| 179 | 2 | 100 | 61 | return if __PACKAGE__->can('process_request') ne $self->can('process_request'); | |||
| 180 | 1 | 50 | 12 | return if __PACKAGE__->can('process_http_request') ne $self->can('process_http_request'); | |||
| 181 | } | ||||||
| 182 | |||||||
| 183 | 1 | 6 | my $app = $self->{'server'}->{'app'}; | ||||
| 184 | 1 | 50 | 0 | 4 | if (! $app || (ref($app) eq 'ARRAY' && !@$app)) { | ||
| 33 | |||||||
| 185 | 1 | 2 | $app = []; | ||||
| 186 | 1 | 4 | $self->configure({app => $app}); | ||||
| 187 | } | ||||||
| 188 | |||||||
| 189 | 1 | 5 | my %dispatch; | ||||
| 190 | my $first; | ||||||
| 191 | 1 | 0 | my @dispatch; | ||||
| 192 | 1 | 50 | 8 | foreach my $a (ref($app) eq 'ARRAY' ? @$app : $app) { | |||
| 193 | 0 | 0 | 0 | next if ! $a; | |||
| 194 | 0 | 0 | 0 | my @pairs = ref($a) eq 'ARRAY' ? @$a | |||
| 0 | |||||||
| 0 | |||||||
| 0 | |||||||
| 0 | |||||||
| 195 | : ref($a) eq 'HASH' ? %$a | ||||||
| 196 | : ref($a) eq 'CODE' ? ('/', $a) | ||||||
| 197 | : $a =~ m{^(.+?)\s+(.+)$} ? ($1, $2) | ||||||
| 198 | : $a =~ m{^(.+?)=(.+)$} ? ($1, $2) | ||||||
| 199 | : ($a, $a); | ||||||
| 200 | 0 | 0 | for (my $i = 0; $i < @pairs; $i+=2) { | ||||
| 201 | 0 | 0 | my ($key, $val) = ("/$pairs[$i]", $pairs[$i+1]); | ||||
| 202 | 0 | 0 | $key =~ s{/\./}{/}g; | ||||
| 203 | 0 | 0 | $key =~ s{(?:/[^/]+|)/\../}{/}g; | ||||
| 204 | 0 | 0 | $key =~ s{//+}{/}g; | ||||
| 205 | 0 | 0 | 0 | if ($dispatch{$key}) { | |||
| 206 | 0 | 0 | $self->log(2, "Already found a path matching \"$key\" - skipping."); | ||||
| 207 | 0 | 0 | next; | ||||
| 208 | } | ||||||
| 209 | 0 | 0 | $dispatch{$key} = $val; | ||||
| 210 | 0 | 0 | push @dispatch, $key; | ||||
| 211 | 0 | 0 | 0 | $first ||= $key; | |||
| 212 | 0 | 0 | $self->log(2, " Dispatch: $key => $val"); | ||||
| 213 | } | ||||||
| 214 | } | ||||||
| 215 | 1 | 50 | 8 | if (@dispatch) { | |||
| 216 | 0 | 0 | 0 | 0 | if (! $dispatch{'/'} && $first) { | ||
| 217 | 0 | 0 | $dispatch{'/'} = $dispatch{$first}; | ||||
| 218 | 0 | 0 | push @dispatch, '/'; | ||||
| 219 | 0 | 0 | $self->log(2, " Dispatch: / => $dispatch{$first} (default)"); | ||||
| 220 | } | ||||||
| 221 | 0 | 0 | $self->{'dispatch_qr'} = join "|", map {"\Q$_\E"} @dispatch; | ||||
| 0 | 0 | ||||||
| 222 | 0 | 0 | $self->{'dispatch'} = \%dispatch; | ||||
| 223 | } | ||||||
| 224 | } | ||||||
| 225 | |||||||
| 226 | sub http_base_headers { | ||||||
| 227 | 2 | 2 | 0 | 3 | my $self = shift; | ||
| 228 | return [ | ||||||
| 229 | 2 | 70 | [Date => gmtime()." GMT"], | ||||
| 230 | [Connection => 'close'], | ||||||
| 231 | [Server => $self->server_revision], | ||||||
| 232 | ]; | ||||||
| 233 | } | ||||||
| 234 | |||||||
| 235 | sub send_status { | ||||||
| 236 | 2 | 2 | 1 | 7 | my ($self, $status, $msg, $body) = @_; | ||
| 237 | 2 | 50 | 66 | 13 | $msg ||= ($status == 200) ? 'OK' : '-'; | ||
| 238 | 2 | 4 | my $request_info = $self->{'request_info'}; | ||||
| 239 | |||||||
| 240 | 2 | 7 | my $out = "HTTP/1.0 $status $msg\015\012"; | ||||
| 241 | 2 | 4 | foreach my $row (@{ $self->http_base_headers }) { | ||||
| 2 | 9 | ||||||
| 242 | 6 | 17 | $out .= "$row->[0]: $row->[1]\015\012"; | ||||
| 243 | 6 | 8 | push @{ $request_info->{'response_headers'} }, $row; | ||||
| 6 | 14 | ||||||
| 244 | } | ||||||
| 245 | 2 | 29 | $self->{'server'}->{'client'}->print($out); | ||||
| 246 | 2 | 172 | $request_info->{'http_version'} = '1.0'; | ||||
| 247 | 2 | 8 | $request_info->{'response_status'} = $status; | ||||
| 248 | 2 | 5 | $request_info->{'response_header_size'} += length $out; | ||||
| 249 | |||||||
| 250 | 2 | 50 | 12 | if ($body) { | |||
| 251 | 0 | 0 | push @{ $request_info->{'response_headers'} }, ['Content-type', 'text/html']; | ||||
| 0 | 0 | ||||||
| 252 | 0 | 0 | $out = "Content-type: text/html\015\012\015\012"; | ||||
| 253 | 0 | 0 | $request_info->{'response_header_size'} += length $out; | ||||
| 254 | 0 | 0 | $self->{'server'}->{'client'}->print($out); | ||||
| 255 | 0 | 0 | $request_info->{'headers_sent'} = 1; | ||||
| 256 | 0 | 0 | $self->{'server'}->{'client'}->print($body); | ||||
| 257 | 0 | 0 | $request_info->{'response_size'} += length $body; | ||||
| 258 | } | ||||||
| 259 | } | ||||||
| 260 | |||||||
| 261 | sub send_500 { | ||||||
| 262 | 0 | 0 | 1 | 0 | my ($self, $err) = @_; | ||
| 263 | 0 | 0 | $self->send_status(500, 'Internal Server Error', | ||||
| 264 | "Internal Server Error$err "); |
||||||
| 265 | } | ||||||
| 266 | |||||||
| 267 | ###----------------------------------------------------------------### | ||||||
| 268 | |||||||
| 269 | sub run_client_connection { | ||||||
| 270 | 2 | 2 | 1 | 4 | my $self = shift; | ||
| 271 | 2 | 7 | local $self->{'request_info'} = {}; | ||||
| 272 | 2 | 15 | return $self->SUPER::run_client_connection(@_); | ||||
| 273 | } | ||||||
| 274 | |||||||
| 275 | sub get_client_info { | ||||||
| 276 | 2 | 2 | 1 | 5 | my $self = shift; | ||
| 277 | 2 | 21 | $self->SUPER::get_client_info(@_); | ||||
| 278 | 2 | 18 | $self->clear_http_env; | ||||
| 279 | } | ||||||
| 280 | |||||||
| 281 | sub clear_http_env { | ||||||
| 282 | 2 | 2 | 0 | 4 | my $self = shift; | ||
| 283 | 2 | 199 | %ENV = (); | ||||
| 284 | } | ||||||
| 285 | |||||||
| 286 | sub process_request { | ||||||
| 287 | 1 | 1 | 1 | 1 | my $self = shift; | ||
| 288 | 1 | 33 | 7 | my $client = shift || $self->{'server'}->{'client'}; | |||
| 289 | |||||||
| 290 | 1 | 2 | my $ok = eval { | ||||
| 291 | 1 | 0 | 21 | local $SIG{'ALRM'} = sub { die "Server Timeout on headers\n" }; | |||
| 0 | 0 | ||||||
| 292 | 1 | 9 | alarm($self->timeout_header); | ||||
| 293 | 1 | 6 | $self->process_headers($client); | ||||
| 294 | |||||||
| 295 | 1 | 0 | 7 | $SIG{'ALRM'} = sub { die "Server Timeout on process\n" }; | |||
| 0 | 0 | ||||||
| 296 | 1 | 7 | alarm($self->timeout_idle); | ||||
| 297 | 1 | 4 | $self->process_http_request($client); | ||||
| 298 | |||||||
| 299 | 1 | 9 | alarm(0); | ||||
| 300 | 1 | 19 | 1; | ||||
| 301 | }; | ||||||
| 302 | 1 | 5 | alarm(0); | ||||
| 303 | |||||||
| 304 | 1 | 50 | 9 | if (! $ok) { | |||
| 305 | 0 | 0 | 0 | my $err = "$@" || "Something happened"; | |||
| 306 | 0 | 0 | $self->log(1, $err); | ||||
| 307 | 0 | 0 | $self->send_500($err); | ||||
| 308 | } | ||||||
| 309 | } | ||||||
| 310 | |||||||
| 311 | 2 | 50 | 2 | 0 | 23 | sub script_name { shift->{'script_name'} || '' } | |
| 312 | |||||||
| 313 | sub process_headers { | ||||||
| 314 | 2 | 2 | 1 | 4 | my $self = shift; | ||
| 315 | 2 | 66 | 11 | my $client = shift || $self->{'server'}->{'client'}; | |||
| 316 | |||||||
| 317 | 2 | 14 | $ENV{'REMOTE_PORT'} = $self->{'server'}->{'peerport'}; | ||||
| 318 | 2 | 9 | $ENV{'REMOTE_ADDR'} = $self->{'server'}->{'peeraddr'}; | ||||
| 319 | 2 | 6 | $ENV{'SERVER_PORT'} = $self->{'server'}->{'sockport'}; | ||||
| 320 | 2 | 10 | $ENV{'SERVER_ADDR'} = $self->{'server'}->{'sockaddr'}; | ||||
| 321 | 2 | 50 | 7 | $ENV{'HTTPS'} = 'on' if $self->{'server'}->{'client'}->NS_proto =~ /SSL/; | |||
| 322 | |||||||
| 323 | 2 | 9 | my ($ok, $headers) = $client->read_until($self->max_header_size, qr{\n\r?\n}); | ||||
| 324 | 2 | 15 | my ($req, $len, @parsed); | ||||
| 325 | 2 | 50 | 7 | die "Could not parse http headers successfully\n" if $ok != 1; | |||
| 326 | 2 | 50 | 7 | if ($has_xs_parser) { | |||
| 327 | 0 | 0 | $len = HTTP::Parser::XS::parse_http_request($headers, \%ENV); | ||||
| 328 | 0 | 0 | 0 | die "Corrupt request" if $len == -1; | |||
| 329 | 0 | 0 | 0 | die "Incomplete request" if $len == -2; | |||
| 330 | 0 | 0 | $req = "$ENV{'REQUEST_METHOD'} $ENV{'REQUEST_URI'} $ENV{'SERVER_PROTOCOL'}"; | ||||
| 331 | } else { | ||||||
| 332 | 2 | 16 | ($req, my @lines) = split /\r?\n/, $headers; | ||||
| 333 | 2 | 50 | 6 | die "Missing request\n" if ! defined $req; | |||
| 334 | |||||||
| 335 | 2 | 50 | 33 | 25 | if (!defined($req) || $req !~ m{ ^\s*(GET|POST|PUT|DELETE|PUSH|HEAD|OPTIONS)\s+(.+)\s+(HTTP/1\.[01])\s*$ }ix) { | ||
| 336 | 0 | 0 | die "Invalid request\n"; | ||||
| 337 | } | ||||||
| 338 | 2 | 19 | $ENV{'REQUEST_METHOD'} = uc $1; | ||||
| 339 | 2 | 14 | $ENV{'REQUEST_URI'} = $2; | ||||
| 340 | 2 | 8 | $ENV{'SERVER_PROTOCOL'} = $3; | ||||
| 341 | 2 | 50 | 10 | $ENV{'QUERY_STRING'} = $1 if $ENV{'REQUEST_URI'} =~ m{ \?(.*)$ }x; | |||
| 342 | 2 | 50 | 14 | $ENV{'PATH_INFO'} = $1 if $ENV{'REQUEST_URI'} =~ m{^([^\?]+)}; | |||
| 343 | |||||||
| 344 | 2 | 8 | foreach my $l (@lines) { | ||||
| 345 | 2 | 12 | my ($key, $val) = split /\s*:\s*/, $l, 2; | ||||
| 346 | 2 | 7 | push @parsed, [$key, $val]; | ||||
| 347 | 2 | 6 | $key = uc($key); | ||||
| 348 | 2 | 50 | 5 | $key = 'COOKIE' if $key eq 'COOKIES'; | |||
| 349 | 2 | 5 | $key =~ y/-/_/; | ||||
| 350 | 2 | 6 | $key =~ s/^\s+//; | ||||
| 351 | 2 | 50 | 7 | $key = "HTTP_$key" if $key !~ /^CONTENT_(?:LENGTH|TYPE)$/; | |||
| 352 | 2 | 5 | $val =~ s/\s+$//; | ||||
| 353 | 2 | 50 | 8 | if (exists $ENV{$key}) { | |||
| 354 | 0 | 0 | $ENV{$key} .= ", $val"; | ||||
| 355 | } else { | ||||||
| 356 | 2 | 7 | $ENV{$key} = $val; | ||||
| 357 | } | ||||||
| 358 | } | ||||||
| 359 | 2 | 6 | $len = length $headers; | ||||
| 360 | } | ||||||
| 361 | 2 | 50 | 19 | $ENV{'SCRIPT_NAME'} = $self->script_name($ENV{'PATH_INFO'}) || ''; | |||
| 362 | |||||||
| 363 | 2 | 9 | my $type = $Net::Server::HTTP::ISA[0]; | ||||
| 364 | 2 | 50 | 8 | $type = $Net::Server::MultiType::ISA[0] if $type eq 'Net::Server::MultiType'; | |||
| 365 | 2 | 10 | $ENV{'NET_SERVER_TYPE'} = $type; | ||||
| 366 | 2 | 12 | $ENV{'NET_SERVER_SOFTWARE'} = $self->server_revision; | ||||
| 367 | |||||||
| 368 | 2 | 15 | $self->_init_http_request_info($req, \@parsed, $len); | ||||
| 369 | } | ||||||
| 370 | |||||||
| 371 | 0 | 0 | 1 | 0 | sub http_request_info { shift->{'request_info'} } | ||
| 372 | |||||||
| 373 | sub _init_http_request_info { | ||||||
| 374 | 2 | 2 | 6 | my ($self, $req, $parsed, $len) = @_; | |||
| 375 | 2 | 5 | my $prop = $self->{'server'}; | ||||
| 376 | 2 | 4 | my $info = $self->{'request_info'}; | ||||
| 377 | 2 | 9 | @$info{qw(sockaddr sockport peeraddr peerport)} = @$prop{qw(sockaddr sockport peeraddr peerport)}; | ||||
| 378 | 2 | 33 | 15 | $info->{'peerhost'} = $prop->{'peerhost'} || $info->{'peeraddr'}; | |||
| 379 | 2 | 18 | $info->{'begin'} = time; | ||||
| 380 | 2 | 6 | $info->{'request'} = $req; | ||||
| 381 | 2 | 7 | $info->{'request_headers'} = $parsed; | ||||
| 382 | 2 | 50 | 7 | $info->{'query_string'} = "?$ENV{'QUERY_STRING'}" if defined $ENV{'QUERY_STRING'}; | |||
| 383 | 2 | 50 | 6 | $info->{'request_protocol'} = $ENV{'HTTPS'} ? 'https' : 'http'; | |||
| 384 | 2 | 6 | $info->{'request_method'} = $ENV{'REQUEST_METHOD'}; | ||||
| 385 | 2 | 3 | $info->{'request_path'} = $ENV{'PATH_INFO'}; | ||||
| 386 | 2 | 4 | $info->{'request_header_size'} = $len; | ||||
| 387 | 2 | 50 | 9 | $info->{'request_size'} = $ENV{'CONTENT_LENGTH'} || 0; # we might not actually read entire request | |||
| 388 | 2 | 10 | $info->{'remote_user'} = '-'; | ||||
| 389 | } | ||||||
| 390 | |||||||
| 391 | sub http_note { | ||||||
| 392 | 0 | 0 | 1 | 0 | my ($self, $key, $val) = @_; | ||
| 393 | 0 | 0 | 0 | return $self->{'request_info'}->{'notes'}->{$key} = $val if @_ >= 3; | |||
| 394 | 0 | 0 | return $self->{'request_info'}->{'notes'}->{$key}; | ||||
| 395 | } | ||||||
| 396 | |||||||
| 397 | sub http_dispatch { | ||||||
| 398 | 0 | 0 | 1 | 0 | my ($self, $dispatch_qr, $dispatch_table) = @_; | ||
| 399 | |||||||
| 400 | 0 | 0 | 0 | $ENV{'PATH_INFO'} =~ s{^($dispatch_qr)(?=/|$|(?<=/))}{} or die "Dispatch not found\n"; | |||
| 401 | 0 | 0 | $ENV{'SCRIPT_NAME'} = $1; | ||||
| 402 | 0 | 0 | 0 | if ($ENV{'PATH_INFO'}) { | |||
| 403 | 0 | 0 | 0 | $ENV{'PATH_INFO'} = "/$ENV{'PATH_INFO'}" if $ENV{'PATH_INFO'} !~ m{^/}; | |||
| 404 | 0 | 0 | $ENV{'PATH_INFO'} =~ s/%([a-fA-F0-9]{2})/chr(hex $1)/eg; | ||||
| 0 | 0 | ||||||
| 405 | } | ||||||
| 406 | 0 | 0 | my $code = $self->{'dispatch'}->{$1}; | ||||
| 407 | 0 | 0 | 0 | return $self->$code() if ref $code; | |||
| 408 | 0 | 0 | $self->exec_cgi($code); | ||||
| 409 | } | ||||||
| 410 | |||||||
| 411 | sub process_http_request { | ||||||
| 412 | 1 | 1 | 1 | 3 | my ($self, $client) = @_; | ||
| 413 | |||||||
| 414 | 1 | 50 | 4 | if (my $table = $self->{'dispatch'}) { | |||
| 415 | 0 | 0 | 0 | my $qr = $self->{'dispatch_qr'} or die "Dispatch was not correctly setup\n"; | |||
| 416 | 0 | 0 | return $self->http_dispatch($qr, $table) | ||||
| 417 | } | ||||||
| 418 | |||||||
| 419 | 1 | 5 | return $self->http_echo; | ||||
| 420 | } | ||||||
| 421 | |||||||
| 422 | sub http_echo { | ||||||
| 423 | 1 | 1 | 0 | 2 | my $self = shift; | ||
| 424 | 1 | 6 | print "Content-type: text/html\n\n"; | ||||
| 425 | 1 | 10 | print "\n"; | ||||
| 426 | 1 | 50 | 3 | if (eval { require Data::Dumper }) { | |||
| 1 | 489 | ||||||
| 427 | 1 | 5497 | local $Data::Dumper::Sortkeys = 1; | ||||
| 428 | 1 | 3 | my $form = {}; | ||||
| 429 | 1 | 50 | 2 | if (eval { require CGI }) { my $q = CGI->new; $form->{$_} = $q->param($_) for $q->param; } | |||
| 1 | 605 | ||||||
| 1 | 27928 | ||||||
| 1 | 483 | ||||||
| 430 | 1 | 42 | print "".Data::Dumper->Dump([\%ENV, $form], ['*ENV', 'form']).""; |
||||
| 431 | } | ||||||
| 432 | } | ||||||
| 433 | |||||||
| 434 | sub post_process_request { | ||||||
| 435 | 2 | 2 | 1 | 6 | my $self = shift; | ||
| 436 | 2 | 6 | my $info = $self->{'request_info'}; | ||||
| 437 | 2 | 50 | 9 | $info->{'begin'} = time unless defined $info->{'begin'}; | |||
| 438 | 2 | 18 | $info->{'elapsed'} = time - $info->{'begin'}; | ||||
| 439 | 2 | 22 | $self->SUPER::post_process_request(@_); | ||||
| 440 | 2 | 193 | $self->log_http_request($info); | ||||
| 441 | } | ||||||
| 442 | |||||||
| 443 | ###----------------------------------------------------------------### | ||||||
| 444 | |||||||
| 445 | sub log_http_request { | ||||||
| 446 | 2 | 2 | 0 | 7 | my ($self, $info) = @_; | ||
| 447 | 2 | 6 | my $prop = $self->{'server'}; | ||||
| 448 | 2 | 50 | 13 | my $fmt = $prop->{'access_log_format'} || return; | |||
| 449 | 2 | 50 | 11 | my $log = $prop->{'access_log_function'} || return; | |||
| 450 | 0 | $log->($self->http_log_format($fmt, $info)); | |||||
| 451 | } | ||||||
| 452 | |||||||
| 453 | my %fmt_map = qw( | ||||||
| 454 | a peeraddr | ||||||
| 455 | A sockaddr | ||||||
| 456 | B response_size | ||||||
| 457 | f filename | ||||||
| 458 | h peerhost | ||||||
| 459 | H request_protocol | ||||||
| 460 | l remote_logname | ||||||
| 461 | m request_method | ||||||
| 462 | p sockport | ||||||
| 463 | q query_string | ||||||
| 464 | r request | ||||||
| 465 | s response_status | ||||||
| 466 | u remote_user | ||||||
| 467 | U request_path | ||||||
| 468 | ); | ||||||
| 469 | my %fmt_code = qw( | ||||||
| 470 | C http_log_cookie | ||||||
| 471 | e http_log_env | ||||||
| 472 | i http_log_header_in | ||||||
| 473 | n http_log_note | ||||||
| 474 | o http_log_header_out | ||||||
| 475 | P http_log_pid | ||||||
| 476 | t http_log_time | ||||||
| 477 | v http_log_vhost | ||||||
| 478 | V http_log_vhost | ||||||
| 479 | X http_log_constat | ||||||
| 480 | ); | ||||||
| 481 | |||||||
| 482 | sub http_log_format { | ||||||
| 483 | 0 | 0 | 1 | my ($self, $fmt, $info, $orig) = @_; | |||
| 484 | 0 | $fmt =~ s{ % ([<>])? # 1 | |||||
| 485 | (!? \d\d\d (?:,\d\d\d)* )? # 2 | ||||||
| 486 | (?: \{ ([^\}]+) \} )? # 3 | ||||||
| 487 | ([aABDfhHmpqrsTuUvVhblPtIOCeinoPtX%]) # 4 | ||||||
| 488 | }{ | ||||||
| 489 | 0 | 0 | 0 | $info = $orig if $1 && $orig && $1 eq '<'; | |||
| 0 | |||||||
| 490 | my $v = $2 && (substr($2,0,1) eq '!' ? index($2, $info->{'response_status'})!=-1 : index($2, $info->{'response_status'})==-1) ? '-' | ||||||
| 491 | : $fmt_map{$4} ? $info->{$fmt_map{$4}} | ||||||
| 492 | 0 | : $fmt_code{$4} ? do { my $m = $fmt_code{$4}; $self->$m($info, $3, $1, $4) } | |||||
| 0 | |||||||
| 493 | : $4 eq 'b' ? $info->{'response_size'} || '-' # B can be 0, b cannot | ||||||
| 494 | : $4 eq 'I' ? $info->{'request_size'} + $info->{'request_header_size'} | ||||||
| 495 | : $4 eq 'O' ? $info->{'response_size'} + $info->{'response_header_size'} | ||||||
| 496 | : $4 eq 'T' ? sprintf('%d', $info->{'elapsed'}) | ||||||
| 497 | 0 | 0 | 0 | : $4 eq 'D' ? sprintf('%d', $info->{'elapsed'}/.000_001) | |||
| 0 | 0 | ||||||
| 0 | |||||||
| 0 | |||||||
| 0 | |||||||
| 0 | |||||||
| 0 | |||||||
| 0 | |||||||
| 0 | |||||||
| 498 | : $4 eq '%' ? '%' | ||||||
| 499 | : '-'; | ||||||
| 500 | 0 | 0 | 0 | $v = '-' if !defined($v) || !length($v); | |||
| 501 | 0 | 0 | $v =~ s/([^\ -\!\#-\[\]-\~])/$1 eq "\n" ? '\n' : $1 eq "\t" ? '\t' : sprintf('\x%02X', ord($1))/eg; # escape non-printable or " or \ | ||||
| 0 | 0 | ||||||
| 502 | 0 | $v; | |||||
| 503 | }gxe; | ||||||
| 504 | 0 | return $fmt; | |||||
| 505 | } | ||||||
| 506 | sub http_log_time { | ||||||
| 507 | 0 | 0 | 1 | my ($self, $info, $fmt) = @_; | |||
| 508 | 0 | 0 | return '['.POSIX::strftime($fmt || '%d/%b/%Y:%T %z', localtime($info->{'begin'})).']'; | ||||
| 509 | } | ||||||
| 510 | 0 | 0 | 1 | sub http_log_env { $ENV{$_[2]} } | |||
| 511 | sub http_log_cookie { | ||||||
| 512 | 0 | 0 | 1 | my ($self, $info, $var) = @_; | |||
| 513 | 0 | my @c; | |||||
| 514 | 0 | 0 | for my $cookie (map {$_->[1]} grep {$_->[0] eq 'Cookie' } @{ $info->{'request_headers'} || [] }) { | ||||
| 0 | |||||||
| 0 | |||||||
| 0 | |||||||
| 515 | 0 | 0 | push @c, $1 if $cookie =~ /^\Q$var\E=(.*)/; | ||||
| 516 | } | ||||||
| 517 | 0 | return join ', ', @c; | |||||
| 518 | } | ||||||
| 519 | sub http_log_header_in { | ||||||
| 520 | 0 | 0 | 1 | my ($self, $info, $var) = @_; | |||
| 521 | 0 | 0 | return join ', ', map {$_->[1]} grep {$_->[0] eq $var} @{ $info->{'request_headers'} || [] }; | ||||
| 0 | |||||||
| 0 | |||||||
| 0 | |||||||
| 522 | } | ||||||
| 523 | sub http_log_note { | ||||||
| 524 | 0 | 0 | 1 | my ($self, $info, $var) = @_; | |||
| 525 | 0 | return $self->http_note($var); | |||||
| 526 | } | ||||||
| 527 | sub http_log_header_out { | ||||||
| 528 | 0 | 0 | 1 | my ($self, $info, $var) = @_; | |||
| 529 | 0 | 0 | return join ', ', map {$_->[1]} grep {$_->[0] eq $var} @{ $info->{'response_headers'} || [] }; | ||||
| 0 | |||||||
| 0 | |||||||
| 0 | |||||||
| 530 | } | ||||||
| 531 | 0 | 0 | 0 | 1 | sub http_log_pid { $_[1]->{'pid'} || $$ } # we do not support tid yet | ||
| 532 | sub http_log_vhost { | ||||||
| 533 | 0 | 0 | 1 | my ($self, $info, $fmt, $f_l, $type) = @_; | |||
| 534 | 0 | 0 | return $self->http_log_header_in($info, 'Host') || $self->{'server'}->{'client'}->NS_host || $self->{'server'}->{'sockaddr'}; | ||||
| 535 | } | ||||||
| 536 | sub http_log_constat { | ||||||
| 537 | 0 | 0 | 1 | my ($self, $info) = @_; | |||
| 538 | 0 | 0 | return $info->{'headers_sent'} ? '-' : 'X'; | ||||
| 539 | } | ||||||
| 540 | |||||||
| 541 | ###----------------------------------------------------------------### | ||||||
| 542 | |||||||
| 543 | 0 | 1 | sub exec_fork_hook {} | ||||
| 544 | |||||||
| 545 | sub exec_trusted_perl { | ||||||
| 546 | 0 | 0 | 1 | my ($self, $file) = @_; | |||
| 547 | 0 | 0 | die "File $file is not executable\n" if ! -x $file; | ||||
| 548 | 0 | local $!; | |||||
| 549 | 0 | my $pid = fork; | |||||
| 550 | 0 | 0 | die "Could not spawn child process: $!\n" if ! defined $pid; | ||||
| 551 | 0 | $self->exec_fork_hook($pid, $file, 1); | |||||
| 552 | 0 | 0 | if (!$pid) { | ||||
| 553 | 0 | 0 | if (!eval { require $file }) { | ||||
| 0 | |||||||
| 554 | 0 | 0 | my $err = "$@" || "Error while running trusted perl script\n"; | ||||
| 555 | 0 | $err =~ s{\s*Compilation failed in require at lib/Net/Server/HTTP\.pm line \d+\.\s*\z}{\n}; | |||||
| 556 | 0 | 0 | die $err if !$self->{'request_info'}->{'headers_sent'}; | ||||
| 557 | 0 | warn $err; | |||||
| 558 | } | ||||||
| 559 | 0 | exit; | |||||
| 560 | } else { | ||||||
| 561 | 0 | waitpid $pid, 0; | |||||
| 562 | 0 | return; | |||||
| 563 | } | ||||||
| 564 | } | ||||||
| 565 | |||||||
| 566 | sub exec_cgi { | ||||||
| 567 | 0 | 0 | 1 | my ($self, $file) = @_; | |||
| 568 | |||||||
| 569 | 0 | my $done = 0; | |||||
| 570 | 0 | my $pid; | |||||
| 571 | Net::Server::SIG::register_sig(CHLD => sub { | ||||||
| 572 | 0 | 0 | while (defined(my $chld = waitpid(-1, POSIX::WNOHANG()))) { | ||||
| 573 | 0 | 0 | 0 | $done = ($? >> 8) || -1 if $pid == $chld; | |||
| 574 | 0 | 0 | last unless $chld > 0; | ||||
| 575 | } | ||||||
| 576 | 0 | }); | |||||
| 577 | |||||||
| 578 | 0 | require IPC::Open3; | |||||
| 579 | 0 | require Symbol; | |||||
| 580 | 0 | my $in; | |||||
| 581 | my $out; | ||||||
| 582 | 0 | my $err = Symbol::gensym(); | |||||
| 583 | 0 | local $!; | |||||
| 584 | 0 | 0 | $pid = eval { IPC::Open3::open3($in, $out, $err, $file) } or die "Could not run external script $file: $!\n"; | ||||
| 0 | |||||||
| 585 | 0 | $self->exec_fork_hook($pid, $file); # won't occur for the child | |||||
| 586 | 0 | 0 | my $len = $ENV{'CONTENT_LENGTH'} || 0; | ||||
| 587 | 0 | 0 | my $s_in = $len ? IO::Select->new($in) : undef; | ||||
| 588 | 0 | my $s_out = IO::Select->new($out, $err); | |||||
| 589 | 0 | my $printed; | |||||
| 590 | 0 | while (!$done) { | |||||
| 591 | 0 | my ($o, $i, $e) = IO::Select->select($s_out, $s_in, undef); | |||||
| 592 | 0 | Net::Server::SIG::check_sigs(); | |||||
| 593 | 0 | for my $fh (@$o) { | |||||
| 594 | 0 | 0 | read($fh, my $buf, 4096) || next; | ||||
| 595 | 0 | 0 | if ($fh == $out) { | ||||
| 596 | 0 | print $buf; | |||||
| 597 | 0 | 0 | $printed ||= 1; | ||||
| 598 | } else { | ||||||
| 599 | 0 | print STDERR $buf; | |||||
| 600 | } | ||||||
| 601 | } | ||||||
| 602 | 0 | 0 | if (@$i) { | ||||
| 603 | 0 | my $bytes = read(STDIN, my $buf, $len); | |||||
| 604 | 0 | 0 | print $in $buf if $bytes; | ||||
| 605 | 0 | $len -= $bytes; | |||||
| 606 | 0 | 0 | $s_in = undef if $len <= 0; | ||||
| 607 | } | ||||||
| 608 | } | ||||||
| 609 | 0 | 0 | if (!$self->{'request_info'}->{'headers_sent'}) { | ||||
| 610 | 0 | 0 | if (!$printed) { | ||||
| 0 | |||||||
| 611 | 0 | $self->send_500("Premature end of script headers"); | |||||
| 612 | } elsif ($done > 0) { | ||||||
| 613 | 0 | $self->send_500("Script exited unsuccessfully"); | |||||
| 614 | } | ||||||
| 615 | } | ||||||
| 616 | |||||||
| 617 | 0 | Net::Server::SIG::unregister_sig('CHLD'); | |||||
| 618 | } | ||||||
| 619 | |||||||
| 620 | 1; | ||||||
| 621 | |||||||
| 622 | __END__ |