diff options
Diffstat (limited to 'Bugzilla/Quantum')
-rw-r--r-- | Bugzilla/Quantum/CGI.pm | 161 | ||||
-rw-r--r-- | Bugzilla/Quantum/Plugin/BlockIP.pm | 43 | ||||
-rw-r--r-- | Bugzilla/Quantum/Plugin/Glue.pm | 112 | ||||
-rw-r--r-- | Bugzilla/Quantum/Plugin/Hostage.pm | 80 | ||||
-rw-r--r-- | Bugzilla/Quantum/SES.pm | 214 | ||||
-rw-r--r-- | Bugzilla/Quantum/Static.pm | 30 | ||||
-rw-r--r-- | Bugzilla/Quantum/Stdout.pm | 42 | ||||
-rw-r--r-- | Bugzilla/Quantum/Template.pm | 39 |
8 files changed, 721 insertions, 0 deletions
diff --git a/Bugzilla/Quantum/CGI.pm b/Bugzilla/Quantum/CGI.pm new file mode 100644 index 000000000..16c733686 --- /dev/null +++ b/Bugzilla/Quantum/CGI.pm @@ -0,0 +1,161 @@ +# 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. + +package Bugzilla::Quantum::CGI; +use Mojo::Base 'Mojolicious::Controller'; + +use CGI::Compile; +use Bugzilla::Constants qw(bz_locations); +use Bugzilla::Quantum::Stdout; +use File::Slurper qw(read_text); +use File::Spec::Functions qw(catfile); +use Sub::Name; +use Sub::Quote 2.005000; +use Try::Tiny; +use Taint::Util qw(untaint); +use Socket qw(AF_INET inet_aton); +use Sys::Hostname; +use English qw(-no_match_vars); + +our $C; +my %SEEN; + +sub load_all { + my ($class, $r) = @_; + + foreach my $file (glob '*.cgi') { + my $name = _file_to_method($file); + $class->load_one($name, $file); + $r->any("/$file")->to("CGI#$name"); + } +} + +sub load_one { + my ($class, $name, $file) = @_; + my $package = __PACKAGE__ . "::$name", + my $inner_name = "_$name"; + my $content = read_text( catfile( bz_locations->{cgi_path}, $file ) ); + $content = "package $package; $content"; + untaint($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; + my $wrapper = sub { + my ($c) = @_; + my $stdin = $c->_STDIN; + my $stdout = ''; + local $C = $c; + local %ENV = $c->_ENV($file); + local *STDIN; ## no critic (local) + local $CGI::Compile::USE_REAL_EXIT = 0; + local $PROGRAM_NAME = $file; + open STDIN, '<', $stdin->path or die "STDIN @{[$stdin->path]}: $!" if -s $stdin->path; + tie *STDOUT, 'Bugzilla::Quantum::Stdout', controller => $c; ## no critic (tie) + try { + Bugzilla->init_page(); + $inner->(); + } + catch { + die $_ unless ref $_ eq 'ARRAY' && $_->[0] eq "EXIT\n"; + } + finally { + untie *STDOUT; + $c->finish; + Bugzilla->_cleanup; ## no critic (private) + CGI::initialize_globals(); + }; + }; + + no strict 'refs'; ## no critic (strict) + *{$name} = subname($name, $wrapper); + return 1; +} + +sub _ENV { + my ($c, $script_name) = @_; + my $tx = $c->tx; + my $req = $tx->req; + my $headers = $req->headers; + my $content_length = $req->content->is_multipart ? $req->body_size : $headers->content_length; + my %env_headers = ( HTTP_COOKIE => '', HTTP_REFERER => '' ); + + for my $name ( @{ $headers->names } ) { + my $key = uc "http_$name"; + $key =~ s!\W!_!g; + $env_headers{$key} = $headers->header($name); + } + + my $remote_user; + if ( my $userinfo = $c->req->url->to_abs->userinfo ) { + $remote_user = $userinfo =~ /([^:]+)/ ? $1 : ''; + } + elsif ( my $authenticate = $headers->authorization ) { + $remote_user = $authenticate =~ /Basic\s+(.*)/ ? b64_decode $1 : ''; + $remote_user = $remote_user =~ /([^:]+)/ ? $1 : ''; + } + my $path_info = $c->param('PATH_INFO'); + my %captures = %{ $c->stash->{'mojo.captures'} // {} }; + foreach my $key (keys %captures) { + if ($key eq 'action' || $key eq 'PATH_INFO' || $key =~ /^REWRITE_/) { + delete $captures{$key}; + } + } + my $cgi_query = Mojo::Parameters->new(%captures); + $cgi_query->append($req->url->query); + + return ( + %ENV, + CONTENT_LENGTH => $content_length || 0, + CONTENT_TYPE => $headers->content_type || '', + GATEWAY_INTERFACE => 'CGI/1.1', + HTTPS => $req->is_secure ? 'YES' : 'NO', + %env_headers, + QUERY_STRING => $cgi_query->to_string, + PATH_INFO => $path_info ? "/$path_info" : '', + REMOTE_ADDR => $tx->remote_address, + REMOTE_HOST => gethostbyaddr( inet_aton( $tx->remote_address || '127.0.0.1' ), AF_INET ) || '', + REMOTE_PORT => $tx->remote_port, + REMOTE_USER => $remote_user || '', + REQUEST_METHOD => $req->method, + SCRIPT_NAME => "/$script_name", + SERVER_NAME => hostname, + SERVER_PORT => $tx->local_port, + SERVER_PROTOCOL => $req->is_secure ? 'HTTPS' : 'HTTP', # TODO: Version is missing + SERVER_SOFTWARE => __PACKAGE__, + ); +} + +sub _STDIN { + my $c = shift; + my $stdin; + + if ( $c->req->content->is_multipart ) { + $stdin = Mojo::Asset::File->new; + $stdin->add_chunk( $c->req->build_body ); + } + else { + $stdin = $c->req->content->asset; + } + + return $stdin if $stdin->isa('Mojo::Asset::File'); + return Mojo::Asset::File->new->add_chunk( $stdin->slurp ); +} + +sub _file_to_method { + my ($name) = @_; + $name =~ s/\./_/s; + $name =~ s/\W+/_/gs; + return $name; +} + + +1; diff --git a/Bugzilla/Quantum/Plugin/BlockIP.pm b/Bugzilla/Quantum/Plugin/BlockIP.pm new file mode 100644 index 000000000..fbfffad66 --- /dev/null +++ b/Bugzilla/Quantum/Plugin/BlockIP.pm @@ -0,0 +1,43 @@ +package Bugzilla::Quantum::Plugin::BlockIP; +use 5.10.1; +use Mojo::Base 'Mojolicious::Plugin'; + +use Bugzilla::Memcached; + +use constant BLOCK_TIMEOUT => 60*60; + +my $MEMCACHED = Bugzilla::Memcached->_new()->{memcached}; + +sub register { + my ( $self, $app, $conf ) = @_; + + $app->hook(before_routes => \&_before_routes); + $app->helper(block_ip => \&_block_ip); + $app->helper(unblock_ip => \&_unblock_ip); +} + +sub _block_ip { + my ($class, $ip) = @_; + $MEMCACHED->set("block_ip:$ip" => 1, BLOCK_TIMEOUT) if $MEMCACHED; +} + +sub _unblock_ip { + my ($class, $ip) = @_; + $MEMCACHED->delete("block_ip:$ip") if $MEMCACHED; +} + +sub _before_routes { + my ( $c ) = @_; + return if $c->stash->{'mojo.static'}; + + my $ip = $c->tx->remote_address; + if ($MEMCACHED && $MEMCACHED->get("block_ip:$ip")) { + $c->block_ip($ip); + $c->res->code(429); + $c->res->message("Too Many Requests"); + $c->res->body("Too Many Requests"); + $c->finish; + } +} + +1; diff --git a/Bugzilla/Quantum/Plugin/Glue.pm b/Bugzilla/Quantum/Plugin/Glue.pm new file mode 100644 index 000000000..54a360003 --- /dev/null +++ b/Bugzilla/Quantum/Plugin/Glue.pm @@ -0,0 +1,112 @@ +# 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. + +package Bugzilla::Quantum::Plugin::Glue; +use 5.10.1; +use Mojo::Base 'Mojolicious::Plugin'; + +use Try::Tiny; +use Bugzilla::Constants; +use Bugzilla::Quantum::Template; +use Bugzilla::Logging; +use Bugzilla::RNG (); +use JSON::MaybeXS qw(decode_json); + +sub register { + my ( $self, $app, $conf ) = @_; + + my %D; + if ($ENV{BUGZILLA_HTTPD_ARGS}) { + my $args = decode_json($ENV{BUGZILLA_HTTPD_ARGS}); + foreach my $arg (@$args) { + if ($arg =~ /^-D(\w+)$/) { + $D{$1} = 1; + } + else { + die "Unknown httpd arg: $arg"; + } + } + } + + # hypnotoad is weird and doesn't look for MOJO_LISTEN itself. + $app->config( + hypnotoad => { + listen => [ $ENV{MOJO_LISTEN} ], + }, + ); + + # Make sure each httpd child receives a different random seed (bug 476622). + # Bugzilla::RNG has one srand that needs to be called for + # every process, and Perl has another. (Various Perl modules still use + # the built-in rand(), even though we never use it in Bugzilla itself, + # so we need to srand() both of them.) + # Also, ping the dbh to force a reconnection. + Mojo::IOLoop->next_tick( + sub { + Bugzilla::RNG::srand(); + srand(); + eval { Bugzilla->dbh->ping }; + } + ); + + $app->hook( + before_dispatch => sub { + my ($c) = @_; + if ($D{HTTPD_IN_SUBDIR}) { + my $path = $c->req->url->path; + $path =~ s{^/bmo}{}s; + $c->req->url->path($path); + } + Log::Log4perl::MDC->put(request_id => $c->req->request_id); + } + ); + + Bugzilla::Extension->load_all(); + if ($app->mode ne 'development') { + Bugzilla->preload_features(); + DEBUG("preloading templates"); + Bugzilla->preload_templates(); + DEBUG("done preloading templates"); + } + $app->secrets([Bugzilla->localconfig->{side_wide_secret}]); + + $app->renderer->add_handler( + 'bugzilla' => sub { + my ( $renderer, $c, $output, $options ) = @_; + my $vars = delete $c->stash->{vars}; + + # Helpers + my %helper; + foreach my $method ( grep {m/^\w+\z/} keys %{ $renderer->helpers } ) { + my $sub = $renderer->helpers->{$method}; + $helper{$method} = sub { $c->$sub(@_) }; + } + $vars->{helper} = \%helper; + + # The controller + $vars->{c} = $c; + my $name = $options->{template}; + unless ($name =~ /\./) { + $name = sprintf '%s.%s.tmpl', $options->{template}, $options->{format}; + } + my $template = Bugzilla->template; + $template->process( $name, $vars, $output ) + or die $template->error; + } + ); + + $app->log( + MojoX::Log::Log4perl::Tiny->new( + logger => Log::Log4perl->get_logger(ref $app) + ) + ); +} + + + + +1; diff --git a/Bugzilla/Quantum/Plugin/Hostage.pm b/Bugzilla/Quantum/Plugin/Hostage.pm new file mode 100644 index 000000000..42a05a910 --- /dev/null +++ b/Bugzilla/Quantum/Plugin/Hostage.pm @@ -0,0 +1,80 @@ +package Bugzilla::Quantum::Plugin::Hostage; +use 5.10.1; +use Mojo::Base 'Mojolicious::Plugin'; + +sub _attachment_root { + my ($base) = @_; + return undef unless $base; + return $base =~ m{^https?://(?:bug)?\%bugid\%\.([a-zA-Z\.-]+)} + ? $1 + : undef; +} + +sub _attachment_host_regex { + my ($base) = @_; + return undef unless $base; + my $val = $base; + $val =~ s{^https?://}{}s; + $val =~ s{/$}{}s; + my $regex = quotemeta $val; + $regex =~ s/\\\%bugid\\\%/\\d+/g; + return qr/^$regex$/s; +} + +sub register { + my ( $self, $app, $conf ) = @_; + + $app->hook(before_routes => \&_before_routes); +} + +sub _before_routes { + my ( $c ) = @_; + state $urlbase = Bugzilla->localconfig->{urlbase}; + state $urlbase_uri = URI->new($urlbase); + state $urlbase_host = $urlbase_uri->host; + state $urlbase_host_regex = qr/^bug(\d+)\.\Q$urlbase_host\E$/; + state $attachment_base = Bugzilla->localconfig->{attachment_base}; + state $attachment_root = _attachment_root($attachment_base); + state $attachment_host_regex = _attachment_host_regex($attachment_base); + + my $stash = $c->stash; + my $req = $c->req; + my $url = $req->url->to_abs; + + return if $stash->{'mojo.static'}; + + my $hostname = $url->host; + return if $hostname eq $urlbase_host; + + my $path = $url->path; + return if $path eq '/__lbheartbeat__'; + + if ($attachment_base && $hostname eq $attachment_root) { + $c->redirect_to($urlbase); + return; + } + elsif ($attachment_base && $hostname =~ $attachment_host_regex) { + if ($path =~ m{^/attachment\.cgi}s) { + return; + } else { + my $new_uri = $url->clone; + $new_uri->scheme($urlbase_uri->scheme); + $new_uri->host($urlbase_host); + $c->redirect_to($new_uri); + return; + } + } + elsif (my ($id) = $hostname =~ $urlbase_host_regex) { + my $new_uri = $urlbase_uri->clone; + $new_uri->path('/show_bug.cgi'); + $new_uri->query_form(id => $id); + $c->redirect_to($new_uri); + return; + } + else { + $c->redirect_to($urlbase); + return; + } +} + +1;
\ No newline at end of file diff --git a/Bugzilla/Quantum/SES.pm b/Bugzilla/Quantum/SES.pm new file mode 100644 index 000000000..e36956b1d --- /dev/null +++ b/Bugzilla/Quantum/SES.pm @@ -0,0 +1,214 @@ +#!/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 lib qw(.. ../lib ../local/lib/perl5); + +use Bugzilla (); +use Bugzilla::Constants qw(ERROR_MODE_DIE); +use Bugzilla::Logging; +use Bugzilla::Mailer qw(MessageToMTA); +use Bugzilla::User (); +use Bugzilla::Util qw(html_quote remote_ip); +use JSON::MaybeXS qw(decode_json); +use LWP::UserAgent (); +use Try::Tiny qw(catch try); + +Bugzilla->error_mode(ERROR_MODE_DIE); +try { + main(); +} +catch { + FATAL("Fatal error: $_"); + respond( 500 => 'Internal Server Error' ); +}; + +sub main { + my $message = decode_json_wrapper( Bugzilla->cgi->param('POSTDATA') ) // return; + my $message_type = $ENV{HTTP_X_AMZ_SNS_MESSAGE_TYPE} // '(missing)'; + + if ( $message_type eq 'SubscriptionConfirmation' ) { + confirm_subscription($message); + } + + elsif ( $message_type eq 'Notification' ) { + my $notification = decode_json_wrapper( $message->{Message} ) // return; + unless ( + # https://docs.aws.amazon.com/ses/latest/DeveloperGuide/event-publishing-retrieving-sns-contents.html + handle_notification( $notification, 'eventType' ) + + # https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html + || handle_notification( $notification, 'notificationType' ) + ) + { + WARN('Failed to find notification type'); + respond( 400 => 'Bad Request' ); + } + } + + else { + WARN("Unsupported message-type: $message_type"); + respond( 200 => 'OK' ); + } +} + +sub confirm_subscription { + my ($message) = @_; + + my $subscribe_url = $message->{SubscribeURL}; + if ( !$subscribe_url ) { + WARN('Bad SubscriptionConfirmation request: missing SubscribeURL'); + respond( 400 => 'Bad Request' ); + return; + } + + my $ua = ua(); + my $res = $ua->get( $message->{SubscribeURL} ); + if ( !$res->is_success ) { + WARN( 'Bad response from SubscribeURL: ' . $res->status_line ); + respond( 400 => 'Bad Request' ); + return; + } + + respond( 200 => 'OK' ); +} + +sub handle_notification { + my ( $notification, $type_field ) = @_; + + if ( !exists $notification->{$type_field} ) { + return 0; + } + my $type = $notification->{$type_field}; + + if ( $type eq 'Bounce' ) { + process_bounce($notification); + } + elsif ( $type eq 'Complaint' ) { + process_complaint($notification); + } + else { + WARN("Unsupported notification-type: $type"); + respond( 200 => 'OK' ); + } + return 1; +} + +sub process_bounce { + my ($notification) = @_; + + # disable each account that is bouncing + foreach my $recipient ( @{ $notification->{bounce}->{bouncedRecipients} } ) { + my $address = $recipient->{emailAddress}; + my $reason = sprintf '(%s) %s', $recipient->{action} // 'error', $recipient->{diagnosticCode} // 'unknown'; + + my $user = Bugzilla::User->new( { name => $address, cache => 1 } ); + if ($user) { + + # never auto-disable admin accounts + if ( $user->in_group('admin') ) { + Bugzilla->audit("ignoring bounce for admin <$address>: $reason"); + } + + else { + my $template = Bugzilla->template_inner(); + my $vars = { + mta => $notification->{bounce}->{reportingMTA} // 'unknown', + reason => $reason, + }; + my $disable_text; + $template->process( 'admin/users/bounce-disabled.txt.tmpl', $vars, \$disable_text ) + || die $template->error(); + + $user->set_disabledtext($disable_text); + $user->set_disable_mail(1); + $user->update(); + Bugzilla->audit( "bounce for <$address> disabled userid-" . $user->id . ": $reason" ); + } + } + + else { + Bugzilla->audit("bounce for <$address> has no user: $reason"); + } + } + + respond( 200 => 'OK' ); +} + +sub process_complaint { + + # email notification to bugzilla admin + my ($notification) = @_; + my $template = Bugzilla->template_inner(); + my $json = JSON::MaybeXS->new( + pretty => 1, + utf8 => 1, + canonical => 1, + ); + + foreach my $recipient ( @{ $notification->{complaint}->{complainedRecipients} } ) { + my $reason = $notification->{complaint}->{complaintFeedbackType} // 'unknown'; + my $address = $recipient->{emailAddress}; + Bugzilla->audit("complaint for <$address> for '$reason'"); + my $vars = { + email => $address, + user => Bugzilla::User->new( { name => $address, cache => 1 } ), + reason => $reason, + notification => $json->encode($notification), + }; + my $message; + $template->process( 'email/ses-complaint.txt.tmpl', $vars, \$message ) + || die $template->error(); + MessageToMTA($message); + } + + respond( 200 => 'OK' ); +} + +sub respond { + my ( $code, $message ) = @_; + print Bugzilla->cgi->header( -status => "$code $message" ); + + # apache will generate non-200 response pages for us + say html_quote($message) if $code == 200; +} + +sub decode_json_wrapper { + my ($json) = @_; + my $result; + if ( !defined $json ) { + WARN( 'Missing JSON from ' . remote_ip() ); + respond( 400 => 'Bad Request' ); + return undef; + } + my $ok = try { + $result = decode_json($json); + } + catch { + WARN( 'Malformed JSON from ' . remote_ip() ); + respond( 400 => 'Bad Request' ); + return undef; + }; + return $ok ? $result : undef; +} + +sub ua { + my $ua = LWP::UserAgent->new(); + $ua->timeout(10); + $ua->protocols_allowed( [ 'http', 'https' ] ); + if ( my $proxy_url = Bugzilla->params->{'proxy_url'} ) { + $ua->proxy( [ 'http', 'https' ], $proxy_url ); + } + else { + $ua->env_proxy; + } + return $ua; +} diff --git a/Bugzilla/Quantum/Static.pm b/Bugzilla/Quantum/Static.pm new file mode 100644 index 000000000..2bb54990e --- /dev/null +++ b/Bugzilla/Quantum/Static.pm @@ -0,0 +1,30 @@ +# 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. + +package Bugzilla::Quantum::Static; +use Mojo::Base 'Mojolicious::Static'; +use Bugzilla::Constants qw(bz_locations); + +my $LEGACY_RE = qr{ + ^ (?:static/v[0-9]+\.[0-9]+/) ? + ( (?:extensions/[^/]+/web|(?:image|skin|j)s)/.+) + $ +}xs; + +sub file { + my ($self, $rel) = @_; + + if (my ($legacy_rel) = $rel =~ $LEGACY_RE) { + local $self->{paths} = [ bz_locations->{cgi_path} ]; + return $self->SUPER::file($legacy_rel); + } + else { + return $self->SUPER::file($rel); + } +} + +1; diff --git a/Bugzilla/Quantum/Stdout.pm b/Bugzilla/Quantum/Stdout.pm new file mode 100644 index 000000000..ee470a56a --- /dev/null +++ b/Bugzilla/Quantum/Stdout.pm @@ -0,0 +1,42 @@ +# 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. + +package Bugzilla::Quantum::Stdout; +use 5.10.1; +use Moo; + +has 'controller' => ( + is => 'ro', + required => 1, +); + +sub TIEHANDLE { ## no critic (unpack) + my $class = shift; + + return $class->new(@_); +} + +sub PRINTF { ## no critic (unpack) + my $self = shift; + $self->PRINT(sprintf @_); +} + +sub PRINT { ## no critic (unpack) + my $self = shift; + + foreach my $chunk (@_) { + my $str = "$chunk"; + utf8::encode($str); + $self->controller->write($str); + } +} + +sub BINMODE { + # no-op +} + +1;
\ No newline at end of file diff --git a/Bugzilla/Quantum/Template.pm b/Bugzilla/Quantum/Template.pm new file mode 100644 index 000000000..2442f1134 --- /dev/null +++ b/Bugzilla/Quantum/Template.pm @@ -0,0 +1,39 @@ +# 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. + +package Bugzilla::Quantum::Template; +use 5.10.1; +use Moo; + +has 'controller' => ( + is => 'ro', + required => 1, +); + +has 'template' => ( + is => 'ro', + required => 1, + handles => ['error', 'get_format'], +); + +sub process { + my ($self, $file, $vars, $output) = @_; + + if (@_ < 4) { + $self->controller->stash->{vars} = $vars; + $self->controller->render(template => $file, handler => 'bugzilla'); + return 1; + } + elsif (@_ == 4) { + return $self->template->process($file, $vars, $output); + } + else { + die __PACKAGE__ . '->process() called with too many arguments'; + } +} + +1;
\ No newline at end of file |