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