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 )