PAGI
view release on metacpan or search on metacpan
lib/PAGI/App/Proxy.pm view on Meta::CPAN
=head1 SYNOPSIS
use PAGI::App::Proxy;
my $app = PAGI::App::Proxy->new(
backend => 'http://localhost:8080',
)->to_app;
=cut
sub new {
my ($class, %args) = @_;
my $backend = $args{backend} // 'http://localhost:8080';
my ($host, $port) = $backend =~ m{://([^:/]+)(?::(\d+))?};
$port //= 80;
return bless {
host => $host,
port => $port,
timeout => $args{timeout} // 30,
headers => $args{headers} // {},
}, $class;
}
sub to_app {
my ($self) = @_;
my $host = $self->{host};
my $port = $self->{port};
my $timeout = $self->{timeout};
my $extra_headers = $self->{headers};
return async sub {
my ($scope, $receive, $send) = @_;
die "Unsupported scope type: $scope->{type}" if $scope->{type} ne 'http';
# Build request
my $method = $scope->{method};
my $path = $scope->{path};
$path .= "?$scope->{query_string}" if $scope->{query_string};
# Collect body
my $body = '';
while (1) {
my $event = await $receive->();
last if $event->{type} ne 'http.request';
$body .= $event->{body} // '';
last unless $event->{more};
}
# 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);
}
my $request = "$method $path HTTP/1.1\r\n" . join("\r\n", @headers) . "\r\n\r\n" . $body;
# Connect to backend
my $sock = IO::Socket::INET->new(
PeerHost => $host,
PeerPort => $port,
Timeout => $timeout,
);
unless ($sock) {
await $send->({
type => 'http.response.start',
status => 502,
headers => [['content-type', 'text/plain']],
});
await $send->({ type => 'http.response.body', body => 'Bad Gateway', more => 0 });
return;
}
print $sock $request;
# Read response
my $response = '';
while (my $chunk = <$sock>) {
$response .= $chunk;
}
close $sock;
# Parse response (simple parsing)
my ($status_line, $rest) = split /\r?\n/, $response, 2;
my ($proto, $status, $reason) = split / /, $status_line, 3;
my ($header_block, $resp_body) = split /\r?\n\r?\n/, $rest, 2;
my @resp_headers;
for my $line (split /\r?\n/, $header_block // '') {
my ($name, $value) = split /:\s*/, $line, 2;
next unless $name;
push @resp_headers, [lc($name), $value];
}
await $send->({
type => 'http.response.start',
status => $status,
headers => \@resp_headers,
});
await $send->({ type => 'http.response.body', body => $resp_body // '', more => 0 });
};
}
( run in 2.204 seconds using v1.01-cache-2.11-cpan-39bf76dae61 )