|
@@ -27,13 +27,13 @@ use Date::Format qw{strftime};
|
|
|
use List::Util();
|
|
use List::Util();
|
|
|
use File::Find;
|
|
use File::Find;
|
|
|
use Sys::Hostname();
|
|
use Sys::Hostname();
|
|
|
-use Plack::MIME ();
|
|
|
|
|
|
|
+use Plack::MIME ();
|
|
|
use DateTime::Format::HTTP();
|
|
use DateTime::Format::HTTP();
|
|
|
|
|
|
|
|
use URL::Encode();
|
|
use URL::Encode();
|
|
|
use File::Touch;
|
|
use File::Touch;
|
|
|
use File::Path;
|
|
use File::Path;
|
|
|
-use Cwd qw{abs_path};
|
|
|
|
|
|
|
+use Cwd qw{abs_path};
|
|
|
use File::Basename qw{dirname basename};
|
|
use File::Basename qw{dirname basename};
|
|
|
use Log::Dispatch;
|
|
use Log::Dispatch;
|
|
|
use Log::Dispatch::Screen;
|
|
use Log::Dispatch::Screen;
|
|
@@ -84,12 +84,13 @@ my %extra_types = (
|
|
|
'.docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
'.docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
-my $ct = 'Content-type';
|
|
|
|
|
|
|
+my $ct = 'Content-type';
|
|
|
|
|
+
|
|
|
#memoize
|
|
#memoize
|
|
|
my $rq;
|
|
my $rq;
|
|
|
|
|
|
|
|
sub request_id {
|
|
sub request_id {
|
|
|
- my ($self, $regenerate) = @_;
|
|
|
|
|
|
|
+ my ( $self, $regenerate ) = @_;
|
|
|
return $self->{uuid} if $self->{uuid} && !$regenerate;
|
|
return $self->{uuid} if $self->{uuid} && !$regenerate;
|
|
|
$self->{uuid} = UUID::uuid();
|
|
$self->{uuid} = UUID::uuid();
|
|
|
return $self->{uuid};
|
|
return $self->{uuid};
|
|
@@ -101,12 +102,12 @@ sub log {
|
|
|
state $log;
|
|
state $log;
|
|
|
return $log if $log;
|
|
return $log if $log;
|
|
|
|
|
|
|
|
- my $LOGNAME = abs_path('.').'/log/tpsgi.log';
|
|
|
|
|
|
|
+ my $LOGNAME = abs_path('.') . '/log/tpsgi.log';
|
|
|
$LOGNAME = $self->{custom_log} if $self->{custom_log};
|
|
$LOGNAME = $self->{custom_log} if $self->{custom_log};
|
|
|
|
|
|
|
|
my $LOGDIR = dirname($LOGNAME);
|
|
my $LOGDIR = dirname($LOGNAME);
|
|
|
File::Path::make_path($LOGDIR) unless -d $LOGDIR;
|
|
File::Path::make_path($LOGDIR) unless -d $LOGDIR;
|
|
|
- File::Touch::touch($LOGNAME) unless -f $LOGNAME;
|
|
|
|
|
|
|
+ File::Touch::touch($LOGNAME) unless -f $LOGNAME;
|
|
|
|
|
|
|
|
my $LEVEL = $self->{verbose} ? 'debug' : 'info';
|
|
my $LEVEL = $self->{verbose} ? 'debug' : 'info';
|
|
|
|
|
|
|
@@ -130,8 +131,8 @@ sub log {
|
|
|
$log = Log::Dispatch->new();
|
|
$log = Log::Dispatch->new();
|
|
|
$log->add($rotate);
|
|
$log->add($rotate);
|
|
|
$log->add($screen);
|
|
$log->add($screen);
|
|
|
- foreach my $logger (@{$self->{loggers}}) {
|
|
|
|
|
- $log->add($logger->new( min_level => $LEVEL, log_dir => $LOGDIR ));
|
|
|
|
|
|
|
+ foreach my $logger ( @{ $self->{loggers} } ) {
|
|
|
|
|
+ $log->add( $logger->new( min_level => $LEVEL, log_dir => $LOGDIR ) );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
$log->info( $self->_log("Opening Log $LOGNAME at $LEVEL level") );
|
|
$log->info( $self->_log("Opening Log $LOGNAME at $LEVEL level") );
|
|
@@ -146,7 +147,7 @@ sub _log {
|
|
|
|
|
|
|
|
#XXX Log lines must start as an ISO8601 date, anything else breaks fail2ban's beautiful mind
|
|
#XXX Log lines must start as an ISO8601 date, anything else breaks fail2ban's beautiful mind
|
|
|
my $tstamp = POSIX::strftime "%Y-%m-%dT%H:%M:%SZ", gmtime;
|
|
my $tstamp = POSIX::strftime "%Y-%m-%dT%H:%M:%SZ", gmtime;
|
|
|
- my $uuid = $self->request_id();
|
|
|
|
|
|
|
+ my $uuid = $self->request_id();
|
|
|
|
|
|
|
|
return "[Worker $$] {Request $uuid} $tstamp : $self->{ip} $msg\n";
|
|
return "[Worker $$] {Request $uuid} $tstamp : $self->{ip} $msg\n";
|
|
|
}
|
|
}
|
|
@@ -154,39 +155,47 @@ sub _log {
|
|
|
# Logger short cuts
|
|
# Logger short cuts
|
|
|
sub DEBUG {
|
|
sub DEBUG {
|
|
|
my $self = shift;
|
|
my $self = shift;
|
|
|
- $self->log->debug( $self->_log( shift ) );
|
|
|
|
|
|
|
+ $self->log->debug( $self->_log(shift) );
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
sub INFO {
|
|
sub INFO {
|
|
|
my $self = shift;
|
|
my $self = shift;
|
|
|
- $self->log->info( $self->_log( shift ) );
|
|
|
|
|
|
|
+ $self->log->info( $self->_log(shift) );
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
sub NOTE {
|
|
sub NOTE {
|
|
|
my $self = shift;
|
|
my $self = shift;
|
|
|
- $self->log->notice( $self->_log( shift ) );
|
|
|
|
|
|
|
+ $self->log->notice( $self->_log(shift) );
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
sub WARN {
|
|
sub WARN {
|
|
|
my $self = shift;
|
|
my $self = shift;
|
|
|
- $self->log->warning( $self->_log( shift ) );
|
|
|
|
|
|
|
+ $self->log->warning( $self->_log(shift) );
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
sub ERROR {
|
|
sub ERROR {
|
|
|
my $self = shift;
|
|
my $self = shift;
|
|
|
- $self->log->error( $self->_log( shift ) );
|
|
|
|
|
|
|
+ $self->log->error( $self->_log(shift) );
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
sub CRIT {
|
|
sub CRIT {
|
|
|
my $self = shift;
|
|
my $self = shift;
|
|
|
- $self->log->critical( $self->_log( shift ) );
|
|
|
|
|
|
|
+ $self->log->critical( $self->_log(shift) );
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
sub ALERT {
|
|
sub ALERT {
|
|
|
my $self = shift;
|
|
my $self = shift;
|
|
|
- $self->log->alert( $self->_log( shift ) );
|
|
|
|
|
|
|
+ $self->log->alert( $self->_log(shift) );
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
sub EMERG {
|
|
sub EMERG {
|
|
|
my $self = shift;
|
|
my $self = shift;
|
|
|
- $self->log->emergency( $self->_log( shift ) );
|
|
|
|
|
|
|
+ $self->log->emergency( $self->_log(shift) );
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
sub FATAL {
|
|
sub FATAL {
|
|
|
my $self = shift;
|
|
my $self = shift;
|
|
|
- $self->log->log_and_die( level => 'emergency', message => $self->_log( shift ) );
|
|
|
|
|
|
|
+ $self->log->log_and_die( level => 'emergency', message => $self->_log(shift) );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
=head1 new(%options)
|
|
=head1 new(%options)
|
|
@@ -202,13 +211,13 @@ Options are:
|
|
|
=cut
|
|
=cut
|
|
|
|
|
|
|
|
sub new {
|
|
sub new {
|
|
|
- my ($class, %options) = @_;
|
|
|
|
|
|
|
+ my ( $class, %options ) = @_;
|
|
|
|
|
|
|
|
# Refuse to run as wrong user, all config will be borked otherwise
|
|
# Refuse to run as wrong user, all config will be borked otherwise
|
|
|
my $pname = getpwuid($>);
|
|
my $pname = getpwuid($>);
|
|
|
- die "Must run as configured user (got: $pname, want: $options{user})!" unless $pname eq ($options{user} // '');
|
|
|
|
|
|
|
+ die "Must run as configured user (got: $pname, want: $options{user})!" unless $pname eq ( $options{user} // '' );
|
|
|
|
|
|
|
|
- my $self = bless(\%options, $class);
|
|
|
|
|
|
|
+ my $self = bless( \%options, $class );
|
|
|
$self->{ip} = '0.0.0.0';
|
|
$self->{ip} = '0.0.0.0';
|
|
|
|
|
|
|
|
my @routes;
|
|
my @routes;
|
|
@@ -216,50 +225,51 @@ sub new {
|
|
|
|
|
|
|
|
# XXX TODO make these routes able to be fully qualified namespaces (::)!
|
|
# XXX TODO make these routes able to be fully qualified namespaces (::)!
|
|
|
no strict 'refs';
|
|
no strict 'refs';
|
|
|
- foreach my $route (@{$self->{routers}}) {
|
|
|
|
|
|
|
+ foreach my $route ( @{ $self->{routers} } ) {
|
|
|
|
|
|
|
|
die "No such routing module $route" unless -f $route;
|
|
die "No such routing module $route" unless -f $route;
|
|
|
|
|
|
|
|
# The router needs to exist and have nonzero numbers of routes.
|
|
# The router needs to exist and have nonzero numbers of routes.
|
|
|
my ($package) = basename($route) =~ m/(\S+)\.pm$/;
|
|
my ($package) = basename($route) =~ m/(\S+)\.pm$/;
|
|
|
- my $r = "$package\:\:routes";
|
|
|
|
|
- my $a = "$package\:\:aliases";
|
|
|
|
|
|
|
+ my $r = "$package\:\:routes";
|
|
|
|
|
+ my $a = "$package\:\:aliases";
|
|
|
|
|
|
|
|
my $libdir = dirname($route);
|
|
my $libdir = dirname($route);
|
|
|
- push(@INC, $libdir);
|
|
|
|
|
|
|
+ push( @INC, $libdir );
|
|
|
|
|
|
|
|
local $@;
|
|
local $@;
|
|
|
$self->DEBUG("require $route");
|
|
$self->DEBUG("require $route");
|
|
|
my $success = eval { require $route; 1; };
|
|
my $success = eval { require $route; 1; };
|
|
|
if ($success) {
|
|
if ($success) {
|
|
|
my $pkg_routes = *$r{ARRAY};
|
|
my $pkg_routes = *$r{ARRAY};
|
|
|
- for (my $i=0; $i < @$pkg_routes; $i += 2) {
|
|
|
|
|
|
|
+ for ( my $i = 0; $i < @$pkg_routes; $i += 2 ) {
|
|
|
$self->DEBUG("Registered route $pkg_routes->[$i]");
|
|
$self->DEBUG("Registered route $pkg_routes->[$i]");
|
|
|
}
|
|
}
|
|
|
- push(@routes,@$pkg_routes);
|
|
|
|
|
|
|
+ push( @routes, @$pkg_routes );
|
|
|
|
|
|
|
|
my $pkg_aliases = *$a{HASH};
|
|
my $pkg_aliases = *$a{HASH};
|
|
|
- foreach my $al (keys(%$pkg_aliases)) {
|
|
|
|
|
|
|
+ foreach my $al ( keys(%$pkg_aliases) ) {
|
|
|
$self->DEBUG("aliased route $al to $pkg_aliases->{$al}");
|
|
$self->DEBUG("aliased route $al to $pkg_aliases->{$al}");
|
|
|
}
|
|
}
|
|
|
- @aliases{keys(%$pkg_aliases)} = values(%$pkg_aliases);
|
|
|
|
|
- } else {
|
|
|
|
|
|
|
+ @aliases{ keys(%$pkg_aliases) } = values(%$pkg_aliases);
|
|
|
|
|
+ }
|
|
|
|
|
+ else {
|
|
|
die "Could not load $route!\n$@\n";
|
|
die "Could not load $route!\n$@\n";
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
use strict 'refs';
|
|
use strict 'refs';
|
|
|
|
|
|
|
|
# Set the pattern used to discover the route if needed later.
|
|
# Set the pattern used to discover the route if needed later.
|
|
|
- for (my $i=0; $i < scalar(@routes); $i += 2 ) {
|
|
|
|
|
- $routes[$i+1]{pattern} = $routes[$i];
|
|
|
|
|
|
|
+ for ( my $i = 0; $i < scalar(@routes); $i += 2 ) {
|
|
|
|
|
+ $routes[ $i + 1 ]{pattern} = $routes[$i];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- $self->{indices} //=[];
|
|
|
|
|
- $self->{indices} = [@{$self->{indices}},qw{index.html index.htm index.cgi}];
|
|
|
|
|
|
|
+ $self->{indices} //= [];
|
|
|
|
|
+ $self->{indices} = [ @{ $self->{indices} }, qw{index.html index.htm index.cgi} ];
|
|
|
|
|
|
|
|
- $self->{routes} = \@routes;
|
|
|
|
|
- $self->{aliases} = \%aliases;
|
|
|
|
|
- $self->{callbacks} = [];
|
|
|
|
|
|
|
+ $self->{routes} = \@routes;
|
|
|
|
|
+ $self->{aliases} = \%aliases;
|
|
|
|
|
+ $self->{callbacks} = [];
|
|
|
return $self;
|
|
return $self;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -359,6 +369,7 @@ sub serve {
|
|
|
|
|
|
|
|
sub _range {
|
|
sub _range {
|
|
|
my ( $self, $fullpath, $fh, $ranges, $sz, %headers ) = @_;
|
|
my ( $self, $fullpath, $fh, $ranges, $sz, %headers ) = @_;
|
|
|
|
|
+
|
|
|
# Set mode
|
|
# Set mode
|
|
|
my $primary_ct = "Content-Type: $headers{'Content-type'}";
|
|
my $primary_ct = "Content-Type: $headers{'Content-type'}";
|
|
|
my $is_multipart = scalar(@$ranges) > 1;
|
|
my $is_multipart = scalar(@$ranges) > 1;
|
|
@@ -417,7 +428,7 @@ sub _range {
|
|
|
sub _generic {
|
|
sub _generic {
|
|
|
my ( $type, $code ) = @_;
|
|
my ( $type, $code ) = @_;
|
|
|
$type .= Carp::longmess() if $debug;
|
|
$type .= Carp::longmess() if $debug;
|
|
|
- return [$code, [$ct => $content_types{html}], ["$type"]];
|
|
|
|
|
|
|
+ return [ $code, [ $ct => $content_types{html} ], ["$type"] ];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
=head2 redirect, redirect_permanent, see_also
|
|
=head2 redirect, redirect_permanent, see_also
|
|
@@ -427,19 +438,19 @@ Redirects to the provided page.
|
|
|
=cut
|
|
=cut
|
|
|
|
|
|
|
|
sub redirect {
|
|
sub redirect {
|
|
|
- my ($self, $to) = @_;
|
|
|
|
|
|
|
+ my ( $self, $to ) = @_;
|
|
|
$self->INFO("redirect: $to");
|
|
$self->INFO("redirect: $to");
|
|
|
return [ 302, [ "Location" => $to, "Content-Length" => 0 ], [''] ];
|
|
return [ 302, [ "Location" => $to, "Content-Length" => 0 ], [''] ];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
sub redirect_permanent {
|
|
sub redirect_permanent {
|
|
|
- my ($self, $to) = @_;
|
|
|
|
|
|
|
+ my ( $self, $to ) = @_;
|
|
|
$self->INFO("permanent redirect: $to");
|
|
$self->INFO("permanent redirect: $to");
|
|
|
return [ 301, [ "Location" => $to, "Content-Length" => 0 ], [''] ];
|
|
return [ 301, [ "Location" => $to, "Content-Length" => 0 ], [''] ];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
sub see_also {
|
|
sub see_also {
|
|
|
- my ($self, $to) = @_;
|
|
|
|
|
|
|
+ my ( $self, $to ) = @_;
|
|
|
$self->INFO("see also: $to");
|
|
$self->INFO("see also: $to");
|
|
|
return [ 303, [ "Location" => $to, "Content-Length" => 0 ], [''] ];
|
|
return [ 303, [ "Location" => $to, "Content-Length" => 0 ], [''] ];
|
|
|
}
|
|
}
|
|
@@ -457,36 +468,36 @@ If you need to return these HTTP errors, return these within a route:
|
|
|
=cut
|
|
=cut
|
|
|
|
|
|
|
|
sub notfound {
|
|
sub notfound {
|
|
|
- my ($self, $query, $body) = @_;
|
|
|
|
|
|
|
+ my ( $self, $query, $body ) = @_;
|
|
|
$self->INFO("$query->{method} 404 $query->{fullpath}");
|
|
$self->INFO("$query->{method} 404 $query->{fullpath}");
|
|
|
$body //= 'Not Found';
|
|
$body //= 'Not Found';
|
|
|
return _generic( $body, 404 );
|
|
return _generic( $body, 404 );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
sub forbidden {
|
|
sub forbidden {
|
|
|
- my ($self, $query, $body) = @_;
|
|
|
|
|
|
|
+ my ( $self, $query, $body ) = @_;
|
|
|
$self->INFO("$query->{method} 403 $query->{fullpath}");
|
|
$self->INFO("$query->{method} 403 $query->{fullpath}");
|
|
|
$body //= 'Forbidden';
|
|
$body //= 'Forbidden';
|
|
|
return _generic( $body, 403 );
|
|
return _generic( $body, 403 );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
sub badrequest {
|
|
sub badrequest {
|
|
|
- my ($self, $query, $body) = @_;
|
|
|
|
|
|
|
+ my ( $self, $query, $body ) = @_;
|
|
|
$self->INFO("$query->{method} 400 $query->{fullpath}");
|
|
$self->INFO("$query->{method} 400 $query->{fullpath}");
|
|
|
$body //= 'Bad Request';
|
|
$body //= 'Bad Request';
|
|
|
return _generic( $body, 400 );
|
|
return _generic( $body, 400 );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
sub toolong {
|
|
sub toolong {
|
|
|
- my ($self, $query, $body) = @_;
|
|
|
|
|
|
|
+ my ( $self, $query, $body ) = @_;
|
|
|
$self->INFO("$query->{method} 419 $query->{fullpath}");
|
|
$self->INFO("$query->{method} 419 $query->{fullpath}");
|
|
|
$body //= 'URI too long';
|
|
$body //= 'URI too long';
|
|
|
return _generic( $body, 419 );
|
|
return _generic( $body, 419 );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
sub error {
|
|
sub error {
|
|
|
- my ($self, $query, $body) = @_;
|
|
|
|
|
- my $method = $query->{method} // "?";
|
|
|
|
|
|
|
+ my ( $self, $query, $body ) = @_;
|
|
|
|
|
+ my $method = $query->{method} // "?";
|
|
|
my $fp = $query->{fullpath} // "?";
|
|
my $fp = $query->{fullpath} // "?";
|
|
|
$body //= 'Internal Server Error';
|
|
$body //= 'Internal Server Error';
|
|
|
$self->INFO("$method 500 $fp");
|
|
$self->INFO("$method 500 $fp");
|
|
@@ -494,16 +505,17 @@ sub error {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
sub unavailable {
|
|
sub unavailable {
|
|
|
- my ($self, $query, $body) = @_;
|
|
|
|
|
|
|
+ my ( $self, $query, $body ) = @_;
|
|
|
$self->INFO("$query->{method} 503 $query->{fullpath}");
|
|
$self->INFO("$query->{method} 503 $query->{fullpath}");
|
|
|
$body //= 'Service Unavailable';
|
|
$body //= 'Service Unavailable';
|
|
|
return _generic( $body, 503 );
|
|
return _generic( $body, 503 );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
my $cur_query = {};
|
|
my $cur_query = {};
|
|
|
|
|
+
|
|
|
sub app {
|
|
sub app {
|
|
|
my $self = shift;
|
|
my $self = shift;
|
|
|
- return eval { _app($self, @_) } || do {
|
|
|
|
|
|
|
+ return eval { _app( $self, @_ ) } || do {
|
|
|
my $env = shift;
|
|
my $env = shift;
|
|
|
$env->{'psgi.errors'}->print($@) if $env->{'psgi.errors'};
|
|
$env->{'psgi.errors'}->print($@) if $env->{'psgi.errors'};
|
|
|
|
|
|
|
@@ -516,6 +528,7 @@ sub app {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
my %etags;
|
|
my %etags;
|
|
|
|
|
+
|
|
|
sub _app {
|
|
sub _app {
|
|
|
my $self = shift;
|
|
my $self = shift;
|
|
|
|
|
|
|
@@ -533,11 +546,11 @@ sub _app {
|
|
|
$env->{REQUEST_ID} = $self->request_id(1);
|
|
$env->{REQUEST_ID} = $self->request_id(1);
|
|
|
|
|
|
|
|
# Discard the path used in the log, it's too long and enough 4xx error code = ban
|
|
# Discard the path used in the log, it's too long and enough 4xx error code = ban
|
|
|
- return $self->toolong({ method => $env->{REQUEST_METHOD}, fullpath => '...' }) if length( $env->{REQUEST_URI} ) > 2048;
|
|
|
|
|
|
|
+ return $self->toolong( { method => $env->{REQUEST_METHOD}, fullpath => '...' } ) if length( $env->{REQUEST_URI} ) > 2048;
|
|
|
|
|
|
|
|
# Various stuff important for logging requests
|
|
# Various stuff important for logging requests
|
|
|
my $domain = $env->{HTTP_X_FORWARDED_HOST} || $env->{HTTP_HOST} // eval { Sys::Hostname::hostname() };
|
|
my $domain = $env->{HTTP_X_FORWARDED_HOST} || $env->{HTTP_HOST} // eval { Sys::Hostname::hostname() };
|
|
|
- my $path = $env->{PATH_INFO};
|
|
|
|
|
|
|
+ my $path = $env->{PATH_INFO};
|
|
|
my $port = $env->{HTTP_X_FORWARDED_PORT} // $env->{HTTP_PORT};
|
|
my $port = $env->{HTTP_X_FORWARDED_PORT} // $env->{HTTP_PORT};
|
|
|
my $pport = defined $port ? ":$port" : "";
|
|
my $pport = defined $port ? ":$port" : "";
|
|
|
my $scheme = $env->{'psgi.url_scheme'} // 'http';
|
|
my $scheme = $env->{'psgi.url_scheme'} // 'http';
|
|
@@ -546,22 +559,22 @@ sub _app {
|
|
|
# It's important that we log what the user ACTUALLY requested rather than the rewritten path later on.
|
|
# It's important that we log what the user ACTUALLY requested rather than the rewritten path later on.
|
|
|
my $fullpath = "$scheme://$domain$pport$path";
|
|
my $fullpath = "$scheme://$domain$pport$path";
|
|
|
|
|
|
|
|
- # So we can log it for fail2ban
|
|
|
|
|
- my $ip = $env->{HTTP_X_FORWARDED_FOR} || $env->{REMOTE_ADDR};
|
|
|
|
|
|
|
+ # So we can log it for fail2ban
|
|
|
|
|
+ my $ip = $env->{HTTP_X_FORWARDED_FOR} || $env->{REMOTE_ADDR};
|
|
|
|
|
|
|
|
- # set the referer & ua to go into DB logs, but not logs in general.
|
|
|
|
|
|
|
+ # set the referer & ua to go into DB logs, but not logs in general.
|
|
|
# The referer/ua largely has no importance beyond being a proto bug report for log messages.
|
|
# The referer/ua largely has no importance beyond being a proto bug report for log messages.
|
|
|
my $referer = $env->{HTTP_REFERER};
|
|
my $referer = $env->{HTTP_REFERER};
|
|
|
my $ua = $env->{HTTP_UA};
|
|
my $ua = $env->{HTTP_UA};
|
|
|
|
|
|
|
|
$cur_query = {
|
|
$cur_query = {
|
|
|
- route => $path,
|
|
|
|
|
- fullpath => $path,
|
|
|
|
|
- method => $method,
|
|
|
|
|
- ip => $ip,
|
|
|
|
|
- ua => $ua,
|
|
|
|
|
- referer => $referer,
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ route => $path,
|
|
|
|
|
+ fullpath => $path,
|
|
|
|
|
+ method => $method,
|
|
|
|
|
+ ip => $ip,
|
|
|
|
|
+ ua => $ua,
|
|
|
|
|
+ referer => $referer,
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
# Support aliased paths
|
|
# Support aliased paths
|
|
|
my $aliases = $self->{aliases};
|
|
my $aliases = $self->{aliases};
|
|
@@ -569,7 +582,7 @@ sub _app {
|
|
|
|
|
|
|
|
# Check eTags. If we don't know about it, just assume it's good and lazily fill the cache
|
|
# Check eTags. If we don't know about it, just assume it's good and lazily fill the cache
|
|
|
# XXX yes, this allows cache poisoning...but only for logged in users!
|
|
# XXX yes, this allows cache poisoning...but only for logged in users!
|
|
|
- # This also needs to be IN DB so that we coordinate properly across forks
|
|
|
|
|
|
|
+ # This also needs to be IN DB so that we coordinate properly across forks
|
|
|
if ( $env->{HTTP_IF_NONE_MATCH} ) {
|
|
if ( $env->{HTTP_IF_NONE_MATCH} ) {
|
|
|
$self->INFO("$env->{REQUEST_METHOD} 304 $fullpath");
|
|
$self->INFO("$env->{REQUEST_METHOD} 304 $fullpath");
|
|
|
return [ 304, [], [''] ] if $env->{HTTP_IF_NONE_MATCH} eq ( $etags{ $env->{REQUEST_URI} } || '' );
|
|
return [ 304, [], [''] ] if $env->{HTTP_IF_NONE_MATCH} eq ( $etags{ $env->{REQUEST_URI} } || '' );
|
|
@@ -603,32 +616,33 @@ sub _app {
|
|
|
my $streaming = $env->{'psgi.streaming'};
|
|
my $streaming = $env->{'psgi.streaming'};
|
|
|
|
|
|
|
|
# If we have an actual route, just use it.
|
|
# If we have an actual route, just use it.
|
|
|
- my $r = $self->routes;
|
|
|
|
|
- my $route_index = List::Util::first { ($r->[$_] // '') eq $path } 0..scalar(@$r);
|
|
|
|
|
|
|
+ my $r = $self->routes;
|
|
|
|
|
+ my $route_index = List::Util::first { ( $r->[$_] // '' ) eq $path } 0 .. scalar(@$r);
|
|
|
my $route_actual;
|
|
my $route_actual;
|
|
|
- $route_actual = $r->[$route_index+1] if defined($route_index);
|
|
|
|
|
|
|
+ $route_actual = $r->[ $route_index + 1 ] if defined($route_index);
|
|
|
|
|
|
|
|
# Might be a regexed route. Sort reversed so we try the longest routes first.
|
|
# Might be a regexed route. Sort reversed so we try the longest routes first.
|
|
|
- if (!$route_actual && @$r) {
|
|
|
|
|
|
|
+ if ( !$route_actual && @$r ) {
|
|
|
my $matched = List::Util::first {
|
|
my $matched = List::Util::first {
|
|
|
|
|
|
|
|
# Here's where you want to use Regexp::Debugger in the context of call.pl to debug routes... EX:
|
|
# Here's where you want to use Regexp::Debugger in the context of call.pl to debug routes... EX:
|
|
|
# perl ./call.pl GET /path/to/route
|
|
# perl ./call.pl GET /path/to/route
|
|
|
#print $r->[$_]."\n";
|
|
#print $r->[$_]."\n";
|
|
|
$path =~ m/^$r->[$_]$/;
|
|
$path =~ m/^$r->[$_]$/;
|
|
|
- } 0..scalar(@$r)-1;
|
|
|
|
|
- $route_actual = $r->[$matched+1] if defined($matched);
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ 0 .. scalar(@$r) - 1;
|
|
|
|
|
+ $route_actual = $r->[ $matched + 1 ] if defined($matched);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return $self->route( $route_actual, $env, $fullpath, $path, $start, $last_fetch, $deflate, $domain, $port ) if $route_actual;
|
|
return $self->route( $route_actual, $env, $fullpath, $path, $start, $last_fetch, $deflate, $domain, $port ) if $route_actual;
|
|
|
|
|
|
|
|
# Do a case insensitive match, because osx and windows
|
|
# Do a case insensitive match, because osx and windows
|
|
|
my $file_possible = $self->mangle_filename("www/$path");
|
|
my $file_possible = $self->mangle_filename("www/$path");
|
|
|
- my $file_actual = $file_possible if -f $file_possible;
|
|
|
|
|
|
|
+ my $file_actual = $file_possible if -f $file_possible;
|
|
|
|
|
|
|
|
# Dirindices
|
|
# Dirindices
|
|
|
- if (-d $file_possible) {
|
|
|
|
|
- foreach my $index (@{$self->indices}) {
|
|
|
|
|
|
|
+ if ( -d $file_possible ) {
|
|
|
|
|
+ foreach my $index ( @{ $self->indices } ) {
|
|
|
next if $file_actual;
|
|
next if $file_actual;
|
|
|
my $dirindex = $self->mangle_filename("www$path/$index");
|
|
my $dirindex = $self->mangle_filename("www$path/$index");
|
|
|
$dirindex =~ s|//|/|g;
|
|
$dirindex =~ s|//|/|g;
|
|
@@ -637,16 +651,17 @@ sub _app {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
my $file_is_cgi = appears_executable($file_actual);
|
|
my $file_is_cgi = appears_executable($file_actual);
|
|
|
- return $self->cgi( $env, $fullpath, $file_actual, $last_fetch, $deflate) if $file_is_cgi;
|
|
|
|
|
|
|
+ return $self->cgi( $env, $fullpath, $file_actual, $last_fetch, $deflate ) if $file_is_cgi;
|
|
|
|
|
|
|
|
- $self->INFO("Attempting to serve $fullpath [".($file_possible // "")."]");
|
|
|
|
|
|
|
+ $self->INFO( "Attempting to serve $fullpath [" . ( $file_possible // "" ) . "]" );
|
|
|
my @ranges = parse_ranges($env);
|
|
my @ranges = parse_ranges($env);
|
|
|
return $self->serve( $fullpath, $file_actual, $start, $streaming, \@ranges, $last_fetch, $deflate ) if $file_actual;
|
|
return $self->serve( $fullpath, $file_actual, $start, $streaming, \@ranges, $last_fetch, $deflate ) if $file_actual;
|
|
|
return $self->notfound($cur_query);
|
|
return $self->notfound($cur_query);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
sub mangle_filename {
|
|
sub mangle_filename {
|
|
|
- my ($self,$path) = @_;
|
|
|
|
|
|
|
+ my ( $self, $path ) = @_;
|
|
|
|
|
+
|
|
|
# XXX yes, you can have case sensitive OSX, but basically nobody enables that ever
|
|
# XXX yes, you can have case sensitive OSX, but basically nobody enables that ever
|
|
|
$path = lc($path) if List::Util::any { $^O eq $_ } qw{darwin MSWin32 dos};
|
|
$path = lc($path) if List::Util::any { $^O eq $_ } qw{darwin MSWin32 dos};
|
|
|
$self->DEBUG("Path mangled to $path");
|
|
$self->DEBUG("Path mangled to $path");
|
|
@@ -655,6 +670,7 @@ sub mangle_filename {
|
|
|
|
|
|
|
|
sub parse_ranges {
|
|
sub parse_ranges {
|
|
|
my $env = shift;
|
|
my $env = shift;
|
|
|
|
|
+
|
|
|
# Handle HTTP range/streaming requests
|
|
# Handle HTTP range/streaming requests
|
|
|
my $range = $env->{HTTP_RANGE} || "bytes=0-" if $env->{HTTP_RANGE} || $env->{HTTP_IF_RANGE};
|
|
my $range = $env->{HTTP_RANGE} || "bytes=0-" if $env->{HTTP_RANGE} || $env->{HTTP_IF_RANGE};
|
|
|
|
|
|
|
@@ -663,17 +679,17 @@ sub parse_ranges {
|
|
|
$range =~ s/bytes=//g;
|
|
$range =~ s/bytes=//g;
|
|
|
push(
|
|
push(
|
|
|
@ranges,
|
|
@ranges,
|
|
|
- map {
|
|
|
|
|
- [ split( /-/, $_ ) ];
|
|
|
|
|
- } split( /,/, $range )
|
|
|
|
|
|
|
+ map { [ split( /-/, $_ ) ]; } split( /,/, $range )
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
return @ranges;
|
|
return @ranges;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
my @executable_extensions = qw{cgi sh exe pl php py};
|
|
my @executable_extensions = qw{cgi sh exe pl php py};
|
|
|
|
|
+
|
|
|
sub appears_executable {
|
|
sub appears_executable {
|
|
|
my $subj = shift;
|
|
my $subj = shift;
|
|
|
|
|
+
|
|
|
# Definitely not executable if it does not exist!
|
|
# Definitely not executable if it does not exist!
|
|
|
return 0 unless $subj;
|
|
return 0 unless $subj;
|
|
|
return 0 unless -x $subj;
|
|
return 0 unless -x $subj;
|
|
@@ -685,18 +701,19 @@ sub appears_executable {
|
|
|
|
|
|
|
|
# Handle actual routes
|
|
# Handle actual routes
|
|
|
sub route {
|
|
sub route {
|
|
|
- my ($self, $route, $env, $fullpath, $path, $start, $last_fetch, $deflate, $domain, $port ) = @_;
|
|
|
|
|
|
|
+ my ( $self, $route, $env, $fullpath, $path, $start, $last_fetch, $deflate, $domain, $port ) = @_;
|
|
|
|
|
|
|
|
- $self->DEBUG('Executing route '.$route->{pattern});
|
|
|
|
|
|
|
+ $self->DEBUG( 'Executing route ' . $route->{pattern} );
|
|
|
|
|
|
|
|
- my $content_type = $env->{CONTENT_TYPE} || 'text/html'; # If not set, that's the assumption.
|
|
|
|
|
- # It is the responsibility of each route to respond to HEAD requests correctly
|
|
|
|
|
- return $self->badrequest($cur_query) unless List::Util::any { $_ eq $env->{REQUEST_METHOD} } grep { defined $_ } ('HEAD', $route->{method});
|
|
|
|
|
|
|
+ my $content_type = $env->{CONTENT_TYPE} || 'text/html'; # If not set, that's the assumption.
|
|
|
|
|
+ # It is the responsibility of each route to respond to HEAD requests correctly
|
|
|
|
|
+ return $self->badrequest($cur_query) unless List::Util::any { $_ eq $env->{REQUEST_METHOD} } grep { defined $_ } ( 'HEAD', $route->{method} );
|
|
|
|
|
|
|
|
my $callback;
|
|
my $callback;
|
|
|
- if (exists $route->{callbacks}{'*'}) {
|
|
|
|
|
|
|
+ if ( exists $route->{callbacks}{'*'} ) {
|
|
|
$callback = $route->{callbacks}{'*'};
|
|
$callback = $route->{callbacks}{'*'};
|
|
|
- } else {
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ else {
|
|
|
return $self->badrequest($cur_query) unless exists $route->{callbacks}{$content_type};
|
|
return $self->badrequest($cur_query) unless exists $route->{callbacks}{$content_type};
|
|
|
$callback = $route->{callbacks}{$content_type};
|
|
$callback = $route->{callbacks}{$content_type};
|
|
|
}
|
|
}
|
|
@@ -709,67 +726,68 @@ sub route {
|
|
|
|
|
|
|
|
# Build the query data for passing to a route.
|
|
# Build the query data for passing to a route.
|
|
|
# GET, POST, URI captures, then explicit data overrides.
|
|
# GET, POST, URI captures, then explicit data overrides.
|
|
|
- my $query = $self->extract_query($path, $route, $env);
|
|
|
|
|
|
|
+ my $query = $self->extract_query( $path, $route, $env );
|
|
|
|
|
|
|
|
# allow this stuff to survive down to the end of some routes
|
|
# allow this stuff to survive down to the end of some routes
|
|
|
$query->{last_fetched} = $last_fetch;
|
|
$query->{last_fetched} = $last_fetch;
|
|
|
$query->{deflate} = $deflate;
|
|
$query->{deflate} = $deflate;
|
|
|
$query->{streaming} = $streaming;
|
|
$query->{streaming} = $streaming;
|
|
|
my @ranges = parse_ranges($env);
|
|
my @ranges = parse_ranges($env);
|
|
|
- $query->{ranges} = \@ranges;
|
|
|
|
|
- $query->{start} = $start;
|
|
|
|
|
|
|
+ $query->{ranges} = \@ranges;
|
|
|
|
|
+ $query->{start} = $start;
|
|
|
|
|
+
|
|
|
# Put things to tv_interval in here for Server-Timing
|
|
# Put things to tv_interval in here for Server-Timing
|
|
|
- $query->{fullpath} = $fullpath;
|
|
|
|
|
- $query->{method} = $route->{method};
|
|
|
|
|
- $query->{route} = $path;
|
|
|
|
|
- $query->{cookies} = $env->{HTTP_COOKIE};
|
|
|
|
|
- $query->{dnt} = $env->{HTTP_DNT};
|
|
|
|
|
- $query->{nosellinfo} = $env->{HTTP_SEC_GPC};
|
|
|
|
|
- $query->{port} = $port;
|
|
|
|
|
- $query->{scheme} = $env->{'psgi.url_scheme'} // 'http';
|
|
|
|
|
- $query->{method} = $env->{REQUEST_METHOD};
|
|
|
|
|
- $query->{lang} = $env->{HTTP_ACCEPT_LANGUAGE};
|
|
|
|
|
- $query->{accept} = $env->{HTTP_ACCEPT};
|
|
|
|
|
- $query->{has_query} = !!$env->{QUERY_STRING};
|
|
|
|
|
- $query->{domain} = $domain;
|
|
|
|
|
- $query->{dispatcher} = $route;
|
|
|
|
|
- $query->{ip} = $cur_query->{ip};
|
|
|
|
|
- $query->{ua} = $cur_query->{ua};
|
|
|
|
|
- $query->{referer} = $cur_query->{referer};
|
|
|
|
|
|
|
+ $query->{fullpath} = $fullpath;
|
|
|
|
|
+ $query->{method} = $route->{method};
|
|
|
|
|
+ $query->{route} = $path;
|
|
|
|
|
+ $query->{cookies} = $env->{HTTP_COOKIE};
|
|
|
|
|
+ $query->{dnt} = $env->{HTTP_DNT};
|
|
|
|
|
+ $query->{nosellinfo} = $env->{HTTP_SEC_GPC};
|
|
|
|
|
+ $query->{port} = $port;
|
|
|
|
|
+ $query->{scheme} = $env->{'psgi.url_scheme'} // 'http';
|
|
|
|
|
+ $query->{method} = $env->{REQUEST_METHOD};
|
|
|
|
|
+ $query->{lang} = $env->{HTTP_ACCEPT_LANGUAGE};
|
|
|
|
|
+ $query->{accept} = $env->{HTTP_ACCEPT};
|
|
|
|
|
+ $query->{has_query} = !!$env->{QUERY_STRING};
|
|
|
|
|
+ $query->{domain} = $domain;
|
|
|
|
|
+ $query->{dispatcher} = $route;
|
|
|
|
|
+ $query->{ip} = $cur_query->{ip};
|
|
|
|
|
+ $query->{ua} = $cur_query->{ua};
|
|
|
|
|
+ $query->{referer} = $cur_query->{referer};
|
|
|
|
|
|
|
|
# This allows for better error handlers if we die in the route.
|
|
# This allows for better error handlers if we die in the route.
|
|
|
$cur_query = $query;
|
|
$cur_query = $query;
|
|
|
|
|
|
|
|
# Setup the CGI vars they expect IF requested
|
|
# Setup the CGI vars they expect IF requested
|
|
|
- local %ENV = (%ENV, CGI::Emulate::PSGI->emulate_environment($env)) if $route->{env};
|
|
|
|
|
|
|
+ local %ENV = ( %ENV, CGI::Emulate::PSGI->emulate_environment($env) ) if $route->{env};
|
|
|
|
|
|
|
|
{
|
|
{
|
|
|
- my $output = $callback->($self, $query);
|
|
|
|
|
|
|
+ my $output = $callback->( $self, $query );
|
|
|
|
|
|
|
|
# If it's streaming, just hand it off.
|
|
# If it's streaming, just hand it off.
|
|
|
- # It's up to the caller to handle things like running post-close callbacks in this event.
|
|
|
|
|
|
|
+ # It's up to the caller to handle things like running post-close callbacks in this event.
|
|
|
return $output if ref $output eq 'CODE';
|
|
return $output if ref $output eq 'CODE';
|
|
|
|
|
|
|
|
die "$fullpath returned no or malformed data!" unless ref $output eq 'ARRAY' && @$output == 3;
|
|
die "$fullpath returned no or malformed data!" unless ref $output eq 'ARRAY' && @$output == 3;
|
|
|
|
|
|
|
|
- my $pport = defined $query->{port} ? ":$query->{port}" : "";
|
|
|
|
|
- my %headers = @{$output->[1]};
|
|
|
|
|
- my $bytes = $headers{'Content-Length'} // '?';
|
|
|
|
|
|
|
+ my $pport = defined $query->{port} ? ":$query->{port}" : "";
|
|
|
|
|
+ my %headers = @{ $output->[1] };
|
|
|
|
|
+ my $bytes = $headers{'Content-Length'} // '?';
|
|
|
$self->INFO("$env->{REQUEST_METHOD} $output->[0] $bytes $fullpath");
|
|
$self->INFO("$env->{REQUEST_METHOD} $output->[0] $bytes $fullpath");
|
|
|
|
|
|
|
|
# Append server-timing headers if they aren't present
|
|
# Append server-timing headers if they aren't present
|
|
|
my $tot = tv_interval($start) * 1000;
|
|
my $tot = tv_interval($start) * 1000;
|
|
|
push( @{ $output->[1] }, 'Server-Timing' => "app;dur=$tot" ) unless List::Util::any { $_ eq 'Server-Timing' } @{ $output->[1] };
|
|
push( @{ $output->[1] }, 'Server-Timing' => "app;dur=$tot" ) unless List::Util::any { $_ eq 'Server-Timing' } @{ $output->[1] };
|
|
|
|
|
|
|
|
- # In the event that we have post-close callbacks, go ahead and run them.
|
|
|
|
|
- return $self->stream_raw_psgi($output, $query) if @{$self->{callbacks}};
|
|
|
|
|
|
|
+ # In the event that we have post-close callbacks, go ahead and run them.
|
|
|
|
|
+ return $self->stream_raw_psgi( $output, $query ) if @{ $self->{callbacks} };
|
|
|
|
|
|
|
|
return $output;
|
|
return $output;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
sub extract_query {
|
|
sub extract_query {
|
|
|
- my ($self, $path, $route, $env) = @_;
|
|
|
|
|
|
|
+ my ( $self, $path, $route, $env ) = @_;
|
|
|
|
|
|
|
|
my $query = URL::Encode::url_params_mixed( $env->{QUERY_STRING} ) if $env->{QUERY_STRING};
|
|
my $query = URL::Encode::url_params_mixed( $env->{QUERY_STRING} ) if $env->{QUERY_STRING};
|
|
|
|
|
|
|
@@ -785,13 +803,13 @@ sub extract_query {
|
|
|
@$query{ keys( %{ $body->upload } ) } = values( %{ $body->upload } );
|
|
@$query{ keys( %{ $body->upload } ) } = values( %{ $body->upload } );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (ref $route->{captures} eq 'ARRAY') {
|
|
|
|
|
- my @captures = $path =~ m/^$route->{pattern}$/;
|
|
|
|
|
- @$query{@{$route->{captures}}} = @captures;
|
|
|
|
|
|
|
+ if ( ref $route->{captures} eq 'ARRAY' ) {
|
|
|
|
|
+ my @captures = $path =~ m/^$route->{pattern}$/;
|
|
|
|
|
+ @$query{ @{ $route->{captures} } } = @captures;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (ref $route->{data} eq 'HASH') {
|
|
|
|
|
- @$query{keys(%{$route->{data}})} = values(%{$route->{data}});
|
|
|
|
|
|
|
+ if ( ref $route->{data} eq 'HASH' ) {
|
|
|
|
|
+ @$query{ keys( %{ $route->{data} } ) } = values( %{ $route->{data} } );
|
|
|
}
|
|
}
|
|
|
return $query;
|
|
return $query;
|
|
|
}
|
|
}
|
|
@@ -799,14 +817,14 @@ sub extract_query {
|
|
|
# Read until we are done with headers, then pass off the filehandle to PSGI for streaming.
|
|
# Read until we are done with headers, then pass off the filehandle to PSGI for streaming.
|
|
|
# TODO support compression
|
|
# TODO support compression
|
|
|
sub cgi {
|
|
sub cgi {
|
|
|
- my ( $self, $env, $fullpath, $file_actual, $last_fetch, $deflate) = @_;
|
|
|
|
|
|
|
+ my ( $self, $env, $fullpath, $file_actual, $last_fetch, $deflate ) = @_;
|
|
|
$self->INFO("Handoff $fullpath to $file_actual for cgi exec");
|
|
$self->INFO("Handoff $fullpath to $file_actual for cgi exec");
|
|
|
|
|
|
|
|
# Setup the CGI vars they expect
|
|
# Setup the CGI vars they expect
|
|
|
- local %ENV = (%ENV, CGI::Emulate::PSGI->emulate_environment($env));
|
|
|
|
|
|
|
+ local %ENV = ( %ENV, CGI::Emulate::PSGI->emulate_environment($env) );
|
|
|
|
|
|
|
|
- my $pid = open(my $out, '-|', "$file_actual");
|
|
|
|
|
- my ($code, $offset, %headers) = extract_headers($out, $last_fetch);
|
|
|
|
|
|
|
+ my $pid = open( my $out, '-|', "$file_actual" );
|
|
|
|
|
+ my ( $code, $offset, %headers ) = extract_headers( $out, $last_fetch );
|
|
|
$code //= 500;
|
|
$code //= 500;
|
|
|
return sub {
|
|
return sub {
|
|
|
my $responder = shift;
|
|
my $responder = shift;
|
|
@@ -816,17 +834,18 @@ sub cgi {
|
|
|
$writer->write($buf);
|
|
$writer->write($buf);
|
|
|
}
|
|
}
|
|
|
$writer->close;
|
|
$writer->close;
|
|
|
|
|
+
|
|
|
# Wait on the CGI script to do whatever it's doing after closing stdout
|
|
# Wait on the CGI script to do whatever it's doing after closing stdout
|
|
|
close $out;
|
|
close $out;
|
|
|
- waitpid($pid, 0);
|
|
|
|
|
|
|
+ waitpid( $pid, 0 );
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
# Used primarily when we have post-close callbacks
|
|
# Used primarily when we have post-close callbacks
|
|
|
sub stream_raw_psgi {
|
|
sub stream_raw_psgi {
|
|
|
- my ($self, $response, $data) = @_;
|
|
|
|
|
|
|
+ my ( $self, $response, $data ) = @_;
|
|
|
|
|
|
|
|
- my $time_to_here = tv_interval($data->{start});
|
|
|
|
|
|
|
+ my $time_to_here = tv_interval( $data->{start} );
|
|
|
$self->DEBUG("Routing took $time_to_here s");
|
|
$self->DEBUG("Routing took $time_to_here s");
|
|
|
my $post_routing = [gettimeofday];
|
|
my $post_routing = [gettimeofday];
|
|
|
|
|
|
|
@@ -834,36 +853,36 @@ sub stream_raw_psgi {
|
|
|
|
|
|
|
|
print $fh "HTTP/1.1 $response->[0]\n";
|
|
print $fh "HTTP/1.1 $response->[0]\n";
|
|
|
|
|
|
|
|
- # Emit the rest of the headers
|
|
|
|
|
- foreach (my $i=0; $i < @{$response->[1]}; $i += 2) {
|
|
|
|
|
- my $header = $response->[1][$i];
|
|
|
|
|
- my $value = $response->[1][$i+1];
|
|
|
|
|
- print $fh "$header: $value\n";
|
|
|
|
|
- }
|
|
|
|
|
- print $fh "\n";
|
|
|
|
|
|
|
+ # Emit the rest of the headers
|
|
|
|
|
+ foreach ( my $i = 0; $i < @{ $response->[1] }; $i += 2 ) {
|
|
|
|
|
+ my $header = $response->[1][$i];
|
|
|
|
|
+ my $value = $response->[1][ $i + 1 ];
|
|
|
|
|
+ print $fh "$header: $value\n";
|
|
|
|
|
+ }
|
|
|
|
|
+ print $fh "\n";
|
|
|
|
|
|
|
|
- # Emit the body
|
|
|
|
|
- print $fh $response->[2][0];
|
|
|
|
|
|
|
+ # Emit the body
|
|
|
|
|
+ print $fh $response->[2][0];
|
|
|
|
|
|
|
|
close($fh);
|
|
close($fh);
|
|
|
|
|
|
|
|
- $self->DEBUG("Response written in ".(tv_interval($post_routing))." s");
|
|
|
|
|
- while( my $callback = shift @{$self->{callbacks}}) {
|
|
|
|
|
- if (ref $callback eq 'CODE') {
|
|
|
|
|
- my $post_start = [gettimeofday];
|
|
|
|
|
- local $@;
|
|
|
|
|
- eval { $callback->() };
|
|
|
|
|
- $self->ERROR("Post-close callback encountered exception: $@") if $@;
|
|
|
|
|
- $self->DEBUG("Post-close callback took ".(tv_interval($post_start))." s");
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ $self->DEBUG( "Response written in " . ( tv_interval($post_routing) ) . " s" );
|
|
|
|
|
+ while ( my $callback = shift @{ $self->{callbacks} } ) {
|
|
|
|
|
+ if ( ref $callback eq 'CODE' ) {
|
|
|
|
|
+ my $post_start = [gettimeofday];
|
|
|
|
|
+ local $@;
|
|
|
|
|
+ eval { $callback->() };
|
|
|
|
|
+ $self->ERROR("Post-close callback encountered exception: $@") if $@;
|
|
|
|
|
+ $self->DEBUG( "Post-close callback took " . ( tv_interval($post_start) ) . " s" );
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
exit 0;
|
|
exit 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
sub stream_raw_http {
|
|
sub stream_raw_http {
|
|
|
- my ($self, $data, $last_fetch, $to_fork, $callback, $error_handler) = @_;
|
|
|
|
|
|
|
+ my ( $self, $data, $last_fetch, $to_fork, $callback, $error_handler ) = @_;
|
|
|
|
|
|
|
|
- my $time_to_here = tv_interval($data->{start});
|
|
|
|
|
|
|
+ my $time_to_here = tv_interval( $data->{start} );
|
|
|
$self->DEBUG("Routing took $time_to_here s");
|
|
$self->DEBUG("Routing took $time_to_here s");
|
|
|
my $post_routing = [gettimeofday];
|
|
my $post_routing = [gettimeofday];
|
|
|
|
|
|
|
@@ -872,43 +891,47 @@ sub stream_raw_http {
|
|
|
# The CGIs you are executing here SHOULD NOT emit a status line.
|
|
# The CGIs you are executing here SHOULD NOT emit a status line.
|
|
|
# Most apache CGIs you find don't anyways, so this is usually not a big deal.
|
|
# Most apache CGIs you find don't anyways, so this is usually not a big deal.
|
|
|
print $fh "HTTP/1.1 200 OK\n";
|
|
print $fh "HTTP/1.1 200 OK\n";
|
|
|
|
|
+
|
|
|
# Emit our server-timing header.
|
|
# Emit our server-timing header.
|
|
|
- print $fh "Server-Timing: ".$self->build_server_timing($data)."\n"; #app;dur=".($time_to_here*1000)."\n";
|
|
|
|
|
|
|
+ print $fh "Server-Timing: " . $self->build_server_timing($data) . "\n"; #app;dur=".($time_to_here*1000)."\n";
|
|
|
|
|
|
|
|
# We *must* fork because we can't rely on the child to close stdout.
|
|
# We *must* fork because we can't rely on the child to close stdout.
|
|
|
- my $pid = _fork(sub {
|
|
|
|
|
- $self->DEBUG("Forking child took".(tv_interval($post_routing))." s");
|
|
|
|
|
- local $@;
|
|
|
|
|
- eval { $to_fork->() };
|
|
|
|
|
- $self->ERROR("Raw HTTTP streaming encountered exception: $@") if $@;
|
|
|
|
|
- if (ref $error_handler eq 'CODE') {
|
|
|
|
|
|
|
+ my $pid = _fork(
|
|
|
|
|
+ sub {
|
|
|
|
|
+ $self->DEBUG( "Forking child took" . ( tv_interval($post_routing) ) . " s" );
|
|
|
local $@;
|
|
local $@;
|
|
|
- eval { $error_handler->() };
|
|
|
|
|
- $self->ERROR("HTTP streaming error handler encountered exception: $@") if $@;
|
|
|
|
|
- }
|
|
|
|
|
- }, $fh);
|
|
|
|
|
|
|
+ eval { $to_fork->() };
|
|
|
|
|
+ $self->ERROR("Raw HTTTP streaming encountered exception: $@") if $@;
|
|
|
|
|
+ if ( ref $error_handler eq 'CODE' ) {
|
|
|
|
|
+ local $@;
|
|
|
|
|
+ eval { $error_handler->() };
|
|
|
|
|
+ $self->ERROR("HTTP streaming error handler encountered exception: $@") if $@;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ $fh
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
- waitpid($pid, 0);
|
|
|
|
|
|
|
+ waitpid( $pid, 0 );
|
|
|
close($fh);
|
|
close($fh);
|
|
|
|
|
|
|
|
- $self->DEBUG("Response written in ".(tv_interval($post_routing))." s");
|
|
|
|
|
- if (ref $callback eq 'CODE') {
|
|
|
|
|
|
|
+ $self->DEBUG( "Response written in " . ( tv_interval($post_routing) ) . " s" );
|
|
|
|
|
+ if ( ref $callback eq 'CODE' ) {
|
|
|
my $post_start = [gettimeofday];
|
|
my $post_start = [gettimeofday];
|
|
|
local $@;
|
|
local $@;
|
|
|
eval { $callback->() };
|
|
eval { $callback->() };
|
|
|
$self->ERROR("Post-close callback encountered exception: $@") if $@;
|
|
$self->ERROR("Post-close callback encountered exception: $@") if $@;
|
|
|
- $self->DEBUG("Post-close callback took ".(tv_interval($post_start))." s");
|
|
|
|
|
|
|
+ $self->DEBUG( "Post-close callback took " . ( tv_interval($post_start) ) . " s" );
|
|
|
}
|
|
}
|
|
|
exit 0;
|
|
exit 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
sub _fork {
|
|
sub _fork {
|
|
|
- my ($callback, $fh) = @_;
|
|
|
|
|
|
|
+ my ( $callback, $fh ) = @_;
|
|
|
select $fh;
|
|
select $fh;
|
|
|
- $|=1;
|
|
|
|
|
|
|
+ $| = 1;
|
|
|
my $pid = fork();
|
|
my $pid = fork();
|
|
|
die "Could not fork child" unless defined $pid;
|
|
die "Could not fork child" unless defined $pid;
|
|
|
- if ($pid == 0) {
|
|
|
|
|
|
|
+ if ( $pid == 0 ) {
|
|
|
$callback->();
|
|
$callback->();
|
|
|
exit 0;
|
|
exit 0;
|
|
|
}
|
|
}
|
|
@@ -925,7 +948,7 @@ Returns a code and parsed headers hash. Code will be 304 if the file hasn't cha
|
|
|
=cut
|
|
=cut
|
|
|
|
|
|
|
|
sub extract_headers {
|
|
sub extract_headers {
|
|
|
- my ($fh, $last_fetch, $is_ref) = @_;
|
|
|
|
|
|
|
+ my ( $fh, $last_fetch, $is_ref ) = @_;
|
|
|
my $headers = '';
|
|
my $headers = '';
|
|
|
|
|
|
|
|
# NOTE: this is relying on while advancing the file pointer
|
|
# NOTE: this is relying on while advancing the file pointer
|
|
@@ -933,21 +956,22 @@ sub extract_headers {
|
|
|
last if $_ eq "\n";
|
|
last if $_ eq "\n";
|
|
|
$headers .= $_;
|
|
$headers .= $_;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
#XXX still a toctou
|
|
#XXX still a toctou
|
|
|
my $offset = $fh->tell();
|
|
my $offset = $fh->tell();
|
|
|
my ( undef, undef, $status, undef, $headers_parsed ) = HTTP::Parser::XS::parse_http_response( "$headers\n", HEADERS_AS_HASHREF );
|
|
my ( undef, undef, $status, undef, $headers_parsed ) = HTTP::Parser::XS::parse_http_response( "$headers\n", HEADERS_AS_HASHREF );
|
|
|
|
|
|
|
|
my $code = $status // 200;
|
|
my $code = $status // 200;
|
|
|
$headers_parsed //= {};
|
|
$headers_parsed //= {};
|
|
|
- if (!$is_ref) {
|
|
|
|
|
|
|
+ if ( !$is_ref ) {
|
|
|
my $mt = ( stat($fh) )[9];
|
|
my $mt = ( stat($fh) )[9];
|
|
|
my @gm = gmtime($mt);
|
|
my @gm = gmtime($mt);
|
|
|
my $now_string = strftime( "%a, %d %b %Y %H:%M:%S GMT", @gm );
|
|
my $now_string = strftime( "%a, %d %b %Y %H:%M:%S GMT", @gm );
|
|
|
- $code = $mt > $last_fetch ? $status : 304;
|
|
|
|
|
|
|
+ $code = $mt > $last_fetch ? $status : 304;
|
|
|
$headers_parsed->{"Last-Modified"} = $now_string;
|
|
$headers_parsed->{"Last-Modified"} = $now_string;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return ($code, $offset, %$headers_parsed);
|
|
|
|
|
|
|
+ return ( $code, $offset, %$headers_parsed );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
sub static {
|
|
sub static {
|
|
@@ -957,8 +981,8 @@ sub static {
|
|
|
|
|
|
|
|
# XXX because of psgi I can't just vomit the file directly
|
|
# XXX because of psgi I can't just vomit the file directly
|
|
|
if ( open( my $fh, '<', "statics/$path" ) ) {
|
|
if ( open( my $fh, '<', "statics/$path" ) ) {
|
|
|
- my ($code, $offset, $headers_parsed) = (200, undef, {});
|
|
|
|
|
- ($code, $offset, %$headers_parsed) = extract_headers($fh, $last_fetch );
|
|
|
|
|
|
|
+ my ( $code, $offset, $headers_parsed ) = ( 200, undef, {} );
|
|
|
|
|
+ ( $code, $offset, %$headers_parsed ) = extract_headers( $fh, $last_fetch );
|
|
|
|
|
|
|
|
# Append server-timing headers
|
|
# Append server-timing headers
|
|
|
my $tot = tv_interval($start) * 1000;
|
|
my $tot = tv_interval($start) * 1000;
|
|
@@ -985,7 +1009,7 @@ sub static {
|
|
|
|
|
|
|
|
return [ $code, [%$headers_parsed], $fh ];
|
|
return [ $code, [%$headers_parsed], $fh ];
|
|
|
}
|
|
}
|
|
|
- return $self->forbidden($self->{current_query});
|
|
|
|
|
|
|
+ return $self->forbidden( $self->{current_query} );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
sub get_config {
|
|
sub get_config {
|
|
@@ -1002,17 +1026,17 @@ sub get_config {
|
|
|
basedir => $ENV{'HOME'},
|
|
basedir => $ENV{'HOME'},
|
|
|
);
|
|
);
|
|
|
my $config_file = "$ENV{HOME}/.tpsgi.ini";
|
|
my $config_file = "$ENV{HOME}/.tpsgi.ini";
|
|
|
- if (-f $config_file) {
|
|
|
|
|
|
|
+ if ( -f $config_file ) {
|
|
|
my $conf = Config::Simple->new($config_file);
|
|
my $conf = Config::Simple->new($config_file);
|
|
|
my %config;
|
|
my %config;
|
|
|
- %config = %{$conf->param(-block => 'default')} if $conf;
|
|
|
|
|
|
|
+ %config = %{ $conf->param( -block => 'default' ) } if $conf;
|
|
|
|
|
|
|
|
# Merge the configuration with the options
|
|
# Merge the configuration with the options
|
|
|
- foreach my $opt (keys(%options)) {
|
|
|
|
|
|
|
+ foreach my $opt ( keys(%options) ) {
|
|
|
if ( ref $options{$opt} eq 'ARRAY' ) {
|
|
if ( ref $options{$opt} eq 'ARRAY' ) {
|
|
|
next unless exists $config{$opt};
|
|
next unless exists $config{$opt};
|
|
|
- my @arrayed = ref $config{$opt} eq 'ARRAY' ? @{$config{$opt}} : ($config{$opt});
|
|
|
|
|
- push(@{$options{$opt}}, @arrayed);
|
|
|
|
|
|
|
+ my @arrayed = ref $config{$opt} eq 'ARRAY' ? @{ $config{$opt} } : ( $config{$opt} );
|
|
|
|
|
+ push( @{ $options{$opt} }, @arrayed );
|
|
|
next;
|
|
next;
|
|
|
}
|
|
}
|
|
|
$options{$opt} = $config{$opt} if exists $config{$opt};
|
|
$options{$opt} = $config{$opt} if exists $config{$opt};
|
|
@@ -1023,28 +1047,28 @@ sub get_config {
|
|
|
|
|
|
|
|
# Convenience method to keep track of server-timing
|
|
# Convenience method to keep track of server-timing
|
|
|
sub checkpoint {
|
|
sub checkpoint {
|
|
|
- my ($self, $data, $name) = @_;
|
|
|
|
|
- $data->{checkpoints} //= [[ start => $data->{start} ]];
|
|
|
|
|
- push(@{$data->{checkpoints}}, [ $name => [gettimeofday] ]);
|
|
|
|
|
|
|
+ my ( $self, $data, $name ) = @_;
|
|
|
|
|
+ $data->{checkpoints} //= [ [ start => $data->{start} ] ];
|
|
|
|
|
+ push( @{ $data->{checkpoints} }, [ $name => [gettimeofday] ] );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
sub build_server_timing {
|
|
sub build_server_timing {
|
|
|
- my ($self, $data) = @_;
|
|
|
|
|
|
|
+ my ( $self, $data ) = @_;
|
|
|
my @st;
|
|
my @st;
|
|
|
my $last_interval;
|
|
my $last_interval;
|
|
|
- foreach my $checkpoint (@{$data->{checkpoints}}) {
|
|
|
|
|
|
|
+ foreach my $checkpoint ( @{ $data->{checkpoints} } ) {
|
|
|
if ($last_interval) {
|
|
if ($last_interval) {
|
|
|
- my $dur = tv_interval($last_interval, $checkpoint->[1]) * 1000;
|
|
|
|
|
- push(@st, "$checkpoint->[0];dur=$dur");
|
|
|
|
|
|
|
+ my $dur = tv_interval( $last_interval, $checkpoint->[1] ) * 1000;
|
|
|
|
|
+ push( @st, "$checkpoint->[0];dur=$dur" );
|
|
|
}
|
|
}
|
|
|
$last_interval = $checkpoint->[1];
|
|
$last_interval = $checkpoint->[1];
|
|
|
}
|
|
}
|
|
|
- my $tot = tv_interval($data->{start}) * 1000;
|
|
|
|
|
- push(@st, "tot;dur=$tot");
|
|
|
|
|
|
|
+ my $tot = tv_interval( $data->{start} ) * 1000;
|
|
|
|
|
+ push( @st, "tot;dur=$tot" );
|
|
|
|
|
|
|
|
delete $data->{checkpoints};
|
|
delete $data->{checkpoints};
|
|
|
|
|
|
|
|
- return join(', ', @st);
|
|
|
|
|
|
|
+ return join( ', ', @st );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
=head2 add_post_close_callback()
|
|
=head2 add_post_close_callback()
|
|
@@ -1054,8 +1078,8 @@ Add a thing to do after closing stdout; useful for housekeeping that doesn't nee
|
|
|
=cut
|
|
=cut
|
|
|
|
|
|
|
|
sub add_post_close_callback {
|
|
sub add_post_close_callback {
|
|
|
- my ($self, $cb) = @_;
|
|
|
|
|
- push(@{$self->{callbacks}}, $cb);
|
|
|
|
|
|
|
+ my ( $self, $cb ) = @_;
|
|
|
|
|
+ push( @{ $self->{callbacks} }, $cb );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
=head2 signal_restart_parent()
|
|
=head2 signal_restart_parent()
|
|
@@ -1065,14 +1089,14 @@ Instruct tPSGI to reload after closing stdout.
|
|
|
=cut
|
|
=cut
|
|
|
|
|
|
|
|
sub signal_restart_parent {
|
|
sub signal_restart_parent {
|
|
|
- my ($self) = @_;
|
|
|
|
|
- $self->add_post_close_callback(\&_restart_parent);
|
|
|
|
|
|
|
+ my ($self) = @_;
|
|
|
|
|
+ $self->add_post_close_callback( \&_restart_parent );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
# Instruct the parent to restart. Normally this is HUP, but nginx-unit decides to be special.
|
|
# Instruct the parent to restart. Normally this is HUP, but nginx-unit decides to be special.
|
|
|
# Don't do anything if running NOHUP=1, which is useful when doing bulk operations
|
|
# Don't do anything if running NOHUP=1, which is useful when doing bulk operations
|
|
|
sub _restart_parent {
|
|
sub _restart_parent {
|
|
|
- my $parent = shift;
|
|
|
|
|
|
|
+ my $parent = shift;
|
|
|
return if $ENV{NOHUP};
|
|
return if $ENV{NOHUP};
|
|
|
|
|
|
|
|
if ( $ENV{PSGI_ENGINE} && $ENV{PSGI_ENGINE} eq 'nginx-unit' ) {
|
|
if ( $ENV{PSGI_ENGINE} && $ENV{PSGI_ENGINE} eq 'nginx-unit' ) {
|