PAGI
view release on metacpan or search on metacpan
lib/PAGI/App/Proxy.pm view on Meta::CPAN
# Build headers
my @headers;
for my $h (@{$scope->{headers} // []}) {
next if lc($h->[0]) eq 'host'; # Replace host
push @headers, "$h->[0]: $h->[1]";
}
push @headers, "Host: $host:$port";
# Add X-Forwarded headers
push @headers, "X-Forwarded-For: $scope->{client}[0]" if $scope->{client};
push @headers, "X-Forwarded-Proto: $scope->{scheme}" if $scope->{scheme};
# Add extra headers
for my $name (keys %$extra_headers) {
push @headers, "$name: $extra_headers->{$name}";
}
if (length $body) {
push @headers, "Content-Length: " . length($body);
}
lib/PAGI/Middleware/ReverseProxy.pm view on Meta::CPAN
=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;
lib/PAGI/Middleware/ReverseProxy.pm view on Meta::CPAN
# 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]];
lib/PAGI/Server.pm view on Meta::CPAN
location / {
proxy_pass http://pagi_backend;
# Required for upstream keepalive:
proxy_http_version 1.1;
proxy_set_header Connection "";
# Forward client info (since PAGI can't see it over Unix socket):
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
B<Important:> The C<keepalive> directive is critical for performance. Without
it, nginx opens a new Unix socket connection for every request.
C<proxy_http_version 1.1> and C<proxy_set_header Connection ""> are required
for keepalive to work.
B<With TLS termination at nginx:>
lib/PAGI/Server.pm view on Meta::CPAN
port. The C<client> key is omitted entirely from the scope hashref (not set
to C<undef>). This is spec-compliant: the PAGI specification marks C<client>
as optional.
=item * B<C<server> is C<[$socket_path, undef]>>> â instead of C<[$host, $port]>.
=back
B<Middleware implications:> Any middleware that accesses C<< $scope->{client} >>
must check C<< exists $scope->{client} >> first. For client IP identification
behind a reverse proxy, use C<X-Forwarded-For> or C<X-Real-IP> headers
instead of C<< $scope->{client} >>. The C<PAGI::Middleware::XForwardedFor>
middleware (if available) handles this automatically.
B<Access log:> Unix socket connections log C<unix> as the client IP in the
access log instead of an IP address.
=head2 Stale Socket Cleanup
If a socket file already exists at the configured path (e.g., from a previous
crash), it is automatically removed before binding. This matches the behavior
t/middleware/11-url-handling.t view on Meta::CPAN
run_async { $wrapped->($scope, async sub { {} }, async sub {
my ($e) = @_; push @events, $e }) };
is $events[0]{status}, 200, 'excluded path not redirected';
};
# ===================
# ReverseProxy Middleware Tests
# ===================
subtest 'ReverseProxy - updates client from X-Forwarded-For' => sub {
my $proxy = PAGI::Middleware::ReverseProxy->new(
trusted_proxies => ['127.0.0.1'],
);
my $captured_scope;
my $app = async sub {
my ($scope, $receive, $send) = @_;
$captured_scope = $scope;
await $send->({ type => 'http.response.start', status => 200, headers => [] });
await $send->({ type => 'http.response.body', body => 'OK', more => 0 });
};
my $wrapped = $proxy->wrap($app);
my $scope = make_scope(
client => ['127.0.0.1', 12345],
headers => [['X-Forwarded-For', '203.0.113.50, 198.51.100.1']],
);
run_async { $wrapped->($scope, async sub { {} }, async sub { }) };
is $captured_scope->{client}[0], '203.0.113.50', 'client IP from X-Forwarded-For';
is $captured_scope->{original_client}[0], '127.0.0.1', 'original client preserved';
};
subtest 'ReverseProxy - updates scheme from X-Forwarded-Proto' => sub {
my $proxy = PAGI::Middleware::ReverseProxy->new(
trusted_proxies => ['127.0.0.1'],
);
my $captured_scope;
my $app = async sub {
t/middleware/11-url-handling.t view on Meta::CPAN
my $app = async sub {
my ($scope, $receive, $send) = @_;
$captured_scope = $scope;
await $send->({ type => 'http.response.start', status => 200, headers => [] });
await $send->({ type => 'http.response.body', body => 'OK', more => 0 });
};
my $wrapped = $proxy->wrap($app);
my $scope = make_scope(
client => ['192.168.1.100', 12345], # Not trusted
headers => [['X-Forwarded-For', '203.0.113.50']],
);
run_async { $wrapped->($scope, async sub { {} }, async sub { }) };
is $captured_scope->{client}[0], '192.168.1.100', 'client IP unchanged for untrusted proxy';
};
# ===================
# Healthcheck Middleware Tests
# ===================
( run in 0.612 second using v1.01-cache-2.11-cpan-39bf76dae61 )