File Coverage

blib/lib/Any/Daemon/HTTP/VirtualHost.pm
Criterion Covered Total %
statement 39 197 19.8
branch 0 118 0.0
condition 0 48 0.0
subroutine 13 41 31.7
pod 15 16 93.7
total 67 420 15.9


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::VirtualHost;
10 1     1   5 use vars '$VERSION';
  1         2  
  1         42  
11             $VERSION = '0.29';
12              
13              
14 1     1   5 use warnings;
  1         2  
  1         20  
15 1     1   5 use strict;
  1         2  
  1         17  
16              
17 1     1   4 use Log::Report 'any-daemon-http';
  1         1  
  1         5  
18              
19 1     1   598 use Any::Daemon::HTTP::Directory;
  1         22  
  1         67  
20 1     1   453 use Any::Daemon::HTTP::UserDirs;
  1         2  
  1         28  
21 1     1   388 use Any::Daemon::HTTP::Proxy;
  1         2  
  1         34  
22              
23 1     1   6 use HTTP::Status qw/:constants/;
  1         2  
  1         319  
24 1     1   6 use List::Util qw/first/;
  1         2  
  1         55  
25 1     1   5 use File::Spec ();
  1         2  
  1         16  
26 1     1   4 use POSIX::1003 qw(strftime);
  1         2  
  1         9  
27 1     1   265 use Scalar::Util qw(blessed);
  1         2  
  1         35  
28 1     1   4 use Digest::MD5 qw(md5_base64);
  1         2  
  1         2407  
29              
30              
31             sub new(@)
32 0     0 1   { my $class = shift;
33 0 0         my $args = @_==1 ? shift : {@_};
34 0           (bless {}, $class)->init($args);
35             }
36              
37             sub init($)
38 0     0 0   { my ($self, $args) = @_;
39              
40 0           my $name = $self->{ADHV_name} = $args->{name};
41 0 0         defined $name
42             or error __x"virtual host {pkg} has no name", pkg => ref $self;
43              
44 0   0       my $aliases = $args->{aliases} || 'AUTO';
45             $self->{ADHV_aliases}
46 0 0         = ref $aliases eq 'ARRAY' ? $aliases
    0          
    0          
47             : $aliases eq 'AUTO' ? [ $self->generateAliases($name) ]
48             : defined $aliases ? [ $aliases ]
49             : [];
50              
51 0   0       $self->addHandler($args->{handlers} || $args->{handler});
52              
53 0           $self->{ADHV_rewrite} = $self->_rewrite_call($args->{rewrite});
54 0           $self->{ADHV_redirect} = $self->_redirect_call($args->{redirect});
55 0           $self->{ADHV_udirs} = $self->_user_dirs($args->{user_dirs});
56              
57 0           $self->{ADHV_sources} = {};
58 0           $self->_auto_docs($args->{documents});
59 0   0       my $dirs = $args->{directories} || $args->{directory} || [];
60 0 0         $self->addDirectory($_) for ref $dirs eq 'ARRAY' ? @$dirs : $dirs;
61              
62 0           $self->{ADHV_proxies} = {};
63 0   0       my $proxies = $args->{proxies} || $args->{proxy} || [];
64 0 0         $self->addProxy($_) for ref $proxies eq 'ARRAY' ? @$proxies : $proxies;
65              
66 0           $self;
67             }
68              
69             sub _user_dirs($)
70 0     0     { my ($self, $dirs) = @_;
71 0 0         $dirs or return undef;
72              
73 0 0         return Any::Daemon::HTTP::UserDirs->new($dirs)
74             if ref $dirs eq 'HASH';
75              
76 0 0         return $dirs
77             if $dirs->isa('Any::Daemon::HTTP::UserDirs');
78              
79 0           error __x"vhost {name} user_dirs is not an ::UserDirs object"
80             , name => $self->name;
81             }
82              
83             sub _auto_docs($)
84 0     0     { my ($self, $docroot) = @_;
85 0 0         $docroot or return;
86              
87 0 0         File::Spec->file_name_is_absolute($docroot)
88             or error __x"vhost {name} documents directory must be absolute"
89             , name => $self->name;
90              
91 0 0         -d $docroot
92             or error __x"vhost {name} documents `{dir}' must point to dir"
93             , name => $self->name, dir => $docroot;
94              
95 0           $docroot =~ s/\\$//; # strip trailing / if present
96 0           $self->addDirectory(path => '/', location => $docroot);
97             }
98              
99             #---------------------
100              
101 0     0 1   sub name() {shift->{ADHV_name}}
102 0     0 1   sub aliases() {@{shift->{ADHV_aliases}}}
  0            
103              
104              
105             sub generateAliases($)
106 0     0 1   { my ($thing, $h) = @_;
107 0           my @a;
108 0           $h =~ m/^(([^.:]+)(?:[^:]*)?)(?:\:([0-9]+))?$/;
109 0 0         push @a, $1 if $3; # name with port
110 0 0         push @a, $2 if $1 ne $2; # hostname vs fqdn
111 0 0 0       push @a, "$2:$3" if $1 ne $2 && $3; # hostname with port
112 0           @a;
113             }
114              
115             #---------------------
116              
117             sub addHandler(@)
118 0     0 1   { my $self = shift;
119 0 0 0       return if @_==1 && !defined $_[0];
120              
121             my @pairs
122             = @_ > 1 ? @_
123 0 0         : ref $_[0] eq 'HASH' ? %{$_[0]}
  0 0          
124             : ( '/' => $_[0]);
125            
126 0   0       my $h = $self->{ADHV_handlers} ||= {};
127 0           while(@pairs)
128 0           { my $k = shift @pairs;
129 0 0         substr($k, 0, 1) eq '/'
130             or error __x"handler path must be absolute, for {rel} in {vhost}"
131             , rel => $k, vhost => $self->name;
132              
133 0           my $v = shift @pairs;
134 0 0         unless(ref $v)
135 0           { my $method = $v;
136 0 0         $self->can($method)
137             or error __x"handler method {name} not provided by {vhost}"
138             , name => $method, vhost => ref $self;
139 0     0     $v = sub { shift->$method(@_) };
  0            
140             }
141              
142 0           $h->{$k} = $v;
143             }
144 0           $h;
145             }
146              
147              
148             *addHandlers = \&addHandler;
149              
150              
151             sub findHandler(@)
152 0     0 1   { my $self = shift;
153 0 0         my @path = @_>1 ? @_ : ref $_[0] ? $_[0]->path_segments : split('/', $_[0]);
    0          
154              
155 0   0       my $h = $self->{ADHV_handlers} ||= {};
156 0           while(@path)
157 0           { my $handler = $h->{join '/', @path};
158 0 0         return $handler if $handler;
159 0           pop @path;
160             }
161              
162 0 0         if(my $handler = $h->{'/'})
163 0           { return $handler;
164             }
165              
166 0     0     sub { HTTP::Response->new(HTTP_NOT_FOUND) };
  0            
167             }
168              
169              
170             sub handleRequest($$$;$)
171 0     0 1   { my ($self, $server, $session, $req, $uri) = @_;
172 0   0       $uri ||= $req->uri;
173 0           info __x"{host} request {uri}", host => $self->name, uri => $uri->as_string;
174              
175 0           my $new_uri = $self->rewrite($uri);
176 0 0         if($new_uri ne $uri)
177 0           { info __x"{vhost} rewrote {uri} into {new}", vhost => $self->name
178             , uri => $uri->as_string, new => $new_uri->as_string;
179 0           $uri = $new_uri;
180             }
181              
182 0 0         if(my $redir = $self->mustRedirect($new_uri))
183 0           { return $redir;
184             }
185              
186 0           my $path = $uri->path;
187              
188 0           my @path = $uri->path_segments;
189 0           my $source = $self->sourceFor(@path);
190              
191             # static content?
192 0 0         my $resp = $source ? $source->collect($self, $session, $req,$uri) : undef;
193 0 0         return $resp if $resp;
194              
195             # dynamic content
196 0           $resp = $self->findHandler(@path)->($self, $session, $req, $uri, $source);
197 0 0         $resp or return HTTP::Response->new(HTTP_NO_CONTENT);
198              
199 0 0 0       blessed $resp && $resp->isa('HTTP::Response')
200             or error __x"Handler for {uri} does not return an HTTP::Response",
201             uri => $uri->as_string;
202              
203 0 0         $resp->code eq HTTP_OK
204             or return $resp;
205              
206             # cache dynamic content based on md5 checksum
207 0           my $etag = md5_base64 ${$resp->content_ref};
  0            
208 0           my $has_etag = $req->headers->header('ETag');
209 0 0 0       return HTTP::Response->new(HTTP_NOT_MODIFIED, 'cached dynamic data')
210             if $has_etag && $has_etag eq $etag;
211              
212 0           $resp->headers->header(ETag => $etag);
213 0           $resp;
214             }
215              
216             #----------------------
217              
218 0     0 1   sub rewrite($) { $_[0]->{ADHV_rewrite}->(@_) }
219              
220             sub _rewrite_call($)
221 0     0     { my ($self, $rew) = @_;
222 0 0   0     $rew or return sub { $_[1] };
  0            
223 0 0         return $rew if ref $rew eq 'CODE';
224              
225 0 0         if(ref $rew eq 'HASH')
226 0           { my %lookup = %$rew;
227             return sub {
228 0 0   0     my $uri = $_[1] or return undef;
229 0 0         exists $lookup{$uri->path} or return $uri;
230 0           URI->new_abs($lookup{$uri->path}, $uri)
231 0           };
232             }
233              
234 0 0         if(!ref $rew)
235 0     0     { return sub {shift->$rew(@_)}
236 0 0         if $self->can($rew);
237              
238 0           error __x"rewrite rule method {name} in {vhost} does not exist"
239             , name => $rew, vhost => $self->name;
240             }
241              
242 0   0       error __x"unknown rewrite rule type {ref} in {vhost}"
243             , ref => (ref $rew || $rew), vhost => $self->name;
244             }
245              
246              
247             sub redirect($;$)
248 0     0 1   { my ($self, $uri, $code) = @_;
249 0   0       HTTP::Response->new($code//HTTP_TEMPORARY_REDIRECT, undef
250             , [ Location => "$uri" ]
251             );
252             }
253              
254              
255             sub mustRedirect($)
256 0     0 1   { my ($self, $uri) = @_;
257 0           my $new_uri = $self->{ADHV_redirect}->($self, $uri);
258 0 0 0       $new_uri && $new_uri ne $uri or return;
259              
260 0           info __x"{vhost} redirecting {uri} to {new}"
261             , vhost => $self->name, uri => $uri->path, new => "$new_uri";
262              
263 0           $self->redirect($new_uri);
264             }
265              
266             sub _redirect_call($)
267 0     0     { my ($self, $red) = @_;
268 0 0   0     $red or return sub { $_[1] };
  0            
269 0 0         return $red if ref $red eq 'CODE';
270              
271 0 0         if(ref $red eq 'HASH')
272 0           { my %lookup = %$red;
273             return sub {
274 0 0   0     my $uri = $_[1] or return undef;
275 0 0         exists $lookup{$uri->path} or return undef;
276 0           URI->new_abs($lookup{$uri->path}, $uri);
277 0           };
278             }
279              
280 0 0         if(!ref $red)
281 0     0     { return sub {shift->$red(@_)}
282 0 0         if $self->can($red);
283              
284 0           error __x"redirect rule method {name} in {vhost} does not exist"
285             , name => $red, vhost => $self->name;
286             }
287              
288 0   0       error __x"unknown redirect rule type {ref} in {vhost}"
289             , ref => (ref $red || $red), vhost => $self->name;
290             }
291              
292              
293             sub addSource($)
294 0     0 1   { my ($self, $source) = @_;
295 0 0         $source or return;
296              
297 0           my $sources = $self->{ADHV_sources};
298 0           my $path = $source->path;
299              
300 0 0         if(my $old = exists $sources->{$path})
301 0           { error __x"vhost {name} directory `{path}' defined twice, for `{old}' and `{new}' "
302             , name => $self->name, path => $path
303             , old => $old->name, new => $source->name;
304             }
305              
306 0           info __x"add configuration `{name}' to {vhost} for {path}"
307             , name => $source->name, vhost => $self->name, path => $path;
308              
309 0           $sources->{$path} = $source;
310             }
311              
312             #------------------
313              
314             sub filename($)
315 0     0 1   { my ($self, $uri) = @_;
316 0           my $dir = $self->sourceFor($uri);
317 0 0         $dir ? $dir->filename($uri->path) : undef;
318             }
319              
320              
321             sub addDirectory(@)
322 0     0 1   { my $self = shift;
323 0 0 0       my $dir = @_==1 && blessed $_[0] ? shift
324             : Any::Daemon::HTTP::Directory->new(@_);
325              
326 0           $self->addSource($dir);
327             }
328              
329              
330             sub sourceFor(@)
331 0     0 1   { my $self = shift;
332 0 0 0       my @path = @_>1 || index($_[0], '/')==-1 ? @_ : split('/', $_[0]);
333              
334             return $self->{ADHV_udirs}
335 0 0         if substr($path[0], 0, 1) eq '~';
336              
337 0           my $sources = $self->{ADHV_sources};
338 0           while(@path)
339 0           { my $dir = $sources->{join '/', @path};
340 0 0         return $dir if $dir;
341 0           pop @path;
342             }
343              
344             # return empty list, not undef, when not found
345 0 0         $sources->{'/'} ? $sources->{'/'} : ();
346             }
347              
348             #-----------------------------
349              
350             sub addProxy(@)
351 0     0 1   { my $self = shift;
352 0 0 0       my $proxy = @_==1 && blessed $_[0] ? shift
353             : Any::Daemon::HTTP::Proxy->new(@_);
354              
355 0 0         error __x"proxy {name} has a map, so cannot be added to a vhost"
356             , name => $proxy->name
357             if $proxy->forwardMap;
358              
359 0           info __x"add proxy configuration to {vhost} for {path}"
360             , vhost => $self->name, path => $proxy->path;
361              
362 0           $self->addSource($proxy);
363             }
364              
365             #-----------------------------
366              
367              
368             1;