Skip to content

Commit

Permalink
Merge remote branch 'hiratara/rewritelocation_mapped'
Browse files Browse the repository at this point in the history
  • Loading branch information
leedo committed Feb 17, 2011
2 parents 50f1fbe + 0fe45f8 commit 0129c88
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 6 deletions.
96 changes: 90 additions & 6 deletions lib/Plack/Middleware/Proxy/RewriteLocation.pm
Expand Up @@ -3,6 +3,35 @@ use strict;
use parent 'Plack::Middleware';

use Plack::Util;
use Plack::Util::Accessor 'url_map';
use URI;

sub _different_part($$) {
my ($from, $to) = @_;

while ($from =~ m{[^/]+(?:\://$|/$|$)}g) {
my $last_part = $&;
last unless $to =~ /\Q$last_part\E$/;

$from =~ s!\Q$last_part\E$!!;
$to =~ s!\Q$last_part\E$!!;
}

$from => $to;
}

sub new {
my $self = shift->SUPER::new( @_ );

# regularize the remote URLs in the URL map
if( my $m = $self->url_map ) {
for( my $i = 1; $i < @$m; $i += 2 ) {
$m->[$i] = $self->_regularize_url( $m->[$i] );
}
}

return $self;
}

sub call {
my($self, $env) = @_;
Expand All @@ -11,20 +40,55 @@ sub call {
my $respond = shift;

my $cb = $self->app->($env);
return $respond->( $cb ) unless ref $cb eq 'CODE';

$cb->(sub {
my $res = shift;
if ( my $location = Plack::Util::header_get($res->[1], 'Location') ) {
my $remote = ($env->{'plack.proxy.last_url'} =~ m!^(https?://[^/]*/)!)[0];
if ($remote && $env->{HTTP_HOST}) {
$location =~ s!^$remote!$env->{'psgi.url_scheme'}://$env->{HTTP_HOST}/!;

if ( $env->{HTTP_HOST} and my $location = Plack::Util::header_get($res->[1], 'Location') ) {

my @map;
if ($self->url_map) {
# regularize the format of the location so we can
# compare it correctly (some apps print this
# non-canonically)
$location = $self->_regularize_url( $location );

my $proxy = "$env->{'psgi.url_scheme'}://$env->{HTTP_HOST}";
my @url_map = @{$self->url_map};

while(my ($proxy_path, $remote) = splice @url_map, 0, 2) {
push @map, "$proxy$proxy_path" => $remote;
}
} else {
# Auto-guessing url_map
my $original_url = "$env->{'psgi.url_scheme'}://" .
"$env->{HTTP_HOST}$env->{REQUEST_URI}";
$original_url .= '?' . $env->{QUERY_STRING}
if defined $env->{QUERY_STRING} && $env->{QUERY_STRING};
@map = _different_part(
$original_url => $env->{'plack.proxy.last_url'}
);
}
Plack::Util::header_set($res->[1], 'Location' => $location);

while(my ($proxy_url, $remote) = splice @map, 0, 2) {
last if $location =~ s!^$remote!$proxy_url!;
}

$location =~ s!//$!/!; #< avoid double slashes

Plack::Util::header_set( $res->[1], 'Location' => $location );
}
return $respond->($res);

return $respond->( $res );
});
};
}

sub _regularize_url {
'' . URI->new( $_[1] )->canonical
}

1;

__END__
Expand All @@ -43,15 +107,35 @@ Plack::Middleware::Proxy::RewriteLocation - Rewrites redirect headers
Plack::App::Proxy->new(remote => "http://10.0.1.2:8080/")->to_app;
};
### or, if mounting (i.e. URLMap) the proxied site at /foo
builder {
enable "Proxy::RewriteLocation", url_map => [ '/foo' => http://10.0.1.2:8080' ];
mount '/foo' => Plack::App::Proxy->new(remote => "http://10.0.1.2:8080/")->to_app;
};
=head1 DESCRIPTION
Plack::Middleware::Proxy::RewriteLocation rewrites the C<Location>
header in the response when the remote host redirects using its own
headers, like mod_proxy's C<ProxyPassReverse> option.
=head1 OPTIONS
=over 4
=item url_map (arrayref)
If given, will account for mounted (URLMapped) Proxy apps when
rewriting C<Location> headers. Will be applied in order, stopping at the
first successful match with the remote C<Location>.
=back
=head1 AUTHOR
Tatsuhiko Miyagawa
Robert Buels
=head1 SEE ALSO
Expand Down
70 changes: 70 additions & 0 deletions t/middleware/rewrite-auto_guess.t
@@ -0,0 +1,70 @@
use strict;
use warnings;
use HTTP::Request::Common qw(GET);
use Plack::App::Proxy;
use Plack::App::Proxy::Test;
use Plack::Builder;
use Test::More;

sub test_rewriting_path($$$) {
my ($from, $to, $redirect_to) = @_;

s!/$!! for $from, $to;

test_proxy(
proxy => sub {
my ($host, $port) = @_;
return builder {
enable 'Proxy::RewriteLocation';
mount "$from/" => Plack::App::Proxy->new(
remote => "http://$host:$port$to"
);
};
},
app => sub {
my $env = shift;

if ($env->{PATH_INFO} eq "$to/redirect") {
return [
301,
[Location => "http://$env->{HTTP_HOST}$to$redirect_to"],
['Redirected']
];
}

return [
200,
[
"Content-Type" => "text/plain",
"X-Request-URI" => $env->{REQUEST_URI}
],
["OK\n"],
];
},
client => sub {
my $cb = shift;

my $res = $cb->(GET "http://localhost$from/redirect");
is $res->code, 301, 'got right status to redirect';
like $res->header('Location'),
qr!^http://[^/]+\Q$from$redirect_to\E$!,
'got right proxied redirect URL';

$res = $cb->(GET $res->header('Location'));
like $res->header('X-Request-URI'), qr!^\Q$to$redirect_to\E$!,
'arrived in the target http server'
},
);
}

test_rewriting_path "/" => "/", "/goal";
test_rewriting_path "/" => "/foo", "/goal";
test_rewriting_path "/foo" => "/", "/goal";
test_rewriting_path "/foo" => "/bar", "/goal";
test_rewriting_path "/bar" => "/foo/bar", "/goal";
test_rewriting_path "/foo/bar" => "/bar", "/goal";
test_rewriting_path "/foo/goal" => "/foo", "/goal";
test_rewriting_path "/foo" => "/foo/goal", "/goal";
test_rewriting_path "/foo" => "/bar", "/goal?param=999";

done_testing;
46 changes: 46 additions & 0 deletions t/middleware/rewrite-guess_fails.t
@@ -0,0 +1,46 @@
use strict;
use warnings;
use HTTP::Request::Common qw(GET);
use Plack::App::Proxy;
use Plack::App::Proxy::Test;
use Plack::Builder;
use Test::More;

test_proxy(
proxy => sub {
my ($host, $port) = @_;
return builder {
# We must specify url_map to avoid auto-guess failure.
# Otherwise, ":$port" will be changed even if the redirect URL
# isn't proxied. ("/goal" isn't mapped in this case.)
enable 'Proxy::RewriteLocation',
url_map => ['/foo' => "http://$host:$port/foo"];
mount "/foo" => Plack::App::Proxy->new(
remote => "http://$host:$port/foo"
);
};
},
app => sub {
my $env = shift;
my $no_proxied_url = "http://$env->{HTTP_HOST}/goal";
return [
301,
[
Location => $no_proxied_url,
'X-Original-Location' => $no_proxied_url,
],
['Redirected']
];
},
client => sub {
my $cb = shift;

my $res = $cb->(GET "http://localhost/foo/");
is $res->code, 301, 'got right status to redirect';
is $res->header('Location'), $res->header('X-Original-Location'),
"Don't rewrite outer paths.";
},
);


done_testing;
47 changes: 47 additions & 0 deletions t/middleware/rewrite.t
@@ -1,6 +1,7 @@
use strict;
use warnings;
use Plack::App::Proxy;
use Plack::Builder;
use Plack::Middleware::Proxy::RewriteLocation;
use Test::More;
use Plack::App::Proxy::Test;
Expand Down Expand Up @@ -36,4 +37,50 @@ test_proxy(
},
);


######

test_proxy(
proxy => sub {
my ( $host, $port ) = @_;
return builder {
enable 'Proxy::RewriteLocation', url_map => [
'/foo/bar' => "http://$host:$port/uuuugh",
'/foo' => "http://$host:$port/noggin",
];

mount '/foo' => Plack::App::Proxy->new( remote => "http://$host:$port/noggin" );
mount '/foo/bar' => sub { [ 402, [], ['oh hai'] ] };
};
},
app => sub {
my $env = shift;

unless( $env->{PATH_INFO} =~ m!^/noggin! ) {
return [ 404, [], 'Not found dude!' ];
}

if( $env->{PATH_INFO} eq '/noggin/redirect' ) {
return [ 301, [ Location => 'http://perl.org/' ], [ 'hi' ] ];
}

return [
301,
[ "Location" => "http://$env->{HTTP_HOST}/noggin/redirect", "X-Server-Port" => $env->{SERVER_PORT} ],
[ 'Redirected' ],
];
},
client => sub {
my $cb = shift;

my $res = $cb->( HTTP::Request->new( GET => "http://localhost/foo" ) );
is $res->code, 301, 'got right status for request at /foo';
my $port = $res->header('X-Server-Port');
like $res->header('Location'), qr!http://[^/]+/foo/redirect!, 'got right proxied redirect URL';

$res = $cb->(HTTP::Request->new(GET => 'http://localhost/foo/redirect'));
is $res->header('Location'), 'http://perl.org/', 'got right non-proxied redirect URL'
},
);

done_testing;

0 comments on commit 0129c88

Please sign in to comment.