diff --git a/cpanfile b/cpanfile index 3933401cf..183d4fe3f 100644 --- a/cpanfile +++ b/cpanfile @@ -30,6 +30,8 @@ requires 'Path::Tiny'; requires 'Plack', '1.0040'; requires 'Plack::Middleware::FixMissingBodyInRedirect'; requires 'Plack::Middleware::RemoveRedundantBody'; +requires 'Plack::Middleware::ReverseProxy'; +requires 'Plack::Middleware::ReverseProxyPath'; requires 'POSIX'; requires 'Ref::Util'; requires 'Safe::Isa'; diff --git a/lib/Dancer2/Core/App.pm b/lib/Dancer2/Core/App.pm index 6c9ea6737..98e418d18 100644 --- a/lib/Dancer2/Core/App.pm +++ b/lib/Dancer2/Core/App.pm @@ -17,6 +17,7 @@ use Plack::Middleware::FixMissingBodyInRedirect; use Plack::Middleware::Head; use Plack::Middleware::Conditional; use Plack::Middleware::ConditionalGET; +use Dancer2::Middleware::BehindProxy; use Dancer2::FileUtils 'path'; use Dancer2::Core; @@ -1419,10 +1420,15 @@ sub to_app { # Wrap with common middleware if ( ! $self->config->{'no_default_middleware'} ) { + # BehindProxy (this is not runtime configurable) + $self->config->{'behind_proxy'} + and $psgi = Dancer2::Middleware::BehindProxy->wrap($psgi); + # FixMissingBodyInRedirect - $psgi = Plack::Middleware::FixMissingBodyInRedirect->wrap( $psgi ); + $psgi = Plack::Middleware::FixMissingBodyInRedirect->wrap($psgi); + # Apply Head. After static so a HEAD request on static content DWIM. - $psgi = Plack::Middleware::Head->wrap( $psgi ); + $psgi = Plack::Middleware::Head->wrap($psgi); } return $psgi; @@ -1587,12 +1593,11 @@ sub build_request { # If we have an app, send the serialization engine my $request = Dancer2::Core::Request->new( - env => $env, - is_behind_proxy => $self->settings->{'behind_proxy'} || 0, + env => $env, - $self->has_serializer_engine - ? ( serializer => $self->serializer_engine ) - : (), + $self->has_serializer_engine + ? ( serializer => $self->serializer_engine ) + : (), ); return $request; diff --git a/lib/Dancer2/Core/Request.pm b/lib/Dancer2/Core/Request.pm index b545437ab..c7b7879ad 100644 --- a/lib/Dancer2/Core/Request.pm +++ b/lib/Dancer2/Core/Request.pm @@ -44,7 +44,7 @@ our $XS_HTTP_COOKIES = eval { require_module('HTTP::XSCookies'); 1; }; our $_id = 0; -# self->new( env => {}, serializer => $s, is_behind_proxy => 0|1 ) +# self->new( env => {}, serializer => $s ) sub new { my ( $class, @args ) = @_; @@ -65,9 +65,8 @@ sub new { } # additionally supported attributes - $self->{'id'} = ++$_id; - $self->{'vars'} = {}; - $self->{'is_behind_proxy'} = !!$opts{'is_behind_proxy'}; + $self->{'id'} = ++$_id; + $self->{'vars'} = {}; $opts{'body_params'} and $self->{'_body_params'} = $opts{'body_params'}; @@ -130,40 +129,17 @@ sub _set_route_params { # XXX: incompatible with Plack::Request sub uploads { $_[0]->{'uploads'} } -sub is_behind_proxy { $_[0]->{'is_behind_proxy'} || 0 } - sub host { - my ($self) = @_; - - if ( $self->is_behind_proxy and exists $self->env->{'HTTP_X_FORWARDED_HOST'} ) { - my @hosts = split /\s*,\s*/, $self->env->{'HTTP_X_FORWARDED_HOST'}, 2; - return $hosts[0]; - } else { - return $self->env->{'HTTP_HOST'}; - } + return shift->env->{'HTTP_HOST'}; } # aliases, kept for backward compat sub agent { shift->user_agent } sub remote_address { shift->address } + sub forwarded_for_address { shift->env->{'HTTP_X_FORWARDED_FOR'} } sub forwarded_host { shift->env->{'HTTP_X_FORWARDED_HOST'} } - -# there are two options -sub forwarded_protocol { - $_[0]->env->{'HTTP_X_FORWARDED_PROTO'} || - $_[0]->env->{'HTTP_X_FORWARDED_PROTOCOL'} || - $_[0]->env->{'HTTP_FORWARDED_PROTO'} -} - -sub scheme { - my ($self) = @_; - my $scheme = $self->is_behind_proxy - ? $self->forwarded_protocol - : ''; - - return $scheme || $self->env->{'psgi.url_scheme'}; -} +sub forwarded_protocol { shift->env->{'HTTP_X_FORWARDED_PROTO'} } sub serializer { $_[0]->{'serializer'} } @@ -589,8 +565,7 @@ sub _shallow_clone { $new_request->{headers} = $self->headers; # Copy remaining settings - $new_request->{is_behind_proxy} = $self->{is_behind_proxy}; - $new_request->{vars} = $self->{vars}; + $new_request->{vars} = $self->{vars}; # Clone any existing decoded & cached body params. (GH#1116 GH#1269) $new_request->{'body_parameters'} = $self->body_parameters->clone; diff --git a/lib/Dancer2/Core/Runner.pm b/lib/Dancer2/Core/Runner.pm index ceea52e02..b76f224ad 100644 --- a/lib/Dancer2/Core/Runner.pm +++ b/lib/Dancer2/Core/Runner.pm @@ -7,7 +7,6 @@ use Module::Runtime 'require_module'; use Dancer2::Core::MIME; use Dancer2::Core::Types; use Dancer2::Core::Dispatcher; -use Plack::Builder qw(); use Ref::Util qw< is_ref is_regexpref >; # Hashref of configurable items for the runner. diff --git a/lib/Dancer2/Middleware/BehindProxy.pm b/lib/Dancer2/Middleware/BehindProxy.pm new file mode 100644 index 000000000..a4adf00ff --- /dev/null +++ b/lib/Dancer2/Middleware/BehindProxy.pm @@ -0,0 +1,58 @@ +package Dancer2::Middleware::BehindProxy; +# ABSTRACT: Support Dancer2 apps when operating behing a reverse proxy + +use warnings; +use strict; + +use parent 'Plack::Middleware'; +use Plack::Middleware::ReverseProxy; +use Plack::Middleware::ReverseProxyPath; + +sub call { + my($self, $env) = @_; + + # Plack::Middleware::ReverseProxy only supports + # HTTP_X_FORWARDED_PROTO whereas Dancer2 also supports + # HTTP_X_FORWARDED_PROTOCOL and HTTP_FORWARDED_PROTO + for my $header (qw/HTTP_X_FORWARDED_PROTOCOL HTTP_FORWARDED_PROTO/) { + if ( ! $env->{HTTP_X_FORWARDED_PROTO} + && $env->{$header} ) + { + $env->{HTTP_X_FORWARDED_PROTO} = $env->{$header}; + last; + } + } + + # Pr#503 added support for HTTP_X_FORWARDED_HOST containing multiple + # values. Plack::Middleware::ReverseProxy takes the last (most recent) + # whereas that #503 takes the first. + if ( $env->{HTTP_X_FORWARDED_HOST} ) { + my @hosts = split /\s*,\s*/, $env->{HTTP_X_FORWARDED_HOST}, 2; + $env->{HTTP_X_FORWARDED_HOST} = $hosts[0]; + } + + # Plack::Middleware::ReverseProxyPath uses X-Forwarded-Script-Name + # whereas Dancer previously supported HTTP_REQUEST_BASE + if ( ! $env->{HTTP_X_FORWARDED_SCRIPT_NAME} + && $env->{HTTP_REQUEST_BASE} ) + { + $env->{HTTP_X_FORWARDED_SCRIPT_NAME} = $env->{HTTP_REQUEST_BASE}; + } + + # Wrap in reverse proxy middleware and call the wrapped app + my $app = Plack::Middleware::ReverseProxyPath->wrap($self->app); + $app = Plack::Middleware::ReverseProxy->wrap($app); + return $app->($env); +} + +1; + +__END__ + +=head1 DESCRIPTION + +Modifies request headers supported by L altered by reverse proxies before +wraping the request in the commonly used reverse proxy PSGI middlewares; +L and L. + +=cut diff --git a/t/issues/gh-730.t b/t/issues/gh-730.t index adc519cf1..7c74b11b7 100644 --- a/t/issues/gh-730.t +++ b/t/issues/gh-730.t @@ -8,7 +8,7 @@ use HTTP::Request::Common; package App; use Dancer2; - get '/' => sub { request->is_behind_proxy }; + get '/' => sub { app->config->{'behind_proxy'} }; } my $app = App->to_app; diff --git a/t/request.t b/t/request.t index 4e087f983..92e1573bb 100644 --- a/t/request.t +++ b/t/request.t @@ -97,31 +97,6 @@ sub run_test { is $req->base, 'http://oddhostname:5000/foo'; } - note "testing behind proxy"; { - my $req = Dancer2::Core::Request->new( - env => $env, - is_behind_proxy => 1 - ); - is $req->secure, 1; - is $req->host, $env->{HTTP_X_FORWARDED_HOST}; - is $req->scheme, 'https'; - } - - note "testing behind proxy when optional headers are not set"; { - # local modifications to env: - local $env->{HTTP_HOST} = 'oddhostname:5000'; - delete local $env->{'HTTP_X_FORWARDED_FOR'}; - delete local $env->{'HTTP_X_FORWARDED_HOST'}; - delete local $env->{'HTTP_X_FORWARDED_PROTOCOL'}; - my $req = Dancer2::Core::Request->new( - env => $env, - is_behind_proxy => 1 - ); - is ! $req->secure, 1; - is $req->host, 'oddhostname:5000'; - is $req->scheme, 'http'; - } - note "testing path and uri_base"; { # Base env used for path and uri_base tests my $base = {