Reverse-Proxy
view release on metacpan or search on metacpan
t/07-headers.t view on Meta::CPAN
use strict;
use warnings;
use Test::More;
use IO::Socket::INET;
use Reverse::Proxy;
# Request-side header handling: hop-by-hop stripping (including names listed in
# the client's own Connection header), X-Forwarded-For chaining, X-Forwarded-Host,
# a pre-existing X-Forwarded-Proto, and the Via header (default and omitted).
#
# The backend echoes every request header it received back in the body, one
# "name: value" per line, so the test can assert exactly what was forwarded.
sub spawn_backend {
my $srv = IO::Socket::INET->new(
LocalHost => '127.0.0.1', LocalPort => 0, Listen => 16, ReuseAddr => 1,
) or die "listen: $!";
my $port = $srv->sockport;
my $pid = fork // die "fork: $!";
if (!$pid) {
while (my $c = $srv->accept) {
$c->autoflush(1);
my $line = <$c>; $line = '' unless defined $line;
my @hdrs;
while (my $h = <$c>) { $h =~ s/\r\n$//; last if $h eq ''; push @hdrs, $h }
my $out = join "\n", @hdrs;
print $c "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n"
. "Content-Length: " . length($out) . "\r\nConnection: close\r\n\r\n$out";
close $c;
}
exit 0;
}
return ($pid, $port);
}
sub make_env {
my ($hdrs) = @_;
open my $in, '<', \(my $b = '');
my %env = (
REQUEST_METHOD => 'GET', PATH_INFO => '/', QUERY_STRING => '',
SERVER_PROTOCOL => 'HTTP/1.1', REMOTE_ADDR => '203.0.113.7',
'psgi.version' => [1,1], 'psgi.url_scheme' => 'http',
'psgi.input' => $in, 'psgi.errors' => \*STDERR,
);
for my $k (keys %{ $hdrs || {} }) {
(my $n = uc $k) =~ tr/-/_/;
$env{"HTTP_$n"} = $hdrs->{$k};
}
return \%env;
}
# Parse the echoed header block into a lowercased name => value map.
sub seen_headers {
my ($body) = @_;
my %h;
for my $line (split /\n/, $body) {
next unless $line =~ /^([^:]+):\s*(.*)$/;
$h{ lc $1 } = $2;
}
return \%h;
}
my ($pid, $port) = spawn_backend();
select undef, undef, undef, 0.2;
my $app = Reverse::Proxy->new(upstream => "http://127.0.0.1:$port")->to_app;
# ---- hop-by-hop stripping (fixed set + Connection-named tokens) -----------
{
my $res = $app->(make_env({
'Connection' => 'close, X-Custom-Hop', # names X-Custom-Hop as hop
'X-Custom-Hop' => 'secret', # must be dropped
'Upgrade' => 'h2c', # hop-by-hop
'TE' => 'trailers', # hop-by-hop
'X-Keep' => 'yes', # ordinary -> forwarded
}));
is($res->[0], 200, 'request with hop-by-hop headers proxied 200');
my $h = seen_headers($res->[2][0]);
is($h->{'connection'}, undef, 'Connection stripped from forwarded request');
is($h->{'x-custom-hop'}, undef, 'header named in client Connection is stripped');
is($h->{'upgrade'}, undef, 'Upgrade stripped from forwarded request');
is($h->{'te'}, undef, 'TE stripped from forwarded request');
is($h->{'x-keep'}, 'yes', 'ordinary header forwarded through');
}
# ---- X-Forwarded-For chaining --------------------------------------------
{
my $res = $app->(make_env({ 'X-Forwarded-For' => '10.0.0.1' }));
my $h = seen_headers($res->[2][0]);
is($h->{'x-forwarded-for'}, '10.0.0.1, 203.0.113.7',
'existing X-Forwarded-For chained with REMOTE_ADDR');
}
# ---- X-Forwarded-Host from client Host -----------------------------------
{
my $res = $app->(make_env({ Host => 'client.example' }));
my $h = seen_headers($res->[2][0]);
is($h->{'x-forwarded-host'}, 'client.example',
'X-Forwarded-Host derived from client Host');
}
# ---- pre-existing X-Forwarded-Proto preserved ----------------------------
{
my $res = $app->(make_env({ 'X-Forwarded-Proto' => 'https' }));
my $h = seen_headers($res->[2][0]);
is($h->{'x-forwarded-proto'}, 'https',
'existing X-Forwarded-Proto preserved (not overwritten by url_scheme)');
}
# ---- Via header: default value -------------------------------------------
{
my $res = $app->(make_env({}));
my $h = seen_headers($res->[2][0]);
is($h->{'via'}, 'Reverse::Proxy', 'default Via header added');
}
# ---- Via header: omitted when via => undef -------------------------------
{
my $novia = Reverse::Proxy->new(
upstream => "http://127.0.0.1:$port", via => undef,
)->to_app;
my $res = $novia->(make_env({}));
my $h = seen_headers($res->[2][0]);
is($h->{'via'}, undef, 'via => undef omits the Via header');
}
END { if ($pid) { local $?; kill 'TERM', $pid; waitpid $pid, 0 } }
done_testing();
( run in 0.676 second using v1.01-cache-2.11-cpan-54e63673c56 )