#!/usr/bin/perl # Standalone HTTP server for running Bugzilla based on HTTP::Server::Simple and Net::Server # USAGE: perl HTTPServerSimple.pl [--option=value] [bugzilla.conf] # See bugzilla.conf sample in the end of this file use strict; BEGIN { require File::Basename; my $dir = File::Basename::dirname($0); ($dir) = $dir =~ /^(.*)$/s; chdir($dir); $Bugzilla::HTTPServerSimple::DOCROOT = $dir; } use lib qw(.); use CGI (); CGI->compile(qw(:cgi -no_xhtml -nph -oldstyle_urls :private_tempfiles :unique_headers SERVER_PUSH :push)); $CGI::USE_PARAM_SEMICOLONS = 0; # Fake exit() function to only terminate current request my $in_eval = 0; *CORE::GLOBAL::exit = sub { if ($in_eval) { die bless { rc => shift }, 'Bugzilla::HTTPServerSimple::FakeExit'; } else { CORE::exit(@_); } }; $SIG{INT} = sub { warn "Terminating"; CORE::exit(); }; # Create and run my @args = @ARGV; @ARGV = (); my $server = Bugzilla::HTTPServerSimple->new(@args); $server->run(); # HTTP::Server::Simple subclass (the real server) package Bugzilla::HTTPServerSimple; use Bugzilla; use Bugzilla::Util qw(html_quote); use Time::HiRes qw(gettimeofday tv_interval); use Sys::Sendfile qw(sendfile); use POSIX qw(strftime); use LWP::MediaTypes qw(guess_media_type); use base qw(HTTP::Server::Simple::CGI); use constant DEFAULT_CONFIG => ( class => 'Net::Server::PreFork', port => '127.0.0.1:8157', min_servers => 4, max_servers => 20, min_spare_servers => 4, max_spare_servers => 8, max_requests => 1000, user => POSIX::geteuid(), group => POSIX::getegid(), log_file => '/var/log/bugzilla.log', log_level => 2, pid_file => '/var/run/bugzilla.pid', background => 1, deny_regexp => '^(localconfig|data/(?!webdot/)|.*\.(pm|pl|sh)($|\?)|(/|^)(CVS|\.(ht|svn|hg|bzr|git)).*)', preload => '*.cgi', reload => 1, ); # Code cache my %subs = (); my %mtime = (); sub new { my ($class, @args) = @_; my $self = HTTP::Server::Simple::new($class); my $cmdline = []; my $series = []; my $nextvalue = 0; for (@args) { if (/^--([^=]+)(?:=(.*))?/s) { push @$cmdline, 1 if $nextvalue; $nextvalue = !$2; push @$cmdline, $1; } elsif ($nextvalue) { push @$cmdline, $_; $nextvalue = 0; } else { push @$series, Bugzilla::NetServerConfigParser->_read_conf($_); } } push @$cmdline, 1 if $nextvalue; push @$series, $cmdline; unshift @$series, [ DEFAULT_CONFIG() ]; my $r = {}; for my $array (@$series) { my $cfg = {}; for (my $i = 0; $i < @$array; $i += 2) { push @{$cfg->{$array->[$i]}}, $array->[$i+1]; } $r = { %$r, %$cfg }; } # Preload scripts (before transforming $r) for my $script (@{$r->{preload} || []}) { for (glob $script) { ($_) = /^(.*)$/so; eval { $self->load_script($_); }; if ($@ && ref $@ eq 'Bugzilla::HTTPServerSimple::FakeExit') { print STDERR "Preloading $_ failed: $@->{error}->[2]\n"; } } } $self->{_config} = []; for my $k (keys %$r) { push @{$self->{_config}}, map { $k => $_ } @{$r->{$k}}; ($r->{$k}) = @{$r->{$k}} if @{$r->{$k}} < 2; } if ($r->{reload}) { # Enable reload support $^P |= 0x10; } $self->{_config_hash} = $r; return $self; } sub run { my $self = shift; $self->SUPER::run(@{$self->{_config}}); } sub port { my $self = shift; return $self->{_config_hash}->{port}; } # Returns Net::Server subclass to run under sub net_server { my $self = shift; return $self->{_config_hash}->{class}; } # Format an error in HTML sub print_error { my $self = shift; my ($status_code, $status_line, $error_text) = @_; print STDERR strftime("[%Y-%m-%d %H:%M:%S] ", localtime) . $error_text . "\n"; print $ENV{SERVER_PROTOCOL}." $status_code $status_line\r\nContent-Type: text/html\r\n\r\n". "$status_line". "

$status_line

".html_quote($error_text). "


".$ENV{SERVER_SOFTWARE}."

"; return $status_code; } # Abort request with error sub throw { my $self = shift; die bless { error => [ @_ ] }, 'Bugzilla::HTTPServerSimple::FakeExit'; } # Print a "Not found" error sub not_found { my $self = shift; my ($script) = @_; $self->throw(404, 'Not Found', "The requested URL $script was not found on this server."); } # Print an "Internal Server Error" sub internal_error { my $self = shift; my ($text) = @_; $self->throw(500, 'Internal Server Error', $text); } # Load CGI script from file sub get_script { my $self = shift; my ($script, $for_require) = @_; my $fd; if (!open $fd, "<$script") { $self->not_found($script); } # Reload Bugzilla.pm for old versions on each request my $preload = Bugzilla->can('request_cache') ? '' : "delete \$INC{'Bugzilla.pm'}; require 'Bugzilla.pm';"; my $content; local $/ = undef; $content = <$fd>; close $fd; # untaint ($content) = $content =~ /^(.*)$/s; $content =~ s/\n__END__.*/\n/s; $content = "\n#line 1 \"$script\"\n$content"; my $package = lc $script; $package =~ s/^(\W)/'x'.unpack('H*', $1)/es; $package =~ s/(\W)/unpack('H*', $1)/ges; $content = $for_require ? "$preload package Bugzilla::$package; $content" : "package Bugzilla::$package; sub { $preload$content }"; return $content; } # Load script sub load_script { my $self = shift; my ($script) = @_; my $m; if (!$subs{$script} || ($self->{_config_hash}->{reload} && ($m = [stat $script]->[9] || 0) > $mtime{$script})) { my $content = $self->get_script($script); $subs{$script} = eval $content; $mtime{$script} = $m; if ($@) { $self->internal_error("Error while loading $script:\n$@"); } } } # Simple "FastCGI" implementation - cache *.cgi in subs sub run_script { my $self = shift; my ($script) = @_; $self->load_script($script); my $start = [gettimeofday]; $Bugzilla::Error::IN_EVAL++; $in_eval = 1; eval { &{$subs{$script}}(); }; $self->check_errors($script); my $elapsed = tv_interval($start) * 1000; print STDERR strftime("[%Y-%m-%d %H:%M:%S]", localtime)." Served $script in $elapsed ms\n"; } # Use require() instead of sub caching under NYTProf profiler for more correct reports sub run_script_require { my $self = shift; my ($script) = @_; my $content = $self->get_script($script, 1); my $start = [gettimeofday]; $in_eval = 1; eval $content; $self->check_errors($script); my $elapsed = tv_interval($start) * 1000; print STDERR strftime("[%Y-%m-%d %H:%M:%S]", localtime)." Served $script via require() in $elapsed ms\n"; } # Finish script run and check for errors sub check_errors { my $self = shift; my ($script) = @_; my $err; if ($@ && (!ref($@) || ref($@) ne 'Bugzilla::HTTPServerSimple::FakeExit')) { $err = "Error while running $script:\n$@"; } eval { Bugzilla::_cleanup(); }; if ($@ && (!ref($@) || ref($@) ne 'Bugzilla::HTTPServerSimple::FakeExit')) { print STDERR "Error in _cleanup():\n$@"; } $in_eval = 0; $Bugzilla::Error::IN_EVAL--; if ($err) { $self->internal_error($err); } } sub handler { my $self = shift; $self->handle_request; } sub handle_request { my $self = shift; # Set SCRIPT_NAME to REQUEST_URI and clear PATH_INFO # Prevent path traversal $ENV{REQUEST_URI} =~ tr!\\!/!; $ENV{REQUEST_URI} =~ s!\.+/+!!giso; $ENV{REQUEST_URI} =~ s!^/*!/!iso; $ENV{SCRIPT_NAME} = $ENV{REQUEST_URI}; $ENV{PATH_INFO} = ''; # Set non-parsed-headers CGI mode CGI::nph(1); # Determine SCRIPT_FILENAME my $script = $ENV{SCRIPT_FILENAME}; unless ($script) { ($script) = $ENV{REQUEST_URI} =~ m!/+([^\?\#]*)!so; } if ($self->{_config_hash}->{path_parent_regexp}) { $script =~ s!^($self->{_config_hash}->{path_parent_regexp})($|/+)!!s; } $script ||= 'index.cgi'; $ENV{SCRIPT_FILENAME} = $Bugzilla::HTTPServerSimple::DOCROOT.'/'.$script; # Check access if ($script =~ /\/$/s || $self->{_config_hash}->{deny_regexp} && $script =~ /$self->{_config_hash}->{deny_regexp}/s) { return $self->print_error('403', 'Access Denied', "You are not allowed to access URL $script on this server."); } # Serve static files (should be done by nginx, but we support it for completeness) my $fd; $script =~ s!^/*!!so; if ($script !~ /\.cgi$/iso || $script =~ /\//so) { if (open $fd, '<', $script) { print $ENV{SERVER_PROTOCOL}." 200 OK\r\n". "Content-Type: ".guess_media_type($script)."\r\n". "Content-Length: ".(-s $script)."\r\n\r\n"; sendfile(STDOUT, $fd, -s $script); close $fd; print STDERR strftime("[%Y-%m-%d %H:%M:%S]", localtime)." Served $script via sendfile()\n"; return 200; } else { # Failed to open file return $self->print_error('403', 'Access Denied', "You are not allowed to access URL $script on this server."); } } if ($self->{_config_hash}->{http_env}) { # Allow to set environment variables from additional HTTP headers foreach (split /[\s,]*,[\s,]*/, $self->{_config_hash}->{http_env}) { $ENV{$_} = $ENV{'HTTP_X_'.uc $_}; } } if ($self->{_config_hash}->{reload}) { reload(); } # Bugzilla-specific tweaks binmode STDOUT, ':utf8' if Bugzilla->can('params') && Bugzilla->params->{utf8}; # Clear request cache for new versions $Bugzilla::_request_cache = {}; eval { if ($ENV{NYTPROF} && $INC{'Devel/NYTProf.pm'}) { $self->run_script_require($script); } else { $self->run_script($script); } }; if ($@ && ref($@) eq 'Bugzilla::HTTPServerSimple::FakeExit') { return $self->print_error(@{$@->{error}}); } return 200; } my $STATS; # Used to reload Perl modules on-the-fly (debug purposes) sub reload { my ($file, $mtime); my @reload; for my $key (keys %INC) { $file = $INC{$key} or next; $file =~ /\.p[ml]$/i or next; # do not reload *.cgi $mtime = (stat $file)[9]; # Startup time as default $STATS->{$file} = $^T unless defined $STATS->{$file}; # Modified if ($mtime > $STATS->{$file}) { print STDERR __PACKAGE__ . ": $key -> $file modified, unloading\n"; unload($key) or next; push @reload, $key; $STATS->{$file} = $mtime; } } for (@reload) { print STDERR __PACKAGE__ . ": Reloading $_\n"; eval { no warnings 'redefine'; require $_ }; if ($@) { warn $@; } } } sub unload { my ($key) = @_; my $file = $INC{$key} or return; my @subs = grep { index($DB::sub{$_}, "$file:") == 0 } keys %DB::sub; for my $sub (@subs) { eval { undef &$sub }; if ($@) { warn "Can't unload sub '$sub' in '$file': $@"; return undef; } delete $DB::sub{$sub}; } delete $INC{$key}; return 1; } # This implementation is nearly the same as the original, but also uses buffered input sub parse_request { my $self = shift; my $chunk = ; defined($chunk) or return undef; $chunk =~ m/^(\w+)\s+(\S+)(?:\s+(\S+))?\r?$/; my $method = $1 || ''; my $uri = $2 || ''; my $protocol = $3 || ''; return ($method, $uri, $protocol); } # Override bad HTTP::Server::Simple::parse_headers implementation with a good one sub parse_headers { my $self = shift; my @headers; my $chunk; while ($chunk = ) { $chunk =~ s/[\r\l\n\s]+$//so; if ($chunk =~ /^([^()<>\@,;:\\"\/\[\]?={} \t]+):\s*(.*)/i) { push @headers, $1 => $2; } last if $chunk =~ /^$/so; } return \@headers; } # Net::Server fake subclass used to call _read_conf() package Bugzilla::NetServerConfigParser; use base 'Net::Server'; # _read_conf() could call fatal() in case of parse error sub fatal { shift; die @_; } # Fake "exit" exception class package Bugzilla::HTTPServerSimple::FakeExit; 1; __END__ Sample bugzilla.conf (all parameters can be specified on commandline using --option=value or here in config): # Net::Server subclass to use for serving class Net::Server::PreFork # That subclass's parameters port 0.0.0.0:8157 min_servers 4 max_servers 20 min_spare_servers 4 max_spare_servers 8 max_requests 1000 user www-data group www-data log_file /var/log/bugzilla.log log_level 2 pid_file /var/run/bugzilla.pid background 1 # This regexp (optional) will be stripped from the URI beginning path_parent_regexp bugs|bugzilla # HTTP 403 Access Denied will be shown for URLs matching deny_regexp: # You are URGED also to disable these URLs on your frontend. deny_regexp ^(localconfig|data/(?!webdot/)|.*\.(pm|pl|sh)($|\?)|(/|^)(CVS|\.(ht|svn|hg|bzr|git)).*) # 'http_env' specifies which environment variables to set from # a corresponding 'X-' HTTP header (value is comma-separated). # For example to support multiple Bugzilla 'projects' specify: http_env PROJECT # For http_env to work you need to push an appropriate header from your # frontend. For example, for nginx: # proxy_set_header X-Project 'project'; # Or for Apache: # RequestHeader set X-Project project # You can preload all scripts during startup so the maximum amount # of memory will be shared between workers preload *.cgi # Specify reload=1 to reload all modules (*.pm) on every request reload 1