File Coverage

blib/lib/Any/Daemon/HTTP.pm
Criterion Covered Total %
statement 54 234 23.0
branch 0 94 0.0
condition 0 69 0.0
subroutine 18 38 47.3
pod 11 14 78.5
total 83 449 18.4


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