From: Dylan William Hardison Date: Wed, 10 Apr 2019 15:47:29 +0000 (-0400) Subject: Bug 1543155 - Observatory score dropped from an A+ to a D- X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=ac8c918e6edfffecba43e3f10ed75bed05e09c42;p=thirdparty%2Fbugzilla.git Bug 1543155 - Observatory score dropped from an A+ to a D- --- diff --git a/Bugzilla/App.pm b/Bugzilla/App.pm index b61bf9382..6fa6fb3e1 100644 --- a/Bugzilla/App.pm +++ b/Bugzilla/App.pm @@ -131,15 +131,44 @@ sub startup { } $self->hook(after_dispatch => sub { my ($c) = @_; - if ($c->req->is_secure - && ! $c->res->headers->strict_transport_security + my ($req, $res) = ($c->req, $c->res); + + if ( $req->is_secure + && !$res->headers->strict_transport_security && Bugzilla->params->{'strict_transport_security'} ne 'off') { my $sts_opts = 'max-age=' . MAX_STS_AGE; if (Bugzilla->params->{'strict_transport_security'} eq 'include_subdomains') { $sts_opts .= '; includeSubDomains'; } - $c->res->headers->strict_transport_security($sts_opts); + $res->headers->strict_transport_security($sts_opts); + } + + # Add X-Frame-Options header to prevent framing and subsequent + # possible clickjacking problems. + unless ($c->url_is_attachment_base) { + $res->headers->header('X-frame-options' => 'SAMEORIGIN'); + } + + # Add X-XSS-Protection header to prevent simple XSS attacks + # and enforce the blocking (rather than the rewriting) mode. + $res->headers->header('X-xss-protection' => '1; mode=block'); + + # Add X-Content-Type-Options header to prevent browsers sniffing + # the MIME type away from the declared Content-Type. + $res->headers->header('X-content-type-options' => 'nosniff'); + + if (length($req->url->to_abs->to_string) > 8000) { + $res->headers->header('Referrer-policy' => 'origin'); + } + else { + $res->headers->header('Referrer-policy' => 'same-origin'); + } + + unless ($res->headers->content_security_policy) { + if (my $csp = $c->content_security_policy) { + $res->headers->header($csp->header_name, $csp->value); + } } }); Bugzilla::WebService::Server::REST->preload; diff --git a/Bugzilla/App/CGI.pm b/Bugzilla/App/CGI.pm index ebe156a0e..b35568710 100644 --- a/Bugzilla/App/CGI.pm +++ b/Bugzilla/App/CGI.pm @@ -49,7 +49,7 @@ sub load_one { my ($class, $name, $file) = @_; my $package = __PACKAGE__ . "::$name", my $inner_name = "_$name"; my $content = path(bz_locations->{cgi_path}, $file)->slurp; - $content = "package $package; $content"; + $content = "package $package; local our \$C = \$Bugzilla::App::CGI::C; $content"; my %options = (package => $package, file => $file, line => 1, no_defer => 1,); die "Tried to load $file more than once" if $SEEN{$file}++; my $inner = quote_sub $inner_name, $content, {}, \%options; diff --git a/Bugzilla/App/Plugin/Glue.pm b/Bugzilla/App/Plugin/Glue.pm index 9dca9fd60..3f6dc2ad6 100644 --- a/Bugzilla/App/Plugin/Glue.pm +++ b/Bugzilla/App/Plugin/Glue.pm @@ -142,6 +142,61 @@ sub register { } } ); + $app->helper( + 'url_is_attachment_base' => sub { + my ($c, $id) = @_; + return 0 unless Bugzilla::Util::use_attachbase(); + my $attach_base = Bugzilla->localconfig->{'attachment_base'}; + + # If we're passed an id, we only want one specific attachment base + # for a particular bug. If we're not passed an ID, we just want to + # know if our current URL matches the attachment_base *pattern*. + my $regex; + if ($id) { + $attach_base =~ s/\%bugid\%/$id/; + $regex = quotemeta($attach_base); + } + else { + # In this circumstance we run quotemeta first because we need to + # insert an active regex meta-character afterward. + $regex = quotemeta($attach_base); + $regex =~ s/\\\%bugid\\\%/\\d+/; + } + $regex = "^$regex"; + return ($c->req->url->to_abs =~ $regex) ? 1 : 0; + } + ); + + $app->helper( + 'content_security_policy' => sub { + my ($c, %add_params) = @_; + my $stash = $c->stash; + if (%add_params || !$stash->{Bugzilla_csp}) { + my %params = DEFAULT_CSP(); + delete $params{report_only} if %add_params && !$add_params{report_only}; + delete $params{report_only} if !$c->isa('Bugzilla::App::CGI'); + foreach my $key (keys %add_params) { + if (defined $add_params{$key}) { + $params{$key} = $add_params{$key}; + } + else { + delete $params{$key}; + } + } + $stash->{Bugzilla_csp} = Bugzilla::CGI::ContentSecurityPolicy->new(%params); + } + + return $stash->{Bugzilla_csp}; + } + ); + $app->helper( + 'csp_nonce' => sub { + my ($c) = @_; + + my $csp = $c->content_security_policy; + return $csp->has_nonce ? $csp->nonce : ''; + } + ); $app->helper( 'bz_include' => sub { diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm index f2a3b7358..f2404b8b6 100644 --- a/Bugzilla/CGI.pm +++ b/Bugzilla/CGI.pm @@ -34,92 +34,6 @@ BEGIN { *AUTOLOAD = \&CGI::AUTOLOAD; } -sub DEFAULT_CSP { - my %policy = ( - default_src => ['self'], - script_src => - ['self', 'nonce', 'unsafe-inline', 'https://www.google-analytics.com'], - frame_src => [ - # This is for extensions/BMO/web/js/firefox-crash-table.js - 'https://crash-stop-addon.herokuapp.com', - ], - worker_src => ['none',], - img_src => ['self', 'blob:', 'https://secure.gravatar.com'], - style_src => ['self', 'unsafe-inline'], - object_src => ['none'], - connect_src => [ - 'self', - - # This is for extensions/BMO/web/js/firefox-crash-table.js - 'https://product-details.mozilla.org', - - # This is for extensions/GoogleAnalytics using beacon or XHR - 'https://www.google-analytics.com', - - # This is from extensions/OrangeFactor/web/js/orange_factor.js - 'https://treeherder.mozilla.org/api/failurecount/', - ], - form_action => [ - 'self', - - # used in template/en/default/search/search-google.html.tmpl - 'https://www.google.com/search' - ], - frame_ancestors => ['none'], - report_only => 1, - ); - if (Bugzilla->params->{github_client_id} && !Bugzilla->user->id) { - push @{$policy{form_action}}, 'https://github.com/login/oauth/authorize', - 'https://github.com/login'; - } - - return %policy; -} - -# Because show_bug code lives in many different .cgi files, -# we needed a centralized place to define the policy. -# normally the policy would just live in one .cgi file. -# Additionally, Bugzilla->localconfig->{urlbase} cannot be called at compile time, so this can't be a constant. -sub SHOW_BUG_MODAL_CSP { - my ($bug_id) = @_; - my %policy = ( - script_src => [ - 'self', 'nonce', - 'unsafe-inline', 'unsafe-eval', - 'https://www.google-analytics.com' - ], - img_src => ['self', 'https://secure.gravatar.com'], - media_src => ['self'], - connect_src => [ - 'self', - - # This is for extensions/BMO/web/js/firefox-crash-table.js - 'https://product-details.mozilla.org', - - # This is for extensions/GoogleAnalytics using beacon or XHR - 'https://www.google-analytics.com', - - # This is from extensions/OrangeFactor/web/js/orange_factor.js - 'https://treeherder.mozilla.org/api/failurecount/', - ], - frame_src => [ - 'self', - - # This is for extensions/BMO/web/js/firefox-crash-table.js - 'https://crash-stop-addon.herokuapp.com', - ], - worker_src => ['none',], - ); - if (use_attachbase() && $bug_id) { - my $attach_base = Bugzilla->localconfig->{'attachment_base'}; - $attach_base =~ s/\%bugid\%/$bug_id/g; - push @{$policy{img_src}}, $attach_base; - push @{$policy{media_src}}, $attach_base; - } - - return %policy; -} - sub _init_bz_cgi_globals { my $invocant = shift; @@ -171,11 +85,13 @@ sub new { $self->charset(Bugzilla->params->{'utf8'} ? 'UTF-8' : ''); # Redirect to urlbase if we are not viewing an attachment. - if ($self->url_is_attachment_base and $script ne 'attachment.cgi') { - DEBUG( - "Redirecting to urlbase because the url is in the attachment base and not attachment.cgi" - ); - $self->redirect_to_urlbase(); + if (my $C = $Bugzilla::App::CGI::C) { + if ($C->url_is_attachment_base and $script ne 'attachment.cgi') { + DEBUG( + "Redirecting to urlbase because the url is in the attachment base and not attachment.cgi" + ); + $self->redirect_to_urlbase(); + } } # Check for errors @@ -222,32 +138,6 @@ sub target_uri { } } -sub content_security_policy { - my ($self, %add_params) = @_; - if (%add_params || !$self->{Bugzilla_csp}) { - my %params = DEFAULT_CSP; - delete $params{report_only} if %add_params && !$add_params{report_only}; - foreach my $key (keys %add_params) { - if (defined $add_params{$key}) { - $params{$key} = $add_params{$key}; - } - else { - delete $params{$key}; - } - } - $self->{Bugzilla_csp} = Bugzilla::CGI::ContentSecurityPolicy->new(%params); - } - - return $self->{Bugzilla_csp}; -} - -sub csp_nonce { - my ($self) = @_; - - my $csp = $self->content_security_policy; - return $csp->has_nonce ? $csp->nonce : ''; -} - # We want this sorted plus the ability to exclude certain params sub canonicalise_query { my ($self, @exclude) = @_; @@ -590,53 +480,13 @@ sub header { $headers{'-cookie'} = $self->{Bugzilla_cookie_list}; } - # Add Strict-Transport-Security (STS) header if this response - # is over SSL and the strict_transport_security param is turned on. - if ( $self->https - && !$self->url_is_attachment_base - && Bugzilla->params->{'strict_transport_security'} ne 'off') - { - my $sts_opts = 'max-age=' . MAX_STS_AGE; - if (Bugzilla->params->{'strict_transport_security'} eq 'include_subdomains') { - $sts_opts .= '; includeSubDomains'; - } - $headers{'-strict_transport_security'} = $sts_opts; - } - - # Add X-Frame-Options header to prevent framing and subsequent - # possible clickjacking problems. - unless ($self->url_is_attachment_base) { - $headers{'-x_frame_options'} = 'SAMEORIGIN'; - } - if ($self->{'_content_disp'}) { $headers{'-content_disposition'} = $self->{'_content_disp'}; } - # Add X-XSS-Protection header to prevent simple XSS attacks - # and enforce the blocking (rather than the rewriting) mode. - $headers{'-x_xss_protection'} = '1; mode=block'; - - # Add X-Content-Type-Options header to prevent browsers sniffing - # the MIME type away from the declared Content-Type. - $headers{'-x_content_type_options'} = 'nosniff'; - - # Add Referrer-Policy (sic) header to prevent browsers sending - # Referer (sic) headers to external websites. - $headers{'-referrer_policy'} = 'same-origin'; - - Bugzilla::Hook::process('cgi_headers', {cgi => $self, headers => \%headers}); $self->{_header_done} = 1; if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { - if ($self->should_block_referrer) { - $headers{'-referrer_policy'} = 'origin'; - } - my $csp = $self->content_security_policy; - if (defined $csp && !$csp->disable) { - $csp->add_cgi_headers(\%headers); - } - my @fonts = ( "skins/standard/fonts/FiraMono-Regular.woff2?v=3.202", "skins/standard/fonts/FiraSans-Bold.woff2?v=4.203", @@ -896,29 +746,6 @@ sub base_redirect { exit; } -sub url_is_attachment_base { - my ($self, $id) = @_; - return 0 if !use_attachbase() or !i_am_cgi(); - my $attach_base = Bugzilla->localconfig->{'attachment_base'}; - - # If we're passed an id, we only want one specific attachment base - # for a particular bug. If we're not passed an ID, we just want to - # know if our current URL matches the attachment_base *pattern*. - my $regex; - if ($id) { - $attach_base =~ s/\%bugid\%/$id/; - $regex = quotemeta($attach_base); - } - else { - # In this circumstance we run quotemeta first because we need to - # insert an active regex meta-character afterward. - $regex = quotemeta($attach_base); - $regex =~ s/\\\%bugid\\\%/\\d+/; - } - $regex = "^$regex"; - return ($self->url =~ $regex) ? 1 : 0; -} - sub set_dated_content_disp { my ($self, $type, $prefix, $ext) = @_; @@ -1025,18 +852,6 @@ correctly, using C or the mod_perl APIs as appropriate. To remove (expire) a cookie, use C. -=item C - -Set a Content Security Policy for the current request. This is a no-op if the 'csp' feature -is not available. The arguments to this method are passed to the constructor of L, -consult that module for a list of what directives are supported. - -=item C - -Returns a CSP nonce value if CSP is available and 'nonce' is listed as a source in a CSP *_src directive. - -If there is no nonce used, or CSP is not available, this returns the empty string. - =item C This is a wrapper around send_cookie, setting an expiry date in the past, diff --git a/Bugzilla/CGI/ContentSecurityPolicy.pm b/Bugzilla/CGI/ContentSecurityPolicy.pm index 557a896ab..3ac8c067a 100644 --- a/Bugzilla/CGI/ContentSecurityPolicy.pm +++ b/Bugzilla/CGI/ContentSecurityPolicy.pm @@ -59,22 +59,14 @@ sub _has_directive { return $self->$method; } -sub header_names { +sub header_name { my ($self) = @_; - my @names = ('Content-Security-Policy'); + my $name = 'Content-Security-Policy'; if ($self->report_only) { - return map { $_ . '-Report-Only' } @names; + return $name . '-Report-Only'; } else { - return @names; - } -} - -sub add_cgi_headers { - my ($self, $headers) = @_; - return if $self->disable; - foreach my $name ($self->header_names) { - $headers->{"-$name"} = $self->value; + return $name; } } @@ -164,7 +156,7 @@ Bugzilla::CGI::ContentSecurityPolicy - Object-oriented interface to generating C This class provides an object interface to constructing Content Security Policies. -Rather than use this module, scripts should call $cgi->content_security_policy() which constructs the CSP headers +Rather than use this module, scripts should call $C->content_security_policy() which constructs the CSP headers and registers them for the current request. See L for details. diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm index ffa981741..476be3d96 100644 --- a/Bugzilla/Constants.pm +++ b/Bugzilla/Constants.pm @@ -24,6 +24,9 @@ use Memoize; REMOTE_FILE LOCAL_FILE + DEFAULT_CSP + SHOW_BUG_MODAL_CSP + bz_locations IS_NULL @@ -727,6 +730,93 @@ sub _bz_locations { }; } +sub DEFAULT_CSP { + my %policy = ( + default_src => ['self'], + script_src => + ['self', 'nonce', 'unsafe-inline', 'https://www.google-analytics.com'], + frame_src => [ + # This is for extensions/BMO/web/js/firefox-crash-table.js + 'https://crash-stop-addon.herokuapp.com', + ], + worker_src => ['none',], + img_src => ['self', 'blob:', 'https://secure.gravatar.com'], + style_src => ['self', 'unsafe-inline'], + object_src => ['none'], + connect_src => [ + 'self', + + # This is for extensions/BMO/web/js/firefox-crash-table.js + 'https://product-details.mozilla.org', + + # This is for extensions/GoogleAnalytics using beacon or XHR + 'https://www.google-analytics.com', + + # This is from extensions/OrangeFactor/web/js/orange_factor.js + 'https://treeherder.mozilla.org/api/failurecount/', + ], + form_action => [ + 'self', + + # used in template/en/default/search/search-google.html.tmpl + 'https://www.google.com/search' + ], + frame_ancestors => ['none'], + report_only => 1, + ); + if (Bugzilla->params->{github_client_id} && !Bugzilla->user->id) { + push @{$policy{form_action}}, 'https://github.com/login/oauth/authorize', + 'https://github.com/login'; + } + + return %policy; +} + +# Because show_bug code lives in many different .cgi files, +# we needed a centralized place to define the policy. +# normally the policy would just live in one .cgi file. +# Additionally, Bugzilla->localconfig->{urlbase} cannot be called at compile time, so this can't be a constant. +sub SHOW_BUG_MODAL_CSP { + my ($bug_id) = @_; + my %policy = ( + script_src => [ + 'self', 'nonce', + 'unsafe-inline', 'unsafe-eval', + 'https://www.google-analytics.com' + ], + img_src => ['self', 'https://secure.gravatar.com'], + media_src => ['self'], + connect_src => [ + 'self', + + # This is for extensions/BMO/web/js/firefox-crash-table.js + 'https://product-details.mozilla.org', + + # This is for extensions/GoogleAnalytics using beacon or XHR + 'https://www.google-analytics.com', + + # This is from extensions/OrangeFactor/web/js/orange_factor.js + 'https://treeherder.mozilla.org/api/failurecount/', + ], + frame_src => [ + 'self', + + # This is for extensions/BMO/web/js/firefox-crash-table.js + 'https://crash-stop-addon.herokuapp.com', + ], + worker_src => ['none',], + ); + if (Bugzilla::Util::use_attachbase() && $bug_id) { + my $attach_base = Bugzilla->localconfig->{'attachment_base'}; + $attach_base =~ s/\%bugid\%/$bug_id/g; + push @{$policy{img_src}}, $attach_base; + push @{$policy{media_src}}, $attach_base; + } + + return %policy; +} + + # This makes us not re-compute all the bz_locations data every time it's # called. BEGIN { memoize('_bz_locations') } diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm index 131d7fb51..b5affc46a 100644 --- a/Bugzilla/Template.pm +++ b/Bugzilla/Template.pm @@ -969,8 +969,8 @@ sub create { 'current_language' => sub { return Bugzilla->current_language; }, 'script_nonce' => sub { - my $cgi = Bugzilla->cgi; - return $cgi->csp_nonce ? sprintf('nonce="%s"', $cgi->csp_nonce) : ''; + my $C = $Bugzilla::App::CGI::C or return ''; + return $C->csp_nonce ? sprintf('nonce="%s"', $C->csp_nonce) : ''; }, # If an sudo session is in progress, this is the user who diff --git a/attachment.cgi b/attachment.cgi index 081f4468b..40e042221 100755 --- a/attachment.cgi +++ b/attachment.cgi @@ -71,7 +71,7 @@ if ($action ne 'view' && (($action !~ /^(?:interdiff|diff)$/) || $format ne 'raw')) { do_ssl_redirect_if_required(); - if ($cgi->url_is_attachment_base) { + if ($C->url_is_attachment_base) { $cgi->redirect_to_urlbase; } Bugzilla->login(); @@ -244,7 +244,7 @@ sub get_attachment { my $path = 'attachment.cgi?' . join('&', @args); # Make sure the attachment is served from the correct server. - if ($cgi->url_is_attachment_base($bug_id)) { + if ($C->url_is_attachment_base($bug_id)) { # No need to validate the token for public attachments. We cannot request # credentials as we are on the alternate host. @@ -276,7 +276,7 @@ sub get_attachment { delete_token($token); } } - elsif ($cgi->url_is_attachment_base) { + elsif ($C->url_is_attachment_base) { # If we come here, this means that each bug has its own host # for attachments, and that we are trying to view one attachment @@ -669,7 +669,7 @@ sub insert { Bugzilla::Hook::process('show_bug_format', $show_bug_format); if ($show_bug_format->{format} eq 'modal') { - $cgi->content_security_policy(Bugzilla::CGI::SHOW_BUG_MODAL_CSP($bugid)); + $C->content_security_policy(SHOW_BUG_MODAL_CSP($bugid)); } print $cgi->header(); @@ -851,7 +851,7 @@ sub update { Bugzilla::Hook::process('show_bug_format', $show_bug_format); if ($show_bug_format->{format} eq 'modal') { - $cgi->content_security_policy(Bugzilla::CGI::SHOW_BUG_MODAL_CSP($bug->id)); + $C->content_security_policy(SHOW_BUG_MODAL_CSP($bug->id)); } print $cgi->header(); @@ -925,7 +925,7 @@ sub delete_attachment { Bugzilla::Hook::process('show_bug_format', $show_bug_format); if ($show_bug_format->{format} eq 'modal') { - $cgi->content_security_policy(Bugzilla::CGI::SHOW_BUG_MODAL_CSP($bug->id)); + $C->content_security_policy(SHOW_BUG_MODAL_CSP($bug->id)); } print $cgi->header(); diff --git a/chart.cgi b/chart.cgi index 3c5536852..5380e1d9d 100755 --- a/chart.cgi +++ b/chart.cgi @@ -51,7 +51,7 @@ local our $cgi = Bugzilla->cgi; local our $template = Bugzilla->template; local our $vars = {}; my $dbh = Bugzilla->dbh; -$cgi->content_security_policy(report_only => 0); +$C->content_security_policy(report_only => 0); my $user = Bugzilla->login(LOGIN_REQUIRED); diff --git a/index.cgi b/index.cgi index b32e673bb..888f6fc1e 100755 --- a/index.cgi +++ b/index.cgi @@ -68,7 +68,7 @@ if ( } else { my $template = Bugzilla->template; - $cgi->content_security_policy( + $C->content_security_policy( script_src => ['self', 'https://www.google-analytics.com']); # Return the appropriate HTTP response headers. diff --git a/post_bug.cgi b/post_bug.cgi index 577e62d68..ee94b330d 100755 --- a/post_bug.cgi +++ b/post_bug.cgi @@ -305,7 +305,7 @@ $cgi->delete('format'); if ($user->setting('ui_experiments') eq 'on') { Bugzilla->cgi->content_security_policy( - Bugzilla::CGI::SHOW_BUG_MODAL_CSP($bug->id)); + SHOW_BUG_MODAL_CSP($bug->id)); } print $cgi->header(); $template->process($format->{'template'}, $vars) diff --git a/process_bug.cgi b/process_bug.cgi index 9ec444ffa..738ea8c19 100755 --- a/process_bug.cgi +++ b/process_bug.cgi @@ -437,7 +437,7 @@ my $format_params = { Bugzilla::Hook::process('show_bug_format', $format_params); if ($format_params->{format} eq 'modal') { my $bug_id = $vars->{bug} ? $vars->{bug}->id : undef; - $cgi->content_security_policy(Bugzilla::CGI::SHOW_BUG_MODAL_CSP($bug_id)); + $C->content_security_policy(SHOW_BUG_MODAL_CSP($bug_id)); } my $format = $template->get_format( "bug/show", diff --git a/report.cgi b/report.cgi index 46038c7b2..f83d684da 100755 --- a/report.cgi +++ b/report.cgi @@ -25,7 +25,7 @@ my $cgi = Bugzilla->cgi; my $template = Bugzilla->template; my $vars = {}; -$cgi->content_security_policy(report_only => 0); +$C->content_security_policy(report_only => 0); # Go straight back to query.cgi if we are adding a boolean chart. if (grep(/^cmd-/, $cgi->param())) { diff --git a/show_bug.cgi b/show_bug.cgi index d77f91ae1..fe5e3c462 100755 --- a/show_bug.cgi +++ b/show_bug.cgi @@ -59,7 +59,7 @@ if (!$cgi->param('id') && $single) { if ($format_params->{format} eq 'modal') { my $bug_id = $cgi->param('id'); detaint_natural($bug_id); - $cgi->content_security_policy(Bugzilla::CGI::SHOW_BUG_MODAL_CSP($bug_id)); + $C->content_security_policy(SHOW_BUG_MODAL_CSP($bug_id)); } diff --git a/template/en/default/global/header.html.tmpl b/template/en/default/global/header.html.tmpl index 4ac9f7a3e..538295a69 100644 --- a/template/en/default/global/header.html.tmpl +++ b/template/en/default/global/header.html.tmpl @@ -108,12 +108,6 @@ [% END %] [% USE Bugzilla %] - [% IF Bugzilla.cgi.should_block_referrer %] - - [% ELSE %] - - [% END %] - [%- js_BUGZILLA = { config => { basepath => basepath, diff --git a/xmlrpc.cgi b/xmlrpc.cgi index 98c9ab2b9..35d91f8b5 100755 --- a/xmlrpc.cgi +++ b/xmlrpc.cgi @@ -45,7 +45,6 @@ my $stdout = capture_stdout { # for setting SOAPAction, which isn't used by XML-RPC. $server->on_action(sub { $server->handle_login(WS_DISPATCH, @_) })->handle(); }; -my $C = $Bugzilla::App::CGI::C; my ($header_str, $body) = split(/(?:\r\n\r\n|\n\n)/, $stdout, 2); my $headers = Mojo::Headers->new; $headers->parse("$header_str\r\n\r\n");