File Coverage

blib/lib/Any/Daemon/HTTP.pm
Criterion Covered Total %
statement 62 253 24.5
branch 0 104 0.0
condition 9 79 11.3
subroutine 20 42 47.6
pod 13 16 81.2
total 104 494 21.0


line stmt bran cond sub pod time code
1             # Copyrights 2013-2020 by [Mark Overmeer].
2             # For other contributors see ChangeLog.
3             # See the manual pages for details on the licensing terms.
4             # Pod stripped from pm file by OODoc 2.02.
5             # This code is part of distribution Any-Daemon-HTTP. Meta-POD processed
6             # with OODoc into POD and HTML manual-pages. See README.md
7             # Copyright Mark Overmeer. Licensed under the same terms as Perl itself.
8              
9             package Any::Daemon::HTTP;
10 2     2   59036 use vars '$VERSION';
  2         12  
  2         105  
11             $VERSION = '0.30';
12              
13              
14 2     2   445 use Log::Report 'any-daemon-http';
  2         102822  
  2         11  
15 2     2   433 use parent 'Any::Daemon';
  2         4  
  2         12  
16              
17 2     2   11521 use warnings;
  2         4  
  2         43  
18 2     2   9 use strict;
  2         2  
  2         34  
19              
20 2     2   861 use Any::Daemon::HTTP::VirtualHost ();
  2         4  
  2         68  
21 2     2   904 use Any::Daemon::HTTP::Session ();
  2         5  
  2         51  
22 2     2   12 use Any::Daemon::HTTP::Proxy ();
  2         11  
  2         25  
23              
24 2     2   893 use HTTP::Daemon ();
  2         50971  
  2         61  
25 2     2   17 use HTTP::Status qw/:constants :is/;
  2         3  
  2         722  
26 2     2   13 use Socket qw/inet_aton PF_INET AF_INET/;
  2         3  
  2         91  
27 2     2   11 use IO::Socket qw/SOCK_STREAM SOMAXCONN SOL_SOCKET SO_LINGER/;
  2         4  
  2         13  
28 2     2   133 use IO::Socket::IP ();
  2         4  
  2         22  
29 2     2   471 use IO::Select ();
  2         1334  
  2         36  
30 2     2   12 use File::Basename qw/basename/;
  2         3  
  2         76  
31 2     2   11 use File::Spec ();
  2         3  
  2         29  
32 2     2   8 use Scalar::Util qw/blessed/;
  2         3  
  2         80  
33 2     2   11 use Errno qw/EADDRINUSE/;
  2         3  
  2         73  
34              
35             use constant # default ports
36 2         5231 { PORT_HTTP => 80
37             , PORT_HTTPS => 443
38 2     2   11 };
  2         3  
39              
40             # To support IPv6, replace ::INET by ::IP
41             @HTTP::Daemon::ClientConn::ISA = qw(IO::Socket::IP);
42              
43              
44 0 0   0   0 sub _to_list($) { ref $_[0] eq 'ARRAY' ? @{$_[0]} : defined $_[0] ? $_[0] : () }
  0 0       0  
45              
46             sub init($)
47 0     0 0 0 { my ($self, $args) = @_;
48 0         0 $self->SUPER::init($args);
49              
50 0   0     0 my $listen = $args->{listen} || $args->{socket} || $args->{host};
51 0         0 my (@sockets, @hosts);
52 0         0 foreach my $conn (_to_list $listen)
53             { my ($socket, @host) = $self->_create_socket($conn
54             , protocol => $args->{protocol}
55 0         0 );
56              
57 0 0       0 push @sockets, $socket if $socket;
58 0         0 push @hosts, @host;
59             }
60              
61 0 0       0 @sockets or error __x"host or socket required for {pkg}::new()"
62             , pkg => ref $self;
63              
64 0         0 $self->{ADH_sockets} = \@sockets;
65 0         0 $self->{ADH_hosts} = \@hosts;
66              
67             $self->{ADH_session_class}
68 0   0     0 = $args->{session_class} || 'Any::Daemon::HTTP::Session';
69             $self->{ADH_vhost_class}
70 0   0     0 = $args->{vhost_class} || 'Any::Daemon::HTTP::VirtualHost';
71             $self->{ADH_proxy_class}
72 0   0     0 = $args->{proxy_class} || 'Any::Daemon::HTTP::Proxy';
73              
74 0         0 $self->{ADH_vhosts} = {};
75 0   0     0 $self->addVirtualHost($_) for _to_list($args->{vhosts} || $args->{vhost});
76              
77 0         0 $self->{ADH_proxies} = [];
78 0   0     0 $self->addProxy($_) for _to_list($args->{proxies} || $args->{proxy});
79              
80             !$args->{docroot}
81 0 0       0 or error __x"docroot parameter has been removed in v0.11";
82              
83 0   0     0 $self->{ADH_server} = $args->{server_id} || basename($0);
84 0   0     0 $self->{ADH_headers} = $args->{standard_headers} || [];
85 0   0 0   0 $self->{ADH_error} = $args->{on_error} || sub { $_[1] };
  0         0  
86 0 0       0 $self->{ADH_show_ps} = exists $args->{show_in_ps} ? $args->{show_in_ps} : 1;
87              
88             # "handlers" is probably a common typo
89 0   0     0 my $handler = $args->{handlers} || $args->{handler};
90              
91 0         0 my $host = shift @hosts;
92             $self->addVirtualHost
93             ( name => $host
94             , aliases => [@hosts, 'default']
95             , documents => $args->{documents}
96             , handler => $handler
97 0 0 0     0 ) if $args->{documents} || $handler;
98              
99 0         0 $self;
100             }
101              
102             sub _create_socket($%)
103 0     0   0 { my ($self, $listen, %args) = @_;
104 0 0       0 defined $listen or return;
105              
106 0 0 0     0 return ($listen, $listen->sockhost.':'.$listen->sockport)
107             if blessed $listen && $listen->isa('IO::Socket');
108              
109 0 0       0 my $port = $listen =~ s/\:([0-9]+)$// ? $1 : PORT_HTTP;
110 0         0 my $host = $listen;
111             my $proto = $self->{ADH_protocol}
112 0   0     0 = $args{protocol} || ($port==PORT_HTTPS ? 'HTTPS' : 'HTTP');
113              
114 0         0 my $sock_class;
115 0 0       0 if($proto eq 'HTTPS')
    0          
    0          
116 0         0 { $sock_class = 'IO::Socket::SSL';
117 0 0       0 eval "require IO::Socket::SSL; require HTTP::Daemon::SSL" or panic $@;
118             }
119             elsif($proto eq 'HTTP')
120 0         0 { $sock_class = 'IO::Socket::IP';
121             }
122             elsif($proto eq 'FCGI')
123 0         0 { $sock_class = 'IO::Socket::IP';
124 0 0       0 eval "require Any::Daemon::FCGI" or panic $@;
125             }
126             else
127 0         0 { error __x"Unsupported protocol '{proto}'", proto => $proto;
128             }
129              
130             # Wait max 60 seconds to get the socket
131             # You should be able to reduce the time to wait by setting linger
132             # on the socket in the process which has opened the socket before.
133 0         0 my ($socket, $elapse);
134 0         0 foreach my $retry (1..60)
135 0         0 { $elapse = $retry -1;
136              
137 0         0 $socket = $sock_class->new
138             ( LocalHost => $host
139             , LocalPort => $port
140             , Listen => SOMAXCONN
141             , Reuse => 1
142             , Type => SOCK_STREAM
143             , Proto => 'tcp'
144             );
145              
146 0 0 0     0 last if $socket || $! != EADDRINUSE;
147              
148 0 0       0 notice __x"waiting for socket at {address} to become available"
149             , address => "$host:$port"
150             if $retry==1;
151              
152 0         0 sleep 1;
153             }
154              
155             $socket
156 0 0       0 or fault __x"cannot create socket at {address}"
157             , address => "$host:$port";
158              
159 0 0       0 notice __x"got socket after {secs} seconds", secs => $elapse
160             if $elapse;
161              
162 0         0 ($socket, "$listen:$port", $socket->sockhost.':'.$socket->sockport);
163             }
164              
165             #----------------
166              
167 0     0 1 0 sub sockets() { @{shift->{ADH_sockets}} }
  0         0  
168 0     0 1 0 sub hosts() { @{shift->{ADH_hosts}} }
  0         0  
169 0     0 1 0 sub protocol() { shift->{ADH_protocol} }
170              
171             #-------------
172              
173             sub addVirtualHost(@)
174 0     0 1 0 { my $self = shift;
175 0 0       0 my $config = @_ > 1 ? +{@_} : !defined $_[0] ? return : shift;
    0          
176              
177 0         0 my $vhost;
178 0 0 0     0 if(blessed $config && $config->isa('Any::Daemon::HTTP::VirtualHost'))
    0          
179 0         0 { $vhost = $config }
180             elsif(ref $config eq 'HASH')
181 0         0 { $vhost = $self->{ADH_vhost_class}->new($config) }
182 0         0 else { error __x"virtual host configuration not a valid object nor HASH" }
183              
184 0         0 info __x"adding virtual host {name}", name => $vhost->name;
185              
186             $self->{ADH_vhosts}{$_} = $vhost
187 0         0 for $vhost->name, $vhost->aliases;
188              
189 0         0 $vhost;
190             }
191              
192              
193             sub addProxy(@)
194 0     0 1 0 { my $self = shift;
195 0 0       0 my $config = @_ > 1 ? +{@_} : !defined $_[0] ? return : shift;
    0          
196 0         0 my $proxy;
197 0 0       0 if(UNIVERSAL::isa($config, 'Any::Daemon::HTTP::Proxy'))
    0          
198 0         0 { $proxy = $config }
199             elsif(UNIVERSAL::isa($config, 'HASH'))
200 0         0 { $proxy = $self->{ADH_proxy_class}->new($config) }
201 0         0 else { error __x"proxy configuration not a valid object nor HASH" }
202              
203 0 0       0 $proxy->forwardMap
204             or error __x"proxy {name} has no map, so needs inside vhost"
205             , name => $proxy->name;
206              
207 0         0 info __x"adding proxy {name}", name => $proxy->name;
208              
209 0         0 push @{$self->{ADH_proxies}}, $proxy;
  0         0  
210             }
211              
212              
213             sub removeVirtualHost($)
214 0     0 1 0 { my ($self, $id) = @_;
215 0 0 0     0 my $vhost = blessed $id && $id->isa('Any::Daemon::HTTP::VirtualHost')
216             ? $id : $self->virtualHost($id);
217 0 0       0 defined $vhost or return;
218              
219             delete $self->{ADH_vhosts}{$_}
220 0         0 for $vhost->name, $vhost->aliases;
221 0         0 $vhost;
222             }
223              
224              
225 0     0 1 0 sub virtualHost($) { $_[0]->{ADH_vhosts}{$_[1]} }
226              
227              
228 0     0 1 0 sub proxies() { @{shift->{ADH_proxies}} }
  0         0  
229              
230              
231             sub findProxy($$$)
232 0     0 1 0 { my ($self, $session, $req, $host) = @_;
233 0         0 my $uri = $req->uri->abs("http://$host");
234 0         0 foreach my $proxy ($self->proxies)
235 0 0       0 { my $mapped = $proxy->forwardRewrite($session, $req, $uri) or next;
236 0         0 return ($proxy, $mapped);
237             }
238              
239 0         0 ();
240             }
241              
242             #-------------------
243              
244             sub _connection($$)
245 0     0   0 { my ($self, $client, $args) = @_;
246              
247 0         0 my $nr_req = 0;
248 0   0     0 my $max_req = $args->{max_req_per_conn} ||= 100;
249 0         0 my $start = time;
250 0   0     0 my $deadline = $start + ($args->{max_time_per_conn} ||= 120);
251 0   0     0 my $bonus = $args->{req_time_bonus} //= 2;
252              
253 0         0 my $conn;
254 0 0       0 if($self->protocol eq 'FCGI')
255 0         0 { $args->{socket} = $client;
256 0         0 $conn = Any::Daemon::FCGI::ClientConn->new($args);
257             }
258             else
259             { # Ugly hack, steal HTTP::Daemon's HTTP/1.1 implementation
260 0 0       0 $conn = bless $client, $client->isa('IO::Socket::SSL')
261             ? 'HTTP::Daemon::ClientConn::SSL'
262             : 'HTTP::Daemon::ClientConn';
263              
264 0         0 ${*$conn}{httpd_daemon} = $self;
  0         0  
265             }
266              
267 0         0 my $ip = $client->peerhost;
268 0   0     0 my $host =
269             ( $client->sockdomain == PF_INET
270             ? gethostbyaddr inet_aton($ip), AF_INET
271             : undef
272             ) || $ip;
273              
274 0         0 my $session = $self->{ADH_session_class}->new;
275 0         0 $session->set(peer => { ip => $ip, host => $host });
276 0         0 info __x"new client from {host} on {ip}" , host => $host, ip => $ip;
277              
278 0         0 my $init_conn = $args->{new_connection};
279 0         0 $self->$init_conn($session);
280              
281             # Change title in ps-table
282 0 0       0 my $title = $0 =~ /^(\S+)/ ? basename($1) : $0;
283 0         0 $self->psTitle("$title http from $host");
284              
285             $SIG{ALRM} = sub {
286 0     0   0 notice __x"connection from {host} lasted too long, killed after {time%d} seconds"
287             , host => $host, time => $deadline - $start;
288 0         0 exit 0;
289 0         0 };
290              
291 0         0 alarm $deadline - time;
292 0         0 while(my $req = $conn->get_request)
293 0   0     0 { my $vhostn = $req->header('Host') || 'default';
294 0         0 my $vhost = $self->virtualHost($vhostn);
295              
296 0         0 $self->_clean_uri($req->uri);
297              
298             # Fallback to vhost without specific port number
299 0 0 0     0 $vhost ||= $self->virtualHost($1)
300             if $vhostn =~ /(.*)\:[0-9]+$/;
301              
302 0         0 my $resp;
303 0 0       0 if($vhost)
    0          
    0          
304             { $self->{ADH_host_base}
305 0 0       0 = (ref($conn) =~ /SSL/ ? 'https' : 'http').'://'.$vhost->name;
306 0         0 $resp = $vhost->handleRequest($self, $session, $req);
307             }
308             elsif(my ($proxy, $where) = $self->findProxy($session, $req, $vhostn))
309 0         0 { $resp = $proxy->forwardRequest($session, $req, $where);
310             }
311             elsif(my $default = $self->virtualHost('default'))
312 0         0 { $resp = HTTP::Response->new(HTTP_TEMPORARY_REDIRECT);
313 0         0 $resp->header(Location => 'http://'.$default->name);
314             }
315             else
316 0         0 { $resp = HTTP::Response->new(HTTP_NOT_ACCEPTABLE,
317             "virtual host $vhostn is not available");
318             }
319              
320 0 0       0 unless($resp)
321 0         0 { notice __x"no response produced for {uri}", uri => $req->uri;
322 0         0 $resp = HTTP::Response->new(HTTP_SERVICE_UNAVAILABLE);
323             }
324              
325 0         0 $resp->push_header(@{$self->{ADH_headers}});
  0         0  
326 0         0 $resp->request($req);
327              
328             # No content, then produce something better than an empty page.
329 0 0       0 if(is_error($resp->code))
330 0         0 { $resp = $self->{ADH_error}->($self, $resp, $session, $req);
331 0 0       0 $resp->content or $resp->content($resp->status_line);
332             }
333 0         0 $deadline += $bonus;
334 0         0 alarm $deadline - time;
335              
336 0         0 my $close = $nr_req++ >= $max_req;
337              
338 0 0       0 $resp->header(Connection => ($close ? 'close' : 'open'));
339 0         0 $conn->send_response($resp);
340              
341 0 0       0 last if $close;
342             }
343              
344 0         0 alarm 0;
345 0         0 $nr_req;
346             }
347              
348             sub _clean_uri($)
349 6     6   9441 { my ($self, $uri) = @_;
350 6         15 my $path = $uri->path;
351              
352 6         110 for($path)
353 6   100     140 { 1 while s!/[^/.]+/+\.\.(/|\z)!/$1!
      100        
      100        
354             || s!^/\.\.(/|\z)!/$1!
355             || s!/{2,}!/! # //
356             || s!/\.(/|\z)!/!; # /./
357             }
358              
359 6         16 $uri->path($path);
360             }
361              
362             sub run(%)
363 0     0 1   { my ($self, %args) = @_;
364              
365 0   0       my $new_child = $args{new_child} || 'newChild';
366 0   0       $args{new_connection} ||= 'newConnection';
367              
368 0           my $vhosts = $self->{ADH_vhosts};
369 0 0         unless(keys %$vhosts)
370 0           { my ($host, @aliases) = $self->hosts;
371 0           $self->addVirtualHost(name => $host, aliases => ['default', @aliases]);
372             }
373              
374             # option handle_request is deprecated in 0.11
375 0 0         if(my $handler = delete $args{handle_request})
376 0           { my (undef, $first) = %$vhosts;
377 0           $first->addHandler('/' => $handler);
378             }
379              
380 0 0         my $title = $0 =~ /^(\S+)/ ? basename($1) : $0;
381              
382 0           my ($req_count, $conn_count) = (0, 0);
383 0   0       my $max_conn = $args{max_conn_per_child} || 10_000;
384 0 0         $max_conn = int(0.9 * $max_conn + rand(0.2 * $max_conn))
385             if $max_conn > 10;
386              
387 0   0       my $max_req = $args{max_req_per_child} || 100_000;
388 0           my $linger = $args{linger};
389              
390 0           $self->psTitle("$title manager\x00\x00");
391             $args{child_task} ||= sub {
392 0     0     $self->psTitle("$title not used yet");
393             # even with one port, we still select...
394 0           my $select = IO::Select->new($self->sockets);
395              
396 0           $self->$new_child($select);
397              
398             CONNECTION:
399 0           while(my @ready = $select->can_read)
400             {
401 0           foreach my $socket (@ready)
402 0 0         { my $client = $socket->accept or next;
403 0 0         $client->sockopt(SO_LINGER, (pack "II", 1, $linger))
404             if defined $linger;
405              
406 0           $self->psTitle("$title handling "
407             . $client->peerhost.":".$client->peerport . " at "
408             . $client->sockhost.':'.$client->sockport);
409              
410 0           $req_count += $self->_connection($client, \%args);
411 0           $client->close;
412              
413             last CONNECTION
414 0 0 0       if $conn_count++ >= $max_conn
415             || $req_count >= $max_req;
416             }
417 0           $self->psTitle("$title idle after $conn_count");
418             }
419 0           0;
420 0   0       };
421              
422 0           info __x"start running the webserver";
423 0           $self->SUPER::run(%args);
424             }
425              
426              
427             sub newConnection($)
428 0     0 1   { my ($self, $session) = @_;
429 0           return $self;
430             }
431              
432              
433             sub newChild($)
434 0     0 1   { my ($self, $select) = @_;
435 0           return $self;
436             }
437              
438              
439             sub psTitle($)
440 0     0 1   { my ($self, $string) = @_;
441 0 0         $0 = $string if $self->{ADH_show_ps};
442             }
443              
444             # HTTP::Daemon methods used by ::ClientConn. We steal that parent role,
445             # but need to mimic the object a little. The names are not compatible
446             # with MarkOv's convention, so hidden for the users of this module
447 0     0 0   sub url() { shift->{ADH_host_base} }
448 0     0 0   sub product_tokens() {shift->{ADH_server}}
449              
450             1;
451              
452             __END__