File Coverage

blib/lib/Respite/Server.pm
Criterion Covered Total %
statement 104 470 22.1
branch 39 352 11.0
condition 19 189 10.0
subroutine 23 76 30.2
pod 11 42 26.1
total 196 1129 17.3


line stmt bran cond sub pod time code
1             package Respite::Server;
2              
3             # Respite::Server - generic Respite based Respite server
4              
5 4     4   1546 use strict;
  4         8  
  4         133  
6 4     4   10 use warnings;
  4         9  
  4         298  
7             our @ISA;
8 4     4   19 use base 'Respite::Common'; # Default _configs
  4         5  
  4         571  
9 4     4   21 use Digest::MD5 qw(md5_hex);
  4         6  
  4         360  
10 4     4   17 use Throw qw(throw);
  4         5  
  4         33  
11 4     4   183 use Time::HiRes qw(sleep);
  4         6  
  4         30  
12              
13 18 0 33 18 1 224 sub server_name { $_[0]->{'server_name'} ||= ($0 =~ m|/(\w+)$|x) ? $1 : throw 'Missing server_name' }
14 0   0 0 0 0 sub revision { $_[0]->{'revision'} ||= eval { $_[0]->dispatch_class->_revision } || '-' }
      0        
15 0 0   0 0 0 sub max_request_size { $_[0]->{'max_request_size'} || 2_000_000 }
16 5     5 1 20 sub api_meta { shift->{'api_meta'} }
17 0     0 0 0 sub dispatch_class { shift->{'dispatch_class'} }
18              
19             ###----------------------------------------------------------------###
20              
21             sub new {
22 5     5 1 29 my $class = shift;
23 5 50       82 my $self = bless ref($_[0]) ? shift : {@_}, $class;
24 5 50       67 %$self = (%$_, %$self) if $_ = $self->new_args;
25 5 50 33     51 return $self if $self->{'non_daemon'} || ($ENV{'MOD_PERL'} && ! $self->{'force_daemon'});
      33        
26 5         2420 require Net::Server;
27 5         86436 require Net::Server::HTTP;
28 5 100       22401 unshift @ISA, qw(Net::Server::HTTP) if !$self->isa(qw(Net::Server::HTTP));;
29 5 50       51 throw 'We need a more recent Net::Server revision', {v => $Net::Server::VERSION} if $Net::Server::VERSION < 2.007;
30 5         78 $self->json; # vivify before fork
31 5         100 my $server = $class->SUPER::new(%$self, %{ $self->server_args });
  5         55  
32 5         301 @$server{keys %$self} = values %$self; # TODO - avoid duplicates
33 5 50       35 $self->dispatch_factory('preload') if !$ENV{'NO_PRELOAD'}; # void call will load necessary classes
34              
35 5         50 return $server;
36             }
37              
38       5 0   sub new_args {}
39              
40             sub config {
41 60     60 0 91 my ($self, $key, $def, $name) = @_;
42 60   33     85 $name ||= $self->server_name;
43 60         128 my $c = $self->_configs($name);
44             return exists($self->{$key}) ? $self->{$key}
45             : exists($c->{"${name}_${key}"}) ? $c->{"${name}_${key}"}
46 60 50 33     418 : (ref($c->{$name}) && exists $c->{$name}->{$key}) ? $c->{$name}->{$key}
    50          
    50          
    100          
47             : ref($def) eq 'CODE' ? $def->($self) : $def;
48             }
49              
50             sub dispatch_factory {
51 5     5 0 21 my ($self, $preload) = @_;
52 5   33     46 return $self->{'dispatch_factory'} ||= do {
53 5   33     46 my $meta = $self->api_meta || $self->dispatch_class || throw "Missing one of api_meta or dispatch_class";
54 5 50       16 if (!ref $meta) {
55 5         17 (my $file = "$meta.pm") =~ s|::|/|g;
56 5 0 33     74 throw "Failed to load dispatch class", {class => $meta, file => $file, msg => $@} if !$meta->can('new') && !eval { require $file };
  0         0  
57 5 50       45 throw "Specified class does not have a run_method method", {class => $meta} if ! $meta->can('run_method');
58 5     0   72 sub { $meta->new(@_) };
  0         0  
59             } else {
60 0         0 require Respite::Base;
61 0 0       0 Respite::Base->new({api_meta => $meta})->api_preload if $preload;
62 0     0   0 sub { Respite::Base->new({%{shift()}, api_meta => $meta}) };
  0         0  
  0         0  
63             }
64             };
65             }
66              
67             ###----------------------------------------------------------------###
68             # request handling and method dispatching
69              
70             # mod_perl handler - used via apache conf
71             #
72             # SetHandler modperl
73             # PerlResponseHandler FooServer
74             #
75             # sub handler { __PACKAGE__->modperlhandler(@_) }
76             sub modperlhandler {
77 0     0 0 0 my $class = shift;
78 0   0     0 my $r = shift || throw "Missing apache request during ${class}::modperlhandler", {trace => 1};
79 0         0 my $self = $class->new({apache_req => $r, non_daemon => 1});
80 0         0 my %env = %ENV;
81 0 0       0 if (eval { $self->modperl_init($r); $r->subprocess_env(); 1 }) {
  0         0  
  0         0  
  0         0  
82 0         0 $self->cgihandler();
83             } else {
84 0         0 warn my $err = $self->json->encode({error => "$@", type => 'mod_perl_header'});
85 0         0 $self->send_response($err);
86             }
87 0         0 %ENV = %env;
88 0         0 return 0; # OK - TODO - we actually may want a 403 for digest errors
89             }
90              
91             my $modperl_init;
92             sub modperl_init {
93 0 0   0 0 0 return if $modperl_init;
94 0         0 $modperl_init = 1;
95 0         0 require Apache2::RequestRec;
96 0         0 require Apache2::RequestIO;
97 0         0 require APR::Table;
98             }
99              
100             # normal cgi-bin or Net::Server::HTTP handler
101             # Net::Server::HTTP app => \&cgihandler
102             # cgi-bin/server App::cgihandler() or App->new->cgihandler or App->cgihandler
103             sub cgihandler {
104 0     0 0 0 my $self = shift;
105 0 0 0     0 $self = ($self || __PACKAGE__)->new({%{shift() || {}}, non_daemon => 1}) if ! $self || ! ref($self);
  0 0 0     0  
106 0         0 local $self->{'transport'};
107 0         0 local $self->{'extra_headers'};
108 0         0 local $self->{'cgi_obj'};
109              
110 0         0 my $req_sum;
111 0         0 my $args = eval {
112 0         0 my $r = $self->{'apache_req'};
113 0         0 my $req;
114 0 0 0     0 if ($ENV{'CONTENT_TYPE'} && $ENV{'CONTENT_TYPE'} =~ /\bjson\b/) {
115 0 0 0     0 throw 'JSON data may not be submitted via GET' if !$ENV{'REQUEST_METHOD'} || $ENV{'REQUEST_METHOD'} eq 'GET' || $ENV{'REQUEST_METHOD'} eq 'HEAD';
      0        
116 0   0     0 my $len = $ENV{'CONTENT_LENGTH'} || throw "Missing CONTENT_LENGTH on $ENV{'REQUEST_METHOD'} request", {len => $ENV{'CONTENT_LENGTH'}};
117 0 0       0 throw "Too large a $ENV{'REQUEST_METHOD'} request found", {length => $len, max => $self->max_request_size} if $len > $self->max_request_size;
118 0         0 my $size = 0;
119 0         0 while (1) {
120 0 0       0 $r ? $r->read($req, $len - $size, $size) : read(STDIN, $req, $len - $size, $size);
121 0 0       0 throw "Failed to read bytes", {needed => $len, got => $size} if length($req) == $size;
122 0 0       0 last if ($size = length $req) >= $len;
123             }
124 0 0       0 throw "Failed to read entire $ENV{'REQUEST_METHOD'} request", {length => $len, actual => length($req)} if length($req) != $len;
125             } else {
126 0         0 my $args = $self->parse_form($r);
127 0         0 $req = delete $args->{'POSTDATA'}; # CGI.pm - non-form POST
128 0 0       0 if (!$req) { # get
129 0         0 $self->{'transport'} = 'form';
130 0 0 0     0 $args = Data::URIEncode::flat_to_complex($args) || {} if !$self->{'no_data_uriencode'} && (eval { require Data::URIEncode } || ((grep {$_ =~ /[:.]/} keys %$args) && throw "Failed to load Data::URIEncode", {msg => $@}));
      0        
      0        
131 0         0 return $args;
132             }
133 0 0       0 throw "Found other args in addition to POSTDATA", {args => $args} if scalar keys %$args;
134             }
135 0         0 $self->{'transport'} = 'json';
136 0 0       0 throw "Content data did not look like JSON hash", {head => substr($req, 0, 10)."...", content_type => $ENV{'CONTENT_TYPE'}} if $req !~ /^\{/;
137 0         0 $req_sum = md5_hex($req);
138             return eval { $self->json->decode($req) }
139 0   0     0 || throw 'Trouble unencoding json', {ip => $ENV{'REMOTE_ADDR'}, msg => $@, head => substr($req, 0, 10)."..."};
140             };
141 0 0       0 if (! $args) {
142 0         0 my $err = $self->json->encode({error => "$@", type => 'cgihandler'});
143 0         0 warn $err;
144 0         0 return $self->send_response($err);
145             }
146 0   0     0 $ENV{'PATH_INFO'} ||= '';
147              
148 0 0       0 my ($old_out, $out_ref) = $self->{'warn_on_stdout'} ? do { open my $fh, ">", \(my $str =""); (select($fh), \$str) } : ();
  0         0  
  0         0  
149 0         0 local $self->{'_warn_info'};
150 0         0 my $ref = eval { $self->_do_request($args, $req_sum, \%ENV) };
  0         0  
151 0 0       0 if (! $ref) {
152 0         0 $ref = $@;
153 0 0 0     0 $ref = eval { throw 'Trouble dispatching', {path => $ENV{'PATH_INFO'}, msg => $ref} } || $@ if !ref($ref) || !$ref->{'error'};
      0        
154 0 0       0 local @$ref{keys %$_} = values %$_ if $_ = $self->{'_warn_info'};
155 0         0 warn $ref;
156             }
157 0 0       0 if ($old_out) {
158 0         0 select $old_out;
159 0 0       0 warn "--- INVALID STDOUT ---\n$$out_ref\n" if $$out_ref;
160             }
161              
162 0 0 0     0 if (ref($ref) eq 'ARRAY' && @$ref == 3 && $ref->[0] =~ /^\d+$/) {
      0        
163 0 0       0 return $ref if $self->{'is_psgi'};
164 0         0 require Net::Server::PSGI;
165 0         0 $self->Net::Server::PSGI::print_psgi_headers($ref->[0], $ref->[1]);
166 0         0 $self->Net::Server::PSGI::print_psgi_body($ref->[2]);
167 0         0 return 1;
168             }
169              
170 0 0       0 $self->{'extra_headers'} = delete $ref->{'_extra_headers'} if $ref->{'_extra_headers'};
171 0   0     0 my $out = eval { $self->json->encode($ref) } || do { warn "Trouble encoding json: $@"; "{'error':'Trouble encoding json - check server logs for details'}" };
172 0         0 return $self->send_response($out);
173             }
174              
175             sub _do_request {
176 0     0   0 my ($self, $args, $req_sum, $env) = @_;
177 0         0 my ($method, $brand, $extra) = $self->_map_request($args, $env);
178 0         0 my $ver = $self->verify_sig($args, $req_sum, $env, $method, $brand);
179              
180 0         0 $self->{'_warn_info'} = {caller => {who => $args->{'_w'}, source => $args->{'_c'}, method => $method, brand => $brand, ip => $env->{'REMOTE_ADDR'}}};
181 0         0 local $env->{'REMOTE_USER'};
182             my $disp = $self->dispatch_factory->({
183 0 0       0 %{ $extra || {} },
184             is_server => $self->server_name,
185             ($env->{'HTTP_X_FORWARDED_FOR'}
186             ? (api_ip => $env->{'HTTP_X_FORWARDED_FOR'}, is_proxy => $env->{'REMOTE_ADDR'})
187             : (api_ip => $env->{'REMOTE_ADDR'})),
188             api_auth => $ver,
189             api_brand => $brand,
190             api_method => $method,
191             remote_user => delete($args->{'_w'}),
192             remote_ip => delete($args->{'_i'}),
193             token => delete($args->{'_t'}) || do { my $k = $self->config('admin_cookie_key'); $k ? $self->parse_cookies->{$k} : '' },
194             caller => delete($args->{'_c'}),
195             dbh_cache => $self->_dbh_cache,
196 0 0 0     0 transport => $self->{'transport'},
197             });
198 0 0       0 $disp->server_init($method, $args, $self) if $disp->can('server_init');
199              
200 0         0 local $0 = "$0 ".$self->server_name." $method - $env->{'REMOTE_ADDR'}";
201 0 0       0 return $disp->run_method($method, $args) if !$disp->can('server_post_request');
202              
203 0         0 my $ref;
204 0         0 my $ok = eval { $ref = $disp->run_method($method, $args); 1 };
  0         0  
  0         0  
205 0         0 my $err = $@;
206 0         0 $disp->server_post_request($method, $args, $ok, $ref, $err);
207 0 0       0 return $ref if $ok;
208 0         0 die $err;
209             }
210              
211             sub _map_request {
212 0     0   0 my ($self, $args, $env) = @_;
213 0         0 my $no_brand = $self->_no_brand;
214             my ($meth, $brand) = ((!$no_brand || $no_brand < 0) && $env->{'PATH_INFO'} =~ m|^/+(.+)/([^/]+)$|) ? ($1, $2)
215             : ($env->{'PATH_INFO'} =~ m|^/+(.+)$|) ? ($1, $no_brand ? undef : throw "Failed to find brand with method", {uri => "/$1"})
216 0 0 0     0 : throw "Failed to find method in URI", {uri => $env->{'PATH_INFO'}};
    0          
    0          
217 0         0 delete @$args{qw(_p _b)}; # legacy brand and password passing
218 0         0 return ($meth, $brand);
219             }
220              
221 0     0   0 sub _dbh_cache { {} } # intentionally not persistent
222              
223             sub cgi_obj {
224 0     0 0 0 my ($self, $r) = @_;
225 0   0     0 return $self->{'cgi_obj'} ||= do {
226 0 0 0     0 eval { CGI::initialize_globals() } or warn "Failed to initialize globals: $@" if $INC{'CGI.pm'}; # CGI.pm caches query parameters
  0         0  
227 0 0       0 eval { $self->{'is_psgi'} ? require CGI::PSGI : require CGI } || throw 'Cannot load CGI library during a non-JSON request', {msg => $@, type => $ENV{'CONTENT_TYPE'}};
  0 0       0  
228 0         0 local $CGI::POST_MAX = $self->max_request_size;
229 0 0 0     0 my $q = $self->{'is_psgi'} ? CGI::PSGI->new($self->{'is_psgi'}) : CGI->new($r || $self->{'apache_req'} || ());
230             };
231             }
232              
233             sub parse_form {
234 0     0 0 0 my ($self, $r) = @_;
235 0         0 my $q = $self->cgi_obj($r);
236 0 0       0 return {map {my @v = $q->param($_); $_ => (@v <= 1 ? $v[0] : \@v)} $q->param};
  0         0  
  0         0  
237             }
238              
239             sub parse_cookies {
240 0     0 0 0 my ($self, $r) = @_;
241 0   0     0 my $env = $self->{'is_psgi'} || \%ENV;
242 0 0       0 return {} if !$env->{'HTTP_COOKIE'};
243 0         0 my $q = $self->cgi_obj($r);
244 0 0       0 return {map {my @v = $q->cookie($_); $_ => (@v <= 1 ? $v[0] : \@v)} $q->cookie};
  0         0  
  0         0  
245             }
246              
247 0   0 0 0 0 sub content_type { shift->{'content_type'} ||= 'application/json' }
248              
249             sub send_response {
250 0     0 0 0 my ($self, $str) = @_;
251 0 0       0 $str =~ s/\s*$/\r\n/ if $self->content_type =~ m{^(?:text/|application/json$)};
252 0 0       0 my @extra = $self->{'extra_headers'} ? @{ $self->{'extra_headers'} } : ();
  0         0  
253 0 0 0     0 if ($self->{'is_psgi'}) {
    0          
    0          
254 0         0 return [200, [(map {$_->[0], $_->[1]} @extra), 'Content-type' => $self->content_type, 'Content-length' => length($str)], [$str]];
  0         0  
255             } elsif (my $r = $self->{'apache_req'} || eval { $ENV{'MOD_PERL'} && Apache2::RequestUtil->request }) {
256 0         0 $r->headers_out->set($_->[0] => $_->[1]) for @extra;
257 0         0 $r->headers_out->set('Content-length', length($str));
258 0         0 $r->content_type($self->content_type);
259 0         0 $r->print($str);
260             } elsif (my $c = $self->{'server'}->{'client'}) { # accelerate output header generation under Net::Server
261 0         0 my $ri = $self->{'request_info'};
262 0         0 my $out = "HTTP/1.0 200 OK\015\012";
263 0         0 foreach my $row (@{ $self->http_base_headers }, @extra, ['Content-length', length($str)], ['Content-type', $self->content_type]) {
  0         0  
264 0         0 $out .= "$row->[0]: $row->[1]\015\012";
265 0         0 push @{ $ri->{'response_headers'} }, $row;
  0         0  
266             }
267 0         0 $ri->{'response_header_size'} += length $out;
268 0         0 $ri->{'http_version'} = '1.0';
269 0         0 $ri->{'response_status'} = 200;
270 0         0 $ri->{'headers_sent'} = 1;
271 0         0 $ri->{'response_size'} = length $str;
272 0         0 $c->print("$out\015\012$str");
273             } else {
274             # Otherwise, this is a normal CGI process.
275             # XXX - Do we need to also convert "Status" header for the special NPH format?
276 0 0 0     0 print "HTTP/1.0 200 OK\r\n" if ($ENV{SCRIPT_FILENAME} // "") =~ m{/nph-[^/]+($|\s)};
277 0         0 for my $h (@extra) {
278 0         0 print "$h->[0]: $h->[1]\r\n";
279             }
280 0         0 print "Content-Type: ".$self->content_type."\r\nContent-Length: ".length($str)."\r\nContent-Type: ".$self->content_type."\r\n\r\n",$str;
281             }
282 0         0 return 1;
283             }
284              
285 0     0   0 sub _no_brand { shift->config(no_brand => undef) }
286              
287             sub verify_sig {
288 0     0 0 0 my ($self, $args, $req_sum, $env, $meth, $brand) = @_;
289 0         0 my ($ip, $sig, $script, $path_info, $qs, $auth) = @$env{qw(REMOTE_ADDR HTTP_X_RESPITE_AUTH SCRIPT_NAME PATH_INFO QUERY_STRING HTTP_AUTHORIZATION)};
290 0   0     0 my $uri = $script || throw "Missing script";
291 0 0       0 $uri .= $path_info if $path_info;
292 0 0       0 $uri .= "?$qs" if $qs;
293              
294 0         0 my ($type, $user, $exception);
295 0 0       0 if ($auth) {
296 0 0       0 throw "Cannot pass both Authorization and X-Respite-Auth", {authorization => $auth, x_respite_auth => $sig, uri => $uri, ip => $ip} if $sig;
297 0 0       0 if ($auth =~ s/^Basic \s+ (\S+)$/$1/x) {
    0          
    0          
298 0         0 $type = 'basic';
299 0         0 require MIME::Base64;
300 0         0 ($user, $sig) = split /:/, MIME::Base64::decode_base64($auth), 2;
301 0 0       0 $exception = Throw->new("Basic authentication not allowed", {user => $user}) if ! $self->allow_auth_basic($brand, $user);
302             } elsif ($auth =~ s/^Digest \s+//x) {
303 0         0 $type = 'digest';
304 0         0 $sig->{'method'} = $ENV{'REQUEST_METHOD'};
305 0 0 0     0 $sig->{$1} = (defined($3) && length($3)) ? $3 : $2 while $auth =~ s/^ (\w+) = (?: "([^\"]+)" | ([^\s\",]+)) (?:\s*$|,\s*) //gxs;
306 0         0 $user = $sig->{'username'};
307             } elsif ($auth =~ s/^RespiteAuth \s+//x) {
308 0         0 $type = 'signed';
309 0         0 $sig = $auth;
310             } else {
311 0         0 $exception = Throw->new("Unknown auth type", {authorization => $auth, uri => $uri, ip => $ip, authtype => 'unknown'});
312             }
313             } else {
314 0         0 my $allow_md5 = $self->allow_auth_md5_pass($brand);
315 0 0 0     0 $sig ||= $args->{'x_respite_auth'} if $allow_md5;
316 0 0       0 $type = !$sig ? 'none' : ($sig !~ /^[a-f0-z]{32}$/) ? 'signed' : $allow_md5 ? 'md5_pass' : throw 'Auth type md5_pass not allowed';
    0          
    0          
317             }
318 0   0     0 my $pass = $self->get_api_pass($brand || '', $ip, $sig, $type, $user, $exception) || [];
319 0 0       0 $pass = ref($pass) ? undef : [$pass] if ref($pass) ne 'ARRAY';
    0          
320 0 0 0     0 return {authorization_not_required => 1, ip => $ip, brand => $brand, authtype => $type, exception => $exception} if $pass && !@$pass;
321 0 0       0 die $exception if defined $exception;
322 0 0 0     0 throw "Missing client authorization", {server_name => $self->server_name, ip => $ip, brand => $brand, authtype => $type, uri => $uri} if !$sig && $type && $type ne 'none';
      0        
323              
324 0 0       0 if ($pass) {
325 0         0 for my $i (0 .. $#$pass) {
326             next if ($type eq 'basic') ? $pass->[$i] ne $sig
327             : ($type eq 'md5_pass') ? md5_hex($pass->[$i]) ne $sig
328 0         0 : ($type eq 'signed') ? do { my ($_sum, $time) = split /:/, $sig, 2; md5_hex("$pass->[$i]:$time:$uri:$req_sum") ne $_sum }
  0         0  
329 0 0 0     0 : ($type eq 'digest') ? (eval { $self->verify_digest($sig||={}, $pass->[$i], $uri, $req_sum, $meth, $brand, $ip) } ? 0 : do { $sig->{'verify'} = $@; 1 })
  0 0       0  
  0 0       0  
  0 0       0  
    0          
    0          
330             : 1;
331 0 0       0 return {authtype => $type, ip => $ip, brand => $brand, meth => $meth, i => $i, ($self->{'verify_sig_return_pass'} ? (pass => $pass->[$i]) : ()), ($type eq 'digest'?(digest=>$sig):())};
    0          
332             }
333             }
334 0 0       0 throw "Invalid client authorization", {($type eq 'digest'?(digest=>$sig):()), server_name => $self->server_name, ip => $ip, brand => $brand, authtype => $type, uri => $uri};
335             }
336              
337             my %cidr;
338             sub get_api_pass {
339 0     0 0 0 my ($self, $brand, $ip, $sig, $type, $user, $except) = @_;
340 0         0 my $ref = $self->config(pass => undef);
341 0 0 0     0 return $ref if ! ref($ref) || ref($ref) eq 'ARRAY';
342 0 0 0     0 if (exists $ref->{$ip}) {
    0          
    0          
343 0 0       0 return $ref->{$ip} if ref($ref->{$ip}) ne 'HASH';
344 0 0       0 return $ref->{$ip}->{$brand} if exists $ref->{$ip}->{$brand};
345 0 0       0 return $ref->{$ip}->{'~default~'} if exists $ref->{$ip}->{'~default~'};
346 0 0       0 return $ref->{$ip}->{'-default'} if exists $ref->{$ip}->{'-default'};
347             } elsif (exists $ref->{$brand}) {
348 0 0       0 return $ref->{$brand} if ref($ref->{$brand}) ne 'HASH';
349 0 0       0 return $ref->{$brand}->{$ip} if exists $ref->{$brand}->{$ip};
350 0 0       0 return $ref->{$brand}->{'~default~'} if exists $ref->{$brand}->{'~default~'};
351 0 0       0 return $ref->{$brand}->{'-default'} if exists $ref->{$brand}->{'-default'};
352             } elsif (my $c = $ref->{'~cidr~'} || $ref->{'-cidr'}) {
353 0         0 my $n = _aton($ip);
354 0         0 foreach my $cidr (keys %$c) {
355 0   0     0 my $range = $cidr{$cidr} ||= _cidr($cidr);
356 0 0 0     0 next if $n < $range->[0] || $n > $range->[1];
357 0         0 my $ref = $c->{$cidr};
358 0 0       0 if (ref($ref) eq 'HASH') {
359 0 0       0 return $ref->{$brand} if exists $ref->{$brand};
360 0 0       0 return $ref->{'~default~'} if exists $ref->{'~default~'};
361 0 0       0 return $ref->{'-default'} if exists $ref->{'-default'};
362             }
363 0         0 return $ref;
364             }
365             }
366              
367 0 0       0 return $ref->{'~default~'} if exists $ref->{'~default~'};
368 0 0       0 return $ref->{'-default'} if exists $ref->{'-default'};
369 0         0 throw "Not authorized - Could not find brand/ip match in pass configuration", {brand => $brand, ip => $ip, service => $self->server_name};
370             }
371 0     0   0 sub _aton { my $ip = shift; return unpack "N", pack "C4", split /\./, $ip }
  0         0  
372 0 0   0   0 sub _cidr { (my $c = shift) =~ s/\s+//; my ($ip, $base) = split /\//, $c; my $i = _aton($ip); $i &= 2**32 - 2**(32-$base) if !$_[0]; return [$i, $i+2**(32-$base)-1] }
  0         0  
  0         0  
  0         0  
  0         0  
373              
374 0     0 0 0 sub allow_auth_md5_pass { shift->config(allow_auth_md5_pass => undef) }
375 0     0 0 0 sub allow_auth_basic { shift->config(allow_auth_basic => undef) }
376 0     0 0 0 sub allow_auth_qop_auth { shift->config(allow_auth_qop_auth => undef) }
377 0 0   0 0 0 sub digest_realm { shift->config(realm => sub { my $name = shift->server_name; return $name =~ /^(\w+)_server/ ? $1 : $name }) }
  0     0   0  
  0         0  
378              
379             sub verify_digest {
380 0     0 0 0 my ($self, $digest, $pass, $uri, $req_sum, $meth, $brand, $ip) = @_;
381 0 0 0 0   0 my $d = sub { my ($key, $opt) = @_; my $val = $digest->{$key}; $opt ? ($val='') : throw "Digest directive $key was missing" if !defined($val) || !length($val); $val };
  0 0       0  
  0         0  
  0         0  
  0         0  
382 0 0 0     0 throw "Missing or invalid digest username" if $brand && $d->('username') ne $brand;
383 0 0       0 throw "Missing or invalid digest realm", {realm => $self->digest_realm} if $d->('realm') ne $self->digest_realm;
384 0 0       0 throw "Digest URI did not match", {digest => $d->('uri'), actual => $uri} if $uri ne $d->('uri');
385 0         0 my $ha1 = md5_hex($d->('username') .':'. $d->('realm').":$pass");
386 0 0       0 $ha1 = md5_hex("$ha1:".$d->('nonce').':'.$d->('cnonce')) if lc($d->('algorithm',1)) eq 'md5-sess';
387 0 0       0 my $ha2 = md5_hex($d->('method').":$uri".(($d->('qop',1) eq 'auth-int') ? ":$req_sum" : $self->allow_auth_qop_auth($brand) ? '' : throw 'Digest qop auth not allowed'));
    0          
388 0 0       0 my $sum = md5_hex("$ha1:".$d->('nonce').($d->('qop',1) ? ':'.$d->('nc').':'.$d->('cnonce').':'.$d->('qop') : '').":$ha2");
389 0 0       0 throw 'Digest did not validate' if $sum ne $d->('response');
390 0         0 return 1;
391             }
392              
393             ###----------------------------------------------------------------###
394             # Net::Server::HTTP bits
395              
396             sub server_args {
397 5     5 0 10 my $self = shift;
398 5         24 my $name = $self->server_name;
399 5     60   81 my $val = sub { my ($key, $def) = @_; $self->config($key, $def, $name) };
  60         171  
  60         120  
400 5 50       37 my $path = $val->(path => ($name =~ /^(\w+)_server/ ? $1 : $name));
401 5         10 my $host = $val->(host => '*');
402 5         25 my $port = $val->(port => 443);
403 5         13 my $ssl = !$val->(no_ssl => undef);
404 5 0 33     18 my $ad = $val->(auto_doc => ''); $ad = ($name =~ /^(\w+)_server/ ? $1 : $name).'_doc' if $ad && $ad eq '1';
  5 50       21  
405 5 50       8 my $is_dev = eval { defined(&config::is_dev) && config::is_dev() };
  5         25  
406 5   33     18 my $use_dev_port = $is_dev && $ssl && !$val->(no_dev_port => '');
407 5         10 my $res = $val->(cgi_bin => undef);
408 5 0       15 my $app = !$res ? \&cgihandler : ($res ne 1) ? $res : 'cgi-bin/'.($name =~ /^(\w+)_server/ ? $1 : $name);
    0          
    50          
409 5 0 33     15 $app = $self->rootdir_server ."/$app" if !ref($app) && $app !~ /^\//;
410 5         10 my $st = $val->(server_type => 'PreFork');
411             return {
412             server_type => ref($st) ? $st : [$st],
413             enable_dispatch => 1,
414             ipv => 4,
415 5 50 33     30 app => [[(map{$_ => $app} ref($path) ? @$path : $path),
  5 50       111  
    50          
    50          
    0          
    50          
    50          
416             ($ad ? ($ad => \&cgidoc) : ()),
417             '' => \&http_not_found]],
418             port => [
419             {port => $port, host => $host, ($ssl ? (proto => 'SSL') : ())},
420             ($use_dev_port ? {port => ($port == 443 ? 80 : $port-1), host => $host} : ()), # allow for dev to telnet to a non-ssl
421             ],
422             serialize => ($is_dev && $ssl) ? 'flock' : 'none', # can only do if hard coded to ipv4 and host resolves to one ip
423             access_log_file => $val->(access_log_file => "/var/log/${name}/${name}.access_log"),
424             log_file => $val->(log_file => "/var/log/${name}/${name}.error_log"),
425             pid_file => $val->(pid_file => "/var/run/${name}.pid"),
426             user => $val->(user => 'readonly'),
427             group => $val->(group => 'cvs'),
428             };
429             }
430              
431 0   0 0 1 0 sub rootdir_server { shift->config(rootdir_server => $config::config{'rootdir_server'} || sub { require FindBin; $FindBin::RealBin }) }
  0     0   0  
  0         0  
432 0     0 0 0 sub SSL_base_domain { 'example.com' }
433 0     0 0 0 sub SSL_cert_file { shift->config(ssl_cert => sub { shift->rootdir_server .shift->SSL_base_domain().'.crt' }) }
  0     0   0  
434 0     0 0 0 sub SSL_key_file { shift->config(ssl_key => sub { shift->rootdir_server .shift->SSL_base_domain().'.key' }) }
  0     0   0  
435              
436             sub post_bind {
437 2     2 1 26665 my $self = shift;
438 2         73 $0 = $self->server_name;
439 2         58 $self->SUPER::post_bind(@_);
440             }
441              
442 0     0 1 0 sub child_init_hook { $0 = shift->server_name ." - waiting" } # prefork server
443              
444             sub run_client_connection {
445 0     0 1 0 my $self = shift;
446 0         0 $0 = $self->server_name . " - connected";
447 0         0 $self->SUPER::run_client_connection(@_);
448 0 0       0 $_->($self) for @{ $self->{'post_client_callbacks'} || [] };
  0         0  
449             }
450              
451             sub server_revision {
452 0     0 1 0 my $self = shift;
453 0 0 0     0 return $self->{'server_revision'} ||= $self->server_name.'/'.$self->revision.($self->{'nshv'} ? ' '.$self->SUPER::server_revision : '');
454             }
455              
456 0     0 0 0 sub http_not_found { shift->send_status(404, "Not found", "

Not Found

") }
457              
458 0     0 1 0 sub post_process_request_hook { $0 = shift->server_name ." - post_request" }
459              
460 8     8 1 25408 sub default_values { {background => 1, setsid => 1} }
461              
462             ###----------------------------------------------------------------###
463             # Net::Server::HTTP daemonization bits
464              
465 2     2 0 466 sub run_server { shift->SUPER::run(@_) }
466              
467 0     0 1 0 sub run { throw "Use either run_server or run_commandline for clarity" }
468              
469             sub run_commandline {
470 0     0 0 0 my $class = shift;
471 0 0 0     0 my $sub = $ARGV[0] && $class->can("__$ARGV[0]") ? "__$ARGV[0]" : undef;
472 0 0       0 shift(@ARGV) if $sub;
473              
474 0 0       0 if ($ENV{'BOUND_SOCKETS'}) { # HUP
    0          
    0          
    0          
475 0 0       0 my $self = ref($class) ? $class : $class->new(@_);
476 0         0 $self->run_server; # will exit
477 0         0 warn "Failed to re-initialize server during HUP\n";
478 0         0 exit 1;
479             } elsif ($sub) { # commandline server service
480 0 0       0 local $ENV{'NO_PRELOAD'} = 1 if $sub !~ /^__(?:start|restart|reload)$/;
481 0 0       0 my $self = ref($class) ? $class : $class->new(@_);
482 0         0 $self->$sub();
483             } elsif ($ENV{'PLACK_ENV'}) {
484 0         0 return $class->psgi_app(@_);
485             } elsif (!@ARGV) {
486 0         0 throw "$0 help|start|restart|reload|stop|status|tail_error|tail_access|ps|(or any Respite commands)";
487             } else {
488 0 0       0 my $args = ref($_[0]) ? shift : {@_};
489 0 0       0 my $self = ref($class) ? $class : $class->new({%$args, non_daemon => 1});
490 0         0 require Respite::CommandLine;
491 0         0 Respite::CommandLine->run({dispatch_factory => $self->dispatch_factory});
492             }
493              
494 0         0 exit 0;
495             }
496              
497             sub psgi_app {
498 0     0 0 0 my ($class, $args) = @_;
499 0         0 require IO::Socket; require Net::Server; require Net::Server::PreFork;
  0         0  
  0         0  
500             sub {
501 0     0   0 local *ENV = my $env = shift;
502 0 0       0 return $class->cgihandler({%{$args||{}}, non_daemon => 1, is_psgi => $env});
  0         0  
503 0         0 };
504             }
505              
506             sub _get_pid { # taken from Net::Server::Daemonize::check_pid_file - but modified
507 3     3   10 my $self = shift;
508 3         38 my $pid_file = $self->{'server'}->{'pid_file'};
509 3 100       84 return if ! -e $pid_file; # no pid_file = return success
510 1 50       9 return if -z $pid_file; # empty pid_file = return success
511 1 50       62 open my $fh, '<', $pid_file or throw "Could not open existing pid_file", {file => $pid_file, msg => $!};
512 1         24 my $line = <$fh>;
513 1         13 close $fh;
514 1 50       58 return ($line =~ /^(\d{1,10})$/) ? $1 : throw "Could not find pid in existing pid_file", {line => $line};
515             }
516              
517             sub _ok {
518 3     3   12 my ($ok, $msg) = @_;
519 3 50       255 warn "$msg\e[60G[". ($ok ? "\e[1;32m OK " : "\e[1;31mFAILED") ."\e[0;39m]\n";
520             }
521              
522             sub __status {
523 0     0   0 my $self = shift;
524 0         0 my $pid = $self->_get_pid;
525 0 0       0 return _ok(0, "Process is not running - no pid") if ! $pid;
526 0 0       0 return _ok(1, "Process appears to be running under pid $pid") if kill 0, $pid;
527 0         0 return _ok(0, "Process does not appear to be running - last pid: $pid");
528             }
529              
530             sub __start {
531 0     0   0 my $self = shift;
532 0         0 my $pid = $self->_get_pid;
533 0 0 0     0 if ($pid && kill(0, $pid)) {
534 0         0 _ok(0, "Starting - pid already exists");
535 0         0 throw "Process appears to already be running under pid $pid ... aborting";
536             }
537              
538 0         0 my $pid_file = $self->{'server'}->{'pid_file'};
539 0 0       0 if (-e $pid_file) {
540 0 0       0 unlink $pid_file or throw "Failed to unlink pid file", {file => $pid_file, msg => $!};
541             }
542              
543 0         0 require Net::Server::Daemonize;
544 0 0       0 if (! Net::Server::Daemonize::safe_fork()) {
545             # child
546 0         0 $self->run_server(); # will exit
547 0         0 _ok(0, "Server run failed - check log");
548 0         0 exit 1;
549             }
550              
551 0         0 sleep 1;
552 0         0 $pid = $self->_get_pid;
553 0 0 0     0 if (!$pid || ! kill 0, $pid) {
554 0         0 _ok(0, "Starting - new pid not started - check log for details");
555 0         0 warn "Log file: $self->{'server'}->{'log_file'}\n";
556 0         0 exit 1;
557             }
558              
559             # could attempt connection to test for open success
560              
561 0         0 _ok(1, "Started server");
562              
563             }
564              
565             sub __stop {
566 3     3   13 my $self = shift;
567 3         144 my $pid = $self->_get_pid;
568 3         66 my $name = $self->server_name;
569 3 100       35 if (!$pid) {
    50          
570 2         44 return _ok(1, "Already Stopped $name");
571             } elsif (! kill 0, $pid) {
572 0         0 warn "Cannot kill 0 $pid while stopping: $!\n";
573 0         0 return _ok(0, "Failed to stop $name");
574             }
575 1 50 33     29 if (! (kill(15, $pid) || kill(9, $pid))) {
576 0         0 warn "Failed to kill TERM or KILL pid $pid while stopping\n";
577 0         0 return _ok(0, "Failed to stop $name");
578             }
579 1         4 for (1 .. 25) {
580 10 100       229 return _ok(1, "Stopped $name") if !kill 0, $pid;
581 9         1805405 sleep 0.2;
582 9         214 require POSIX;
583 9         330 1 while waitpid(-1, POSIX::WNOHANG()) > 0; # handle rare non-setsid uses of run and _stop
584             }
585              
586 0         0 _ok(0, "Stopping - pid still running");
587 0         0 exit 1;
588             }
589              
590             sub __restart {
591 0     0   0 my $self = shift;
592 0         0 $self->__stop;
593 0         0 $self->__start;
594             }
595              
596             sub __reload {
597 0     0   0 my $self = shift;
598 0         0 my $pid = $self->_get_pid;
599 0 0       0 if (!$pid) {
    0          
600 0         0 _ok(1, "Process appears to be stopped already - attempting start");
601 0         0 return $self->__start;
602             } elsif (! kill 0, $pid) {
603 0         0 _ok(1, "Process appears to be stopped (kill 0) - attempting start");
604 0         0 return $self->__start;
605             }
606 0 0       0 if (! kill 1, $pid) {
607 0         0 _ok(0, "Reload failed: $!");
608 0         0 exit 1;
609             }
610              
611 0         0 sleep 1;
612              
613 0 0       0 if (kill 0, $pid) {
614 0         0 _ok(1, "Reloaded server");
615             } else {
616 0         0 _ok(0, "Sent HUP - but server is gone away - attempting start");
617 0         0 $self->__start;
618             }
619             }
620              
621 0     0   0 sub __size_access { shift->__size_error('access_log_file') }
622              
623             sub __size_error {
624 0     0   0 my ($self, $file) = @_;
625 0   0     0 $file = $self->{'server'}->{$file || 'log_file'} || throw "No log_file to size";
626 0         0 return -s $file;
627             }
628              
629 3     3   27 sub __tail_access { shift->__tail_error(shift(), 'access_log_file') }
630              
631             sub __tail_error {
632 6     6   39 my ($self, $how, $file) = @_;
633 6   50     40 $how = quotemeta($how || shift(@ARGV) || 'f');
634 6   33     89 $file = quotemeta($self->{'server'}->{$file || 'log_file'} || throw "No log_file to tail");
635 6         19 my $cmd = "tail -$how $file";
636 6         208 warn "$cmd\n";
637 6 50       28 exec $cmd if $how eq 'f';
638 6   100     55321 warn `$cmd` || "No error log\n";
639             }
640              
641             sub __ps {
642 3     3   59 my $name = shift->server_name;
643 3 100       41635 my $out = join '', grep {$_ =~ $name && $_ !~ /\b(?:$$|watch|ps)\b/} `ps auwx`;
  52         612  
644 3   100     450 warn $out || "No processes found\n";
645             }
646              
647             ###----------------------------------------------------------------###
648              
649             sub cgidoc_brand {
650 0     0 0   my $self = shift;
651 0 0 0 0     return $self->config(no_brand => 0) ? undef : $self->config(brand => sub { eval { config::provider() } || $self->_configs->{'provider'} || do { warn "Missing brand"; '-' } });
  0 0          
  0            
  0            
  0            
652             }
653              
654             sub cgidoc {
655 0     0 0   my $self = shift;
656 0 0 0       eval { CGI::initialize_globals() } or warn "Failed to initialize globals: $@" if $INC{'CGI.pm'}; # CGI.pm caches query parameters
  0            
657              
658 0           my $name = $self->server_name;
659             my $disp = $self->dispatch_factory->({
660             is_server => "$name/doc",
661             api_ip => $ENV{'REMOTE_ADDR'},
662             api_brand => $self->cgidoc_brand,
663 0           remote_ip => $ENV{'REMOTE_ADDR'},
664             remote_user => '-auto-doc-',
665             # token and remote_user will be updated by auto_doc_class if it is based on App::_Admin
666             dbh_cache => {},
667             transport => 'form-doc',
668             });
669              
670 0           my $class = $self->config(auto_doc_class => 'Respite::AutoDoc');
671 0           (my $file = "$class.pm") =~ s|::|/|g;
672 0           require $file;
673 0 0         $class->new({
674             service => (($name =~ /^(\w+)_server/) ? $1 : $name),
675             server => $self,
676             api_obj => $disp,
677             })->navigate;
678             }
679              
680             ###----------------------------------------------------------------###
681              
682             1;