From 8bbc156ca9a4bf3bfff8a9b7014a002808b1b7f0 Mon Sep 17 00:00:00 2001 From: Frédéric Buclin Date: Wed, 23 Dec 2015 20:46:43 +0100 Subject: Bug 1201113: Support to run Bugzilla as a PSGI application r=dylan --- .htaccess | 2 +- Bugzilla.pm | 86 +++++++++++++++++--------------- Bugzilla/CGI.pm | 5 +- Bugzilla/Config.pm | 21 ++++---- Bugzilla/Error.pm | 5 +- Bugzilla/Install/Filesystem.pm | 1 + Bugzilla/Install/Requirements.pm | 21 ++++++++ Bugzilla/Install/Util.pm | 10 ++++ Bugzilla/Template.pm | 4 +- app.psgi | 70 ++++++++++++++++++++++++++ editclassifications.cgi | 1 - editgroups.cgi | 18 ++----- editkeywords.cgi | 20 -------- shutdown.cgi | 17 +++++++ t/002goodperl.t | 2 +- t/Support/Files.pm | 2 +- template/en/default/setup/strings.txt.pl | 1 + testagent.cgi | 5 +- 18 files changed, 197 insertions(+), 94 deletions(-) create mode 100644 app.psgi create mode 100755 shutdown.cgi diff --git a/.htaccess b/.htaccess index 973b396d4..f9eeb541c 100644 --- a/.htaccess +++ b/.htaccess @@ -1,5 +1,5 @@ # Don't allow people to retrieve non-cgi executable files or our private data - + Deny from all diff --git a/Bugzilla.pm b/Bugzilla.pm index c6d7ae39b..16075b2d1 100644 --- a/Bugzilla.pm +++ b/Bugzilla.pm @@ -13,8 +13,11 @@ use warnings; # We want any compile errors to get to the browser, if possible. BEGIN { - # This makes sure we're in a CGI. - if ($ENV{SERVER_SOFTWARE} && !$ENV{MOD_PERL}) { + # This makes sure we're in a CGI. mod_perl doesn't support Carp + # and Plack reports errors elsewhere. + # We cannot call i_am_persistent() from here as its module is + # not loaded yet. + if ($ENV{SERVER_SOFTWARE} && !($ENV{MOD_PERL} || $ENV{BZ_PLACK})) { require CGI::Carp; CGI::Carp->import('fatalsToBrowser'); } @@ -32,7 +35,7 @@ use Bugzilla::Field; use Bugzilla::Flag; use Bugzilla::Install::Localconfig qw(read_localconfig); use Bugzilla::Install::Requirements qw(OPTIONAL_MODULES have_vers); -use Bugzilla::Install::Util qw(init_console include_languages); +use Bugzilla::Install::Util qw(init_console include_languages i_am_persistent); use Bugzilla::Memcached; use Bugzilla::Template; use Bugzilla::Token; @@ -149,43 +152,46 @@ sub init_page { { exit; } + # Plack requires to exit differently. + return -1 if $ENV{BZ_PLACK}; + _shutdown(); + } +} - # For security reasons, log out users when Bugzilla is down. - # Bugzilla->login() is required to catch the logincookie, if any. - my $user; - eval { $user = Bugzilla->login(LOGIN_OPTIONAL); }; - if ($@) { - # The DB is not accessible. Use the default user object. - $user = Bugzilla->user; - $user->{settings} = {}; - } - my $userid = $user->id; - Bugzilla->logout(); - - my $template = Bugzilla->template; - my $vars = {}; - $vars->{'message'} = 'shutdown'; - $vars->{'userid'} = $userid; - # Generate and return a message about the downtime, appropriately - # for if we're a command-line script or a CGI script. - my $extension; - if (i_am_cgi() && (!Bugzilla->cgi->param('ctype') - || Bugzilla->cgi->param('ctype') eq 'html')) { +sub _shutdown { + # For security reasons, log out users when Bugzilla is down. + # Bugzilla->login() is required to catch the logincookie, if any. + my $user = eval { Bugzilla->login(LOGIN_OPTIONAL); }; + if ($@) { + # The DB is not accessible. Use the default user object. + $user = Bugzilla->user; + $user->{settings} = {}; + } + my $userid = $user->id; + Bugzilla->logout(); + + # Generate and return a message about the downtime, appropriately + # for if we're a command-line script or a CGI script. + my $cgi = Bugzilla->cgi; + my $extension = 'txt'; + + if (i_am_cgi()) { + # Set the HTTP status to 503 when Bugzilla is down to avoid pages + # being indexed by search engines. + print $cgi->header(-status => 503, + -retry_after => SHUTDOWNHTML_RETRY_AFTER); + + if (!$cgi->param('ctype') || $cgi->param('ctype') eq 'html') { $extension = 'html'; } - else { - $extension = 'txt'; - } - if (i_am_cgi()) { - # Set the HTTP status to 503 when Bugzilla is down to avoid pages - # being indexed by search engines. - print Bugzilla->cgi->header(-status => 503, - -retry_after => SHUTDOWNHTML_RETRY_AFTER); - } - $template->process("global/message.$extension.tmpl", $vars) - || ThrowTemplateError($template->error); - exit; } + + my $template = Bugzilla->template; + my $vars = { message => 'shutdown', userid => $userid }; + + $template->process("global/message.$extension.tmpl", $vars) + or ThrowTemplateError($template->error); + exit; } ##################################################################### @@ -714,11 +720,13 @@ sub _cleanup { } sub END { - # Bugzilla.pm cannot compile in mod_perl.pl if this runs. - _cleanup() unless $ENV{MOD_PERL}; + # This is managed in mod_perl.pl and app.psgi when running + # in a persistent environment. + _cleanup() unless i_am_persistent(); } -init_page() if !$ENV{MOD_PERL}; +# Also managed in mod_perl.pl and app.psgi. +init_page() unless i_am_persistent(); 1; diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm index fcca0ec6a..25ee0acbe 100644 --- a/Bugzilla/CGI.pm +++ b/Bugzilla/CGI.pm @@ -66,7 +66,7 @@ sub new { # else we will be redirected outside Bugzilla. my $script_name = $self->script_name; $path_info =~ s/^\Q$script_name\E//; - if ($path_info) { + if ($script_name && $path_info) { print $self->redirect($self->url(-path => 0, -query => 1)); } } @@ -283,7 +283,7 @@ sub close_standby_message { print $self->multipart_end(); print $self->multipart_start(-type => $contenttype); } - else { + elsif (!$self->{_header_done}) { print $self->header($contenttype); } } @@ -356,6 +356,7 @@ sub header { Bugzilla::Hook::process('cgi_headers', { cgi => $self, headers => \%headers } ); + $self->{_header_done} = 1; return $self->SUPER::header(%headers) || ""; } diff --git a/Bugzilla/Config.pm b/Bugzilla/Config.pm index 5dfe2e37d..d47577212 100644 --- a/Bugzilla/Config.pm +++ b/Bugzilla/Config.pm @@ -16,6 +16,7 @@ use autodie qw(:default); use Bugzilla::Constants; use Bugzilla::Hook; +use Bugzilla::Install::Util qw(i_am_persistent); use Bugzilla::Util qw(trick_taint); use JSON::XS; @@ -319,15 +320,17 @@ sub read_param_file { } } elsif ($ENV{'SERVER_SOFTWARE'}) { - # We're in a CGI, but the params file doesn't exist. We can't - # Template Toolkit, or even install_string, since checksetup - # might not have thrown an error. Bugzilla::CGI->new - # hasn't even been called yet, so we manually use CGI::Carp here - # so that the user sees the error. - require CGI::Carp; - CGI::Carp->import('fatalsToBrowser'); - die "The $file file does not exist." - . ' You probably need to run checksetup.pl.', + # We're in a CGI, but the params file doesn't exist. We can't + # Template Toolkit, or even install_string, since checksetup + # might not have thrown an error. Bugzilla::CGI->new + # hasn't even been called yet, so we manually use CGI::Carp here + # so that the user sees the error. + unless (i_am_persistent()) { + require CGI::Carp; + CGI::Carp->import('fatalsToBrowser'); + } + die "The $file file does not exist." + . ' You probably need to run checksetup.pl.', } return \%params; } diff --git a/Bugzilla/Error.pm b/Bugzilla/Error.pm index e730022db..ee40ccf8b 100644 --- a/Bugzilla/Error.pm +++ b/Bugzilla/Error.pm @@ -28,8 +28,9 @@ use Date::Format; sub _in_eval { my $in_eval = 0; for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) { - last if $sub =~ /^ModPerl/; - $in_eval = 1 if $sub =~ /^\(eval\)/; + last if $sub =~ /^(?:ModPerl|Plack|CGI::Compile)/; + # An eval followed by CGI::Compile is not a "real" eval. + $in_eval = 1 if $sub =~ /^\(eval\)/ && (caller($stack + 1))[3] !~ /^CGI::Compile/; } return $in_eval; } diff --git a/Bugzilla/Install/Filesystem.pm b/Bugzilla/Install/Filesystem.pm index 5f5677460..e17285b2f 100644 --- a/Bugzilla/Install/Filesystem.pm +++ b/Bugzilla/Install/Filesystem.pm @@ -167,6 +167,7 @@ sub FILESYSTEM { 'install-module.pl' => { perms => OWNER_EXECUTE }, 'clean-bug-user-last-visit.pl' => { perms => WS_EXECUTE }, + 'app.psgi' => { perms => CGI_READ }, 'Bugzilla.pm' => { perms => CGI_READ }, "$localconfig*" => { perms => CGI_READ }, 'bugzilla.dtd' => { perms => WS_SERVE }, diff --git a/Bugzilla/Install/Requirements.pm b/Bugzilla/Install/Requirements.pm index 9a03968d7..a48778c1b 100644 --- a/Bugzilla/Install/Requirements.pm +++ b/Bugzilla/Install/Requirements.pm @@ -312,6 +312,26 @@ sub OPTIONAL_MODULES { version => 0, feature => ['jsonrpc'], }, + { + package => 'Plack', + module => 'Plack', + # 1.0031 contains a security fix which would affect us. + # It also fixes warnings thrown in Perl 5.20 and newer. + version => 1.0031, + feature => ['psgi'], + }, + { + package => 'CGI-Compile', + module => 'CGI::Compile', + version => 0, + feature => ['psgi'], + }, + { + package => 'CGI-Emulate-PSGI', + module => 'CGI::Emulate::PSGI', + version => 0, + feature => ['psgi'], + }, { package => 'Test-Taint', module => 'Test::Taint', @@ -474,6 +494,7 @@ use constant FEATURE_FILES => ( 'Bugzilla/WebService.pm', 'Bugzilla/WebService/*.pm'], rest => ['Bugzilla/WebService/Server/REST.pm', 'rest.cgi', 'Bugzilla/WebService/Server/REST/Resources/*.pm'], + psgi => ['app.psgi'], moving => ['importxml.pl'], auth_ldap => ['Bugzilla/Auth/Verify/LDAP.pm'], auth_radius => ['Bugzilla/Auth/Verify/RADIUS.pm'], diff --git a/Bugzilla/Install/Util.pm b/Bugzilla/Install/Util.pm index c05037061..82752b961 100644 --- a/Bugzilla/Install/Util.pm +++ b/Bugzilla/Install/Util.pm @@ -34,6 +34,7 @@ our @EXPORT_OK = qw( extension_requirement_packages extension_template_directory extension_web_directory + i_am_persistent indicate_progress install_string include_languages @@ -83,6 +84,10 @@ sub get_version_and_os { os_ver => $os_details[3] }; } +sub i_am_persistent { + return ($ENV{MOD_PERL} || $ENV{BZ_PLACK}) ? 1 : 0; +} + sub _extension_paths { my $dir = bz_locations()->{'extensionsdir'}; my @extension_items = glob("$dir/*"); @@ -711,6 +716,11 @@ binary, if the binary is in the C. Returns a hash containing information about what version of Bugzilla we're running, what perl version we're using, and what OS we're running on. +=item C + +Returns true if Bugzilla is running in a persistent environment, such as +mod_perl or PSGI. Returns false if running in mod_cgi mode. + =item C Returns the language to use based on the LC_CTYPE value returned by the OS. diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm index 04abe8200..9398ca4b5 100644 --- a/Bugzilla/Template.pm +++ b/Bugzilla/Template.pm @@ -17,7 +17,7 @@ use Bugzilla::WebService::Constants; use Bugzilla::Hook; use Bugzilla::Install::Requirements; use Bugzilla::Install::Util qw(install_string template_include_path - include_languages); + include_languages i_am_persistent); use Bugzilla::Classification; use Bugzilla::Keyword; use Bugzilla::Util; @@ -740,7 +740,7 @@ sub create { # if a packager has modified bz_locations() to contain absolute # paths. ABSOLUTE => 1, - RELATIVE => $ENV{MOD_PERL} ? 0 : 1, + RELATIVE => i_am_persistent() ? 0 : 1, COMPILE_DIR => bz_locations()->{'template_cache'}, diff --git a/app.psgi b/app.psgi new file mode 100644 index 000000000..c04359fb1 --- /dev/null +++ b/app.psgi @@ -0,0 +1,70 @@ +#!/usr/bin/perl +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +use 5.10.1; +use strict; +use warnings; + +use File::Basename; +use lib dirname(__FILE__); +use Bugzilla::Constants (); +use lib Bugzilla::Constants::bz_locations()->{ext_libpath}; + +use Plack; +use Plack::Builder; +use Plack::App::URLMap; +use Plack::App::WrapCGI; +use Plack::Response; + +use constant STATIC => qw( + data/assets + data/webdot + docs + extensions/[^/]+/web + graphs + images + js + skins +); + +builder { + my $static_paths = join('|', STATIC); + enable 'Static', + path => qr{^/($static_paths)/}, + root => Bugzilla::Constants::bz_locations->{cgi_path}; + + $ENV{BZ_PLACK} = 'Plack/' . Plack->VERSION; + + my $map = Plack::App::URLMap->new; + + my @cgis = glob('*.cgi'); + my $shutdown_app = Plack::App::WrapCGI->new(script => 'shutdown.cgi')->to_app; + + foreach my $cgi_script (@cgis) { + my $app = eval { Plack::App::WrapCGI->new(script => $cgi_script)->to_app }; + # Some CGI scripts won't compile if not all optional Perl modules are + # installed. That's expected. + if ($@) { + warn "Cannot compile $cgi_script. Skipping!\n"; + next; + } + + my $wrapper = sub { + my $ret = Bugzilla::init_page(); + my $res = ($ret eq '-1' && $cgi_script ne 'editparams.cgi') ? $shutdown_app->(@_) : $app->(@_); + Bugzilla::_cleanup(); + return $res; + }; + + my $base_name = basename($cgi_script); + $map->map('/' => $wrapper) if $cgi_script eq 'index.cgi'; + $map->map('/rest' => $wrapper) if $cgi_script eq 'rest.cgi'; + $map->map("/$base_name" => $wrapper); + } + my $app = $map->to_app; +}; diff --git a/editclassifications.cgi b/editclassifications.cgi index ea4b139da..f839cfa03 100755 --- a/editclassifications.cgi +++ b/editclassifications.cgi @@ -38,7 +38,6 @@ sub LoadTemplate { $action =~ /(\w+)/; $action = $1; - print $cgi->header(); $template->process("admin/classifications/$action.html.tmpl", $vars) || ThrowTemplateError($template->error()); exit; diff --git a/editgroups.cgi b/editgroups.cgi index 35989b954..f2c915556 100755 --- a/editgroups.cgi +++ b/editgroups.cgi @@ -135,8 +135,7 @@ sub get_current_and_available { unless ($action) { my @groups = Bugzilla::Group->get_all; $vars->{'groups'} = \@groups; - - print $cgi->header(); + $template->process("admin/groups/list.html.tmpl", $vars) || ThrowTemplateError($template->error()); exit; @@ -155,12 +154,10 @@ if ($action eq 'changeform') { get_current_and_available($group, $vars); $vars->{'group'} = $group; - $vars->{'token'} = issue_session_token('edit_group'); + $vars->{'token'} = issue_session_token('edit_group'); - print $cgi->header(); $template->process("admin/groups/edit.html.tmpl", $vars) || ThrowTemplateError($template->error()); - exit; } @@ -172,10 +169,9 @@ if ($action eq 'changeform') { if ($action eq 'add') { $vars->{'token'} = issue_session_token('add_group'); - print $cgi->header(); + $template->process("admin/groups/create.html.tmpl", $vars) || ThrowTemplateError($template->error()); - exit; } @@ -204,7 +200,6 @@ if ($action eq 'new') { get_current_and_available($group, $vars); $vars->{'token'} = issue_session_token('edit_group'); - print $cgi->header(); $template->process("admin/groups/edit.html.tmpl", $vars) || ThrowTemplateError($template->error()); exit; @@ -228,10 +223,8 @@ if ($action eq 'del') { $vars->{'group'} = $group; $vars->{'token'} = issue_session_token('delete_group'); - print $cgi->header(); $template->process("admin/groups/delete.html.tmpl", $vars) || ThrowTemplateError($template->error()); - exit; } @@ -255,7 +248,6 @@ if ($action eq 'delete') { $vars->{'message'} = 'group_deleted'; $vars->{'groups'} = [Bugzilla::Group->get_all]; - print $cgi->header(); $template->process("admin/groups/list.html.tmpl", $vars) || ThrowTemplateError($template->error()); exit; @@ -277,7 +269,6 @@ if ($action eq 'postchanges') { $vars->{'changes'} = $changes; $vars->{'token'} = issue_session_token('edit_group'); - print $cgi->header(); $template->process("admin/groups/edit.html.tmpl", $vars) || ThrowTemplateError($template->error()); exit; @@ -288,6 +279,7 @@ if ($action eq 'confirm_remove') { $vars->{'group'} = $group; $vars->{'regexp'} = CheckGroupRegexp($cgi->param('regexp')); $vars->{'token'} = issue_session_token('remove_group_members'); + $template->process('admin/groups/confirm-remove.html.tmpl', $vars) || ThrowTemplateError($template->error()); exit; @@ -326,10 +318,8 @@ if ($action eq 'remove_regexp') { $vars->{'group'} = $group->name; $vars->{'groups'} = [Bugzilla::Group->get_all]; - print $cgi->header(); $template->process("admin/groups/list.html.tmpl", $vars) || ThrowTemplateError($template->error()); - exit; } diff --git a/editkeywords.cgi b/editkeywords.cgi index ab079e540..da7513f6f 100755 --- a/editkeywords.cgi +++ b/editkeywords.cgi @@ -24,10 +24,6 @@ my $dbh = Bugzilla->dbh; my $template = Bugzilla->template; my $vars = {}; -# -# Preliminary checks: -# - my $user = Bugzilla->login(LOGIN_REQUIRED); print $cgi->header(); @@ -47,22 +43,16 @@ $vars->{'action'} = $action; if ($action eq "") { $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count(); - print $cgi->header(); $template->process("admin/keywords/list.html.tmpl", $vars) || ThrowTemplateError($template->error()); - exit; } - if ($action eq 'add') { $vars->{'token'} = issue_session_token('add_keyword'); - print $cgi->header(); - $template->process("admin/keywords/create.html.tmpl", $vars) || ThrowTemplateError($template->error()); - exit; } @@ -79,8 +69,6 @@ if ($action eq 'new') { delete_token($token); - print $cgi->header(); - $vars->{'message'} = 'keyword_created'; $vars->{'name'} = $keyword->name; $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count(); @@ -90,7 +78,6 @@ if ($action eq 'new') { exit; } - # # action='edit' -> present the edit keywords from # @@ -104,13 +91,11 @@ if ($action eq 'edit') { $vars->{'keyword'} = $keyword; $vars->{'token'} = issue_session_token('edit_keyword'); - print $cgi->header(); $template->process("admin/keywords/edit.html.tmpl", $vars) || ThrowTemplateError($template->error()); exit; } - # # action='update' -> update the keyword # @@ -129,8 +114,6 @@ if ($action eq 'update') { delete_token($token); - print $cgi->header(); - $vars->{'message'} = 'keyword_updated'; $vars->{'keyword'} = $keyword; $vars->{'changes'} = $changes; @@ -148,7 +131,6 @@ if ($action eq 'del') { $vars->{'keyword'} = $keyword; $vars->{'token'} = issue_session_token('delete_keyword'); - print $cgi->header(); $template->process("admin/keywords/confirm-delete.html.tmpl", $vars) || ThrowTemplateError($template->error()); exit; @@ -163,8 +145,6 @@ if ($action eq 'delete') { delete_token($token); - print $cgi->header(); - $vars->{'message'} = 'keyword_deleted'; $vars->{'keyword'} = $keyword; $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count(); diff --git a/shutdown.cgi b/shutdown.cgi new file mode 100755 index 000000000..7b33ec7c4 --- /dev/null +++ b/shutdown.cgi @@ -0,0 +1,17 @@ +#!/usr/bin/perl -T +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +use 5.10.1; +use strict; +use warnings; + +use lib qw(. lib); + +use Bugzilla; + +Bugzilla::_shutdown(); diff --git a/t/002goodperl.t b/t/002goodperl.t index d1858361f..cfc9fb9e9 100644 --- a/t/002goodperl.t +++ b/t/002goodperl.t @@ -40,7 +40,7 @@ foreach my $file (@testitems) { ok(1,"$file does not have a shebang"); } else { my $flags; - if (!defined $ext || $ext eq "pl") { + if (!defined $ext || $ext eq 'pl' || $ext eq 'psgi') { # standalone programs aren't taint checked yet if (grep { $file eq $_ } @require_taint) { $flags = 'T'; diff --git a/t/Support/Files.pm b/t/Support/Files.pm index f3fae58fc..f1c88e858 100644 --- a/t/Support/Files.pm +++ b/t/Support/Files.pm @@ -34,7 +34,7 @@ sub isTestingFile { my ($file) = @_; my $exclude; - if ($file =~ /\.cgi$|\.pl$|\.pm$/) { + if ($file =~ /\.psgi$|\.cgi$|\.pl$|\.pm$/) { return 1; } my $additional; diff --git a/template/en/default/setup/strings.txt.pl b/template/en/default/setup/strings.txt.pl index d1bb873ca..4409d9ff3 100644 --- a/template/en/default/setup/strings.txt.pl +++ b/template/en/default/setup/strings.txt.pl @@ -103,6 +103,7 @@ END feature_mod_perl => 'mod_perl', feature_moving => 'Move Bugs Between Installations', feature_patch_viewer => 'Patch Viewer', + feature_psgi => 'PSGI Support', feature_rest => 'REST Interface', feature_smtp_auth => 'SMTP Authentication', feature_smtp_ssl => 'SSL Support for SMTP', diff --git a/testagent.cgi b/testagent.cgi index d9d5afd1a..dfb1ff228 100755 --- a/testagent.cgi +++ b/testagent.cgi @@ -15,5 +15,6 @@ use strict; use warnings; say "content-type:text/plain\n"; -say "OK " . ($::ENV{MOD_PERL} || "mod_cgi"); -exit; + +print 'OK '; +say $ENV{BZ_PLACK} || $ENV{MOD_PERL} || 'mod_cgi'; -- cgit v1.2.3-24-g4f1b