PAGI
view release on metacpan or search on metacpan
lib/PAGI/Middleware/ReverseProxy.pm view on Meta::CPAN
package PAGI::Middleware::ReverseProxy;
use strict;
use warnings;
use parent 'PAGI::Middleware';
use Future::AsyncAwait;
=head1 NAME
PAGI::Middleware::ReverseProxy - Handle X-Forwarded-* headers from reverse proxies
=head1 SYNOPSIS
use PAGI::Middleware::Builder;
my $app = builder {
enable 'ReverseProxy',
trusted_proxies => ['127.0.0.1', '10.0.0.0/8'];
$my_app;
};
=head1 DESCRIPTION
PAGI::Middleware::ReverseProxy processes X-Forwarded-* headers from trusted
reverse proxies and updates the scope with the original client information.
=head1 CONFIGURATION
=over 4
=item * trusted_proxies (default: ['127.0.0.1', '::1'])
Arrayref of trusted proxy IP addresses or CIDR ranges.
=item * trust_all (default: 0)
If true, trust X-Forwarded headers from any source. Use with caution!
=back
=head1 HEADERS PROCESSED
=over 4
=item * X-Forwarded-For - Original client IP
=item * X-Forwarded-Proto - Original protocol (http/https)
=item * X-Forwarded-Host - Original Host header
=item * X-Forwarded-Port - Original port
=item * X-Real-IP - Alternative to X-Forwarded-For (nginx)
=back
=cut
sub _init {
my ($self, $config) = @_;
$self->{trusted_proxies} = $config->{trusted_proxies} // ['127.0.0.1', '::1'];
$self->{trust_all} = $config->{trust_all} // 0;
}
sub wrap {
my ($self, $app) = @_;
return async sub {
my ($scope, $receive, $send) = @_;
if ($scope->{type} ne 'http') {
await $app->($scope, $receive, $send);
return;
}
# Check if request is from trusted proxy
my $client_ip = exists $scope->{client} ? ($scope->{client}[0] // '') : '';
unless ($self->{trust_all} || $self->_is_trusted($client_ip)) {
await $app->($scope, $receive, $send);
return;
}
# Build modified scope
my %new_scope = %$scope;
# X-Forwarded-For or X-Real-IP
my $forwarded_for = $self->_get_header($scope, 'x-forwarded-for');
my $real_ip = $self->_get_header($scope, 'x-real-ip');
if ($forwarded_for) {
# Take the leftmost IP (original client)
my ($original_ip) = split /\s*,\s*/, $forwarded_for;
$original_ip =~ s/^\s+//;
$original_ip =~ s/\s+$//;
if (exists $scope->{client}) {
$new_scope{client} = [$original_ip, $scope->{client}[1]];
$new_scope{original_client} = $scope->{client};
} else {
$new_scope{client} = [$original_ip, undef];
}
} elsif ($real_ip) {
if (exists $scope->{client}) {
$new_scope{client} = [$real_ip, $scope->{client}[1]];
$new_scope{original_client} = $scope->{client};
} else {
$new_scope{client} = [$real_ip, undef];
}
}
# X-Forwarded-Proto
my $forwarded_proto = $self->_get_header($scope, 'x-forwarded-proto');
if ($forwarded_proto) {
$forwarded_proto = lc($forwarded_proto);
$new_scope{scheme} = $forwarded_proto if $forwarded_proto =~ /^https?$/;
}
# X-Forwarded-Host
my $forwarded_host = $self->_get_header($scope, 'x-forwarded-host');
if ($forwarded_host) {
# Update headers with new Host
my @new_headers;
for my $h (@{$scope->{headers} // []}) {
if (lc($h->[0]) eq 'host') {
push @new_headers, ['host', $forwarded_host];
} else {
push @new_headers, $h;
}
}
$new_scope{headers} = \@new_headers;
}
# X-Forwarded-Port
my $forwarded_port = $self->_get_header($scope, 'x-forwarded-port');
if ($forwarded_port && $forwarded_port =~ /^\d+$/) {
$new_scope{server} = [$scope->{server}[0], int($forwarded_port)];
}
await $app->(\%new_scope, $receive, $send);
};
}
sub _is_trusted {
my ($self, $ip) = @_;
for my $trusted (@{$self->{trusted_proxies}}) {
if ($trusted =~ m{/}) {
( run in 2.042 seconds using v1.01-cache-2.11-cpan-39bf76dae61 )