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