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__ |