FU: Add FastCGI support + bunch of fixes
I initially planned to only implement the bare minimum to support FastCGI under nginx, but ended up implementing the full protocol instead. This is more code than I had expected and the code is also less trivial than I had hoped. Will need to do more testing, pretty sure there's bugs left. Also TODO: test under alternative process managers + document FU_LISTEN_PROTO. I've also removed the max_request_body setting, this is something that really ought to be configured in the web server instead.
This commit is contained in:
parent
3e84a4f4d3
commit
d9fba4e8d8
4 changed files with 784 additions and 79 deletions
232
FU.pm
232
FU.pm
|
|
@ -25,7 +25,6 @@ sub fu() { $fu }
|
|||
sub debug { state $v = 0; $v = $_[0] if @_; $v }
|
||||
sub log_slow_pages { state $v = 0; $v = $_[0] if @_; $v }
|
||||
sub log_queries { state $v = 0; $v = $_[0] if @_; $v }
|
||||
sub max_request_body { state $v = 10*1024*1024; $v = $_[0] if @_; $v }
|
||||
|
||||
sub mime_types() { state $v = {qw{
|
||||
7z application/x-7z-compressed
|
||||
|
|
@ -136,18 +135,7 @@ my %onerr = (
|
|||
Congratulations!</small>
|
||||
_
|
||||
},
|
||||
413 => sub {
|
||||
fu->_error_page(413, '413 - Request Entity Too Large', <<~_);
|
||||
That's an odd way of saying that you were probably trying to upload a large
|
||||
file. Too large, in fact, for the server to handle. If you believe this
|
||||
error to be mistaken, you can ask the site admin to increase the maximum
|
||||
allowed upload size.
|
||||
_
|
||||
},
|
||||
500 => \&_err_500,
|
||||
'*' => sub($code, @) {
|
||||
fu->_error_page($code, "$code - Unknown error", 'Welp, something went wrong processing your request.');
|
||||
},
|
||||
);
|
||||
sub on_error :prototype($&) { $onerr{$_[0]} = $_[1] }
|
||||
|
||||
|
|
@ -176,25 +164,25 @@ sub _monitor {
|
|||
|
||||
|
||||
sub _decode_utf8 {
|
||||
return if !defined $_[0];
|
||||
fu->error(400, 'Invalid UTF-8 in request') if !utf8::decode($_[0]);
|
||||
# Disallow any control codes, except for x09 (tab), x0a (newline) and x0d (carriage return)
|
||||
fu->error(400, 'Invalid control character in request') if $_[0] =~ /[\x00-\x08\x0b\x0c\x0e-\x1f]/;
|
||||
}
|
||||
|
||||
our $hdrname_re = qr/[!#\$\%&'\*\+-\.^_`\|~0-9a-zA-Z]+/;
|
||||
our $hdrname_re = qr/[!#\$\%&'\*\+-\.^_`\|~0-9a-zA-Z]{1,127}/;
|
||||
our $method_re = qr/(?:GET|POST|DELETE|OPTIONS|PUT|PATCH|QUERY)/;
|
||||
|
||||
# rfc7230 used as reference, though strict conformance is not a goal.
|
||||
# Does not limit size of headers, so not suitable for deployment in untrusted networks.
|
||||
sub _http_read_request($sock, $req) {
|
||||
sub _read_req_http($sock, $req) {
|
||||
local $/ = "\r\n";
|
||||
my $line = $sock->getline;
|
||||
fu->error(400, 'Client disconnect before request was read') if !defined $line;
|
||||
fu->error(400, 'Invalid request') if $line !~ /^(GET|POST|DELETE|OPTIONS|PUT|PATCH|QUERY)\s+(\S+)\s+HTTP\/1\.[01]\r\n$/;
|
||||
fu->error(400, 'Invalid request') if $line !~ /^($method_re)\s+(\S+)\s+HTTP\/1\.[01]\r\n$/;
|
||||
$req->{method} = $1;
|
||||
($req->{path}, $req->{qs}) = split /\?/, $2 =~ s{^https?://[^/]+/}{/}r, 2;
|
||||
$req->{path} =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
|
||||
_decode_utf8 $req->{path};
|
||||
_decode_utf8 $req->{qs} if defined $req->{qs};
|
||||
|
||||
while (1) {
|
||||
# Turns out header line folding has been officially deprecated, so I'm
|
||||
|
|
@ -204,7 +192,6 @@ sub _http_read_request($sock, $req) {
|
|||
last if $line eq "\r\n";
|
||||
fu->error(400, 'Invalid request header syntax') if $line !~ /^($hdrname_re):\s*(.+)\r\n$/;
|
||||
my($hdr, $val) = (lc $1, $2 =~ s/\s*$//r);
|
||||
_decode_utf8 $val;
|
||||
if (exists $req->{hdr}{$hdr}) {
|
||||
$req->{hdr}{$hdr} .= ($hdr eq 'cookie' ? '; ' : ', ') . $val;
|
||||
} else {
|
||||
|
|
@ -215,7 +202,6 @@ sub _http_read_request($sock, $req) {
|
|||
fu->error(400, 'Unexpected Transfer-Encoding request header') if $req->{hdr}{'transfer-encoding'};
|
||||
my $len = $req->{hdr}{'content-length'} // 0;
|
||||
fu->error(400, 'Invalid Content-Length request header') if $len !~ /^(?:0|[1-9][0-9]*)$/;
|
||||
fu->error(413, 'Request body too large') if $len > max_request_body;
|
||||
|
||||
$req->{body} = '';
|
||||
while ($len > 0) {
|
||||
|
|
@ -225,6 +211,34 @@ sub _http_read_request($sock, $req) {
|
|||
}
|
||||
|
||||
|
||||
sub _read_req($c) {
|
||||
if ($c->{fcgi_obj}) {
|
||||
my $r = $c->{fcgi_obj}->read_req($REQ->{hdr}, $REQ);
|
||||
# Only FUFE_ABORT is an error we can recover from, in all other
|
||||
# cases we have not properly consumed the request from the socket
|
||||
# so we'll leave the protocol in an invalid state in case we do
|
||||
# attempt to respond.
|
||||
# All other errors suggest a misconfigured web server, anyway.
|
||||
if ($r == -6) { fu->error(400, 'Client disconnect before request was read') }
|
||||
elsif ($r) {
|
||||
warn $r == -1 ? "Unexpected EOF while reading from FastCGI socket\n"
|
||||
: $r == -2 ? "I/O error while reading from FastCGI socket\n"
|
||||
: $r == -3 ? "FastCGI protocol error\n"
|
||||
: $r == -4 ? "Too long FastCGI parameter\n"
|
||||
: $r == -5 ? "Too long request body\n" : undef;
|
||||
delete $c->{fcgi_obj};
|
||||
fu->error(-1);
|
||||
}
|
||||
fu->error(400, 'Invalid request') if !$REQ->{method} || $REQ->{method} !~ /^$method_re$/ || !$REQ->{path};
|
||||
} else {
|
||||
_read_req_http($c->{client_sock}, $REQ);
|
||||
}
|
||||
|
||||
# The HTTP reader above and the FastCGI XS reader operate on bytes.
|
||||
# Decode these into Unicode strings and check for special characters.
|
||||
_decode_utf8 $_ for ($REQ->{path}, $REQ->{qs}, values $REQ->{hdr}->%*);
|
||||
}
|
||||
|
||||
|
||||
sub _is_done($e) { ref $@ eq 'FU::err' && $@->[0] == 200 }
|
||||
|
||||
|
|
@ -235,20 +249,14 @@ sub _log_err($e) {
|
|||
}
|
||||
|
||||
sub _do_req($c) {
|
||||
if ($c->{monitor} && _monitor) {
|
||||
warn "File change detected, restarting process.\n" if debug;
|
||||
FU::Util::fdpass_send(fileno($c->{supervisor_sock}), fileno($c->{client_sock}), 'f0000');
|
||||
exit;
|
||||
}
|
||||
|
||||
local $REQ = {};
|
||||
local $REQ = { hdr => {} };
|
||||
local $fu = bless {}, 'FU::obj';
|
||||
|
||||
$REQ->{ip} = $c->{client_sock} isa 'IO::Socket::INET' ? $c->{client_sock}->peerhost : '127.0.0.1';
|
||||
fu->reset;
|
||||
|
||||
my $ok = eval {
|
||||
_http_read_request($c->{client_sock}, $REQ);
|
||||
_read_req $c;
|
||||
|
||||
for my $h (@before_request) { $h->() }
|
||||
|
||||
|
|
@ -266,6 +274,7 @@ sub _do_req($c) {
|
|||
}
|
||||
1;
|
||||
};
|
||||
return if !$ok && ref $@ eq 'FU::err' && $@->[0] == -1;
|
||||
my $err = $ok || _is_done($@) ? undef : $@;
|
||||
_log_err $err;
|
||||
|
||||
|
|
@ -283,24 +292,51 @@ sub _do_req($c) {
|
|||
|
||||
if ($err) {
|
||||
fu->reset;
|
||||
my($code, $msg) = ref $@ eq 'FU::err' ? $@->@* : (500, $err);
|
||||
my($code, $msg) = ref $err eq 'FU::err' ? $err->@* : (500, $err);
|
||||
eval {
|
||||
($onerr{$code} || $onerr{'*'})->($code, $msg);
|
||||
($onerr{$code} || $onerr{500})->($code, $msg);
|
||||
1;
|
||||
} || _err_500();
|
||||
}
|
||||
|
||||
fu->_flush($c->{client_sock});
|
||||
$c->{client_sock}->close;
|
||||
|
||||
exit if $c->{max_reqs} && !--$c->{max_reqs};
|
||||
fu->_flush($c->{fcgi_obj} || $c->{client_sock});
|
||||
}
|
||||
|
||||
|
||||
sub _run_loop($c) {
|
||||
my $stop = 0;
|
||||
local $SIG{HUP} = 'IGNORE';
|
||||
local $SIG{TERM} = $SIG{INT} = sub { $stop = 1 };
|
||||
|
||||
my sub passclient {
|
||||
FU::Util::fdpass_send(fileno($c->{supervisor_sock}), fileno($c->{client_sock}), 'f0000')
|
||||
if $c->{supervisor_sock} && $c->{client_sock};
|
||||
exit;
|
||||
}
|
||||
|
||||
while (!$stop) {
|
||||
$c->{client_sock} ||= $c->{listen_sock}->accept || next;
|
||||
$c->{fcgi_obj} ||= $c->{listen_proto} eq 'fcgi' && FU::fcgi::new(fileno $c->{client_sock}, $c->{proc});
|
||||
|
||||
if ($c->{monitor} && _monitor) {
|
||||
warn "File change detected, restarting process.\n" if debug;
|
||||
passclient;
|
||||
}
|
||||
|
||||
_do_req $c;
|
||||
|
||||
$c->{client_sock} = $c->{fcgi_obj} = undef if !($c->{fcgi_obj} && $c->{fcgi_obj}->keepalive);
|
||||
|
||||
passclient if $c->{max_reqs} && !--$c->{max_reqs};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sub _supervisor($c) {
|
||||
my ($rsock, $wsock) = IO::Socket->socketpair(IO::Socket::AF_UNIX(), IO::Socket::SOCK_STREAM(), IO::Socket::PF_UNSPEC());
|
||||
|
||||
my %childs; # pid => 1: spawned, 2: signalled ready
|
||||
$SIG{CHLD} = sub { $wsock->syswrite('c0000',1) };
|
||||
$SIG{CHLD} = sub { $wsock->syswrite('c0000',5) };
|
||||
$SIG{HUP} = $SIG{TERM} = $SIG{INT} = sub($sig,@) {
|
||||
kill 'TERM', keys %childs;
|
||||
return if $sig eq 'HUP';
|
||||
|
|
@ -314,13 +350,16 @@ sub _supervisor($c) {
|
|||
fcntl $wsock, Fcntl::F_SETFD(), 0;
|
||||
|
||||
$ENV{FU_MONITOR} = $c->{monitor};
|
||||
$ENV{FU_PROC} = $c->{proc};
|
||||
$ENV{FU_MAX_REQS} = $c->{max_reqs};
|
||||
$ENV{FU_DEBUG} = debug;
|
||||
$ENV{FU_SUPERVISOR_FD} = fileno $wsock;
|
||||
$ENV{FU_LISTEN_FD} = fileno $c->{listen_sock};
|
||||
$ENV{FU_LISTEN_PROTO} = $c->{listen_proto};
|
||||
|
||||
my $err = 0;
|
||||
my @client_fd;
|
||||
my $msg = '';
|
||||
while (1) {
|
||||
while ((my $pid = waitpid(-1, POSIX::WNOHANG())) > 0) {
|
||||
$err = 1 if POSIX::WIFEXITED($?) && POSIX::WEXITSTATUS($?) != 0;
|
||||
|
|
@ -344,9 +383,9 @@ sub _supervisor($c) {
|
|||
} elsif ($err) {
|
||||
# In error state, wait with loading the script until we've received a request.
|
||||
# Otherwise we'll end up in an infinite spawning loop if the script doesn't start properly.
|
||||
my $sock = $c->{listen_sock}->accept() or die $!;
|
||||
fcntl $sock, Fcntl::F_SETFD, 0 if $sock;
|
||||
$ENV{FU_CLIENT_FD} = fileno $sock;
|
||||
$client = $c->{listen_sock}->accept() or die $!;
|
||||
fcntl $client, Fcntl::F_SETFD, 0;
|
||||
$ENV{FU_CLIENT_FD} = fileno $client;
|
||||
}
|
||||
exec $^X, (map "-I$_", @INC), $0;
|
||||
exit 1;
|
||||
|
|
@ -355,21 +394,24 @@ sub _supervisor($c) {
|
|||
$childs{$pid} = 1;
|
||||
}
|
||||
|
||||
# Assumption: we never get short reads.
|
||||
my ($fd, $msg) = FU::Util::fdpass_recv(fileno($rsock), 5);
|
||||
my ($fd, $msgadd) = FU::Util::fdpass_recv(fileno($rsock), 500);
|
||||
push @client_fd, $fd if $fd;
|
||||
next if !$msg;
|
||||
next if $msg eq 'c0000'; # child died
|
||||
next if $msg eq 'f0000'; # child is about to exit and passed a client fd to us
|
||||
|
||||
if ($msg =~ /^r/) { # child ready
|
||||
my $pid = unpack 'V', substr $msg, 1;
|
||||
$childs{$pid} = 2 if $childs{$pid};
|
||||
$err = 0;
|
||||
next if !defined $msgadd;
|
||||
$msg .= $msgadd;
|
||||
while ($msg =~ s/^(.)(....)//s) {
|
||||
my($cmd, $arg) = ($1, $2);
|
||||
next if $cmd eq 'c'; # child died
|
||||
next if $cmd eq 'f'; # child is about to exit and passed a client fd to us
|
||||
if ($cmd eq 'r') { # child ready
|
||||
my $pid = unpack 'V', $arg;
|
||||
$childs{$pid} = 2 if $childs{$pid};
|
||||
$err = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sub _spawn {
|
||||
state %c;
|
||||
return if keys %c && !@_; # already checked if we need to spawn
|
||||
|
|
@ -381,6 +423,7 @@ sub _spawn {
|
|||
proc => $ENV{FU_PROC} // 1,
|
||||
monitor => $ENV{FU_MONITOR} // 0,
|
||||
max_reqs => $ENV{FU_MAX_REQS} // 0,
|
||||
listen_proto => $ENV{FU_LISTEN_PROTO},
|
||||
listen_sock => $ENV{FU_LISTEN_FD} && IO::Socket->new_from_fd($ENV{FU_LISTEN_FD}, 'r'),
|
||||
client_sock => $ENV{FU_CLIENT_FD} && IO::Socket->new_from_fd($ENV{FU_CLIENT_FD}, 'r+'),
|
||||
supervisor_sock => $ENV{FU_SUPERVISOR_FD} && IO::Socket->new_from_fd($ENV{FU_SUPERVISOR_FD}, 'w'),
|
||||
|
|
@ -405,9 +448,11 @@ sub _spawn {
|
|||
return if !@_ && !$need_supervisor;
|
||||
|
||||
if (!$c{listen_sock}) {
|
||||
my $addr = $c{fcgi} || $c{http};
|
||||
# TODO: check if stdin is a fastcgi sock
|
||||
$c{listen_proto} //= $c{fcgi} ? 'fcgi' : 'http';
|
||||
my $addr = $c{$c{listen_proto}};
|
||||
$c{listen_sock} = IO::Socket->new(
|
||||
Listen => 5,
|
||||
Listen => 10 * $c{proc},
|
||||
Type => IO::Socket::SOCK_STREAM(),
|
||||
$addr =~ m{^(unix:|/)(.+)$} ? do {
|
||||
my $path = ($1 eq '/' ? '/' : '').$2;
|
||||
|
|
@ -427,16 +472,11 @@ sub _spawn {
|
|||
_supervisor \%c;
|
||||
} else {
|
||||
$c{supervisor_sock}->syswrite('r'.pack 'V', $$) if $c{supervisor_sock};
|
||||
my $stop = 0;
|
||||
local $SIG{HUP} = 'IGNORE';
|
||||
local $SIG{TERM} = $SIG{INT} = sub { $stop = 1 };
|
||||
_do_req \%c if $c{client_sock};
|
||||
while (!$stop) {
|
||||
_do_req \%c if ($c{client_sock} = $c{listen_sock}->accept);
|
||||
}
|
||||
_run_loop \%c;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sub run(%conf) {
|
||||
confess "FU::run() called with configuration options, but FU has already been loaded with -spawn" if keys %conf;
|
||||
# Clean up any state we may have accumulated during initialization.
|
||||
|
|
@ -492,9 +532,9 @@ sub error($,$code,$msg=$code) { die bless [$code,$msg], 'FU::err' }
|
|||
sub reset {
|
||||
my $r = $FU::REQ;
|
||||
$r->{status} = 200;
|
||||
$r->{reshdr} = [
|
||||
$r->{reshdr} = {
|
||||
'content-type', 'text/html; charset=UTF-8',
|
||||
];
|
||||
};
|
||||
$r->{resbody} = '';
|
||||
}
|
||||
|
||||
|
|
@ -509,19 +549,16 @@ sub _validate_header($hdr, $val) {
|
|||
|
||||
sub add_header($, $hdr, $val) {
|
||||
_validate_header($hdr, $val);
|
||||
push $FU::REQ->{reshdr}->@*, lc $hdr, $val;
|
||||
$hdr = lc $hdr;
|
||||
my $h = $FU::REQ->{reshdr};
|
||||
if (!defined $h->{$hdr}) { $h->{$hdr} = $val }
|
||||
elsif (ref $h->{$hdr}) { push $h->{$hdr}->@*, $val }
|
||||
else { $h->{$hdr} = [ $h->{$hdr}, $val ] }
|
||||
}
|
||||
|
||||
sub set_header($, $hdr, $val=undef) {
|
||||
_validate_header($hdr, $val);
|
||||
$hdr = lc $hdr;
|
||||
# Not very efficient *shrug*
|
||||
my @r;
|
||||
for my ($ihdr, $ival) ($FU::REQ->{reshdr}->@*) {
|
||||
push @r, $ihdr, $ival if $ihdr ne $hdr;
|
||||
}
|
||||
push @r, $hdr, $val if defined $val;
|
||||
$FU::REQ->{reshdr} = \@r;
|
||||
$FU::REQ{reshdr}{ lc $hdr } = $val;
|
||||
}
|
||||
|
||||
sub _error_page($, $code, $title, $msg) {
|
||||
|
|
@ -550,7 +587,7 @@ sub _error_page($, $code, $title, $msg) {
|
|||
|
||||
sub _flush($, $sock) {
|
||||
my $r = $FU::REQ;
|
||||
$sock->printf("HTTP/1.0 %d Hello\r\n", $r->{status});
|
||||
# TODO: output compression would be nice
|
||||
|
||||
if ($r->{status} == 204) {
|
||||
fu->set_header('content-length', undef);
|
||||
|
|
@ -558,14 +595,28 @@ sub _flush($, $sock) {
|
|||
} else {
|
||||
fu->set_header('content-length', length $r->{resbody});
|
||||
}
|
||||
for my ($hdr, $val) ($r->{reshdr}->@*) {
|
||||
$r->{resbody} = '' if (fu->method//'') eq 'HEAD' || $r->{status} == 204;
|
||||
|
||||
if ($sock isa 'FU::fcgi') {
|
||||
$sock->print('Status: ');
|
||||
$sock->print($r->{status});
|
||||
$sock->print("\r\n");
|
||||
} else {
|
||||
$sock->printf("HTTP/1.0 %d Hello\r\n", $r->{status});
|
||||
}
|
||||
|
||||
for my ($hdr, $val) ($r->{reshdr}->%*) {
|
||||
utf8::encode($hdr);
|
||||
utf8::encode($val);
|
||||
$sock->printf("%s: %s\r\n", $hdr, $val);
|
||||
for (!defined $val ? () : ref $val ? @$val : ($val)) {
|
||||
utf8::encode($_);
|
||||
$sock->print($hdr);
|
||||
$sock->print(': ');
|
||||
$sock->print($_);
|
||||
$sock->print("\r\n");
|
||||
}
|
||||
}
|
||||
$sock->print("\r\n");
|
||||
|
||||
$sock->print($r->{resbody}) if (fu->method//'') ne 'HEAD' && $r->{status} != 204;
|
||||
$sock->print($r->{resbody});
|
||||
$sock->flush;
|
||||
}
|
||||
|
||||
|
|
@ -705,10 +756,12 @@ listen on a UNIX socket. E.g.
|
|||
./your-script.pl --http=unix:/path/to/socket
|
||||
|
||||
B<WARNING:> The built-in HTTP server is only intended for local development
|
||||
setups, it is NOT suitable for production deployments in its current form. It
|
||||
does not enforce a limit on request header size, does not support HTTPS and has
|
||||
no provisions for extracting the client IP address when behind a reverse proxy.
|
||||
Please use FastCGI instead for internet-facing deployments.
|
||||
setups, it is NOT suitable for production deployments. It has no timeouts, does
|
||||
not enforce limits on request size, does not support HTTPS and will never
|
||||
adequately support keep-alive. You could put it behind a reverse proxy, but it
|
||||
currently also lacks provisions for extracting the client IP address from the
|
||||
request headers, so that's not ideal either. Much better to use FastCGI in
|
||||
combination with a proper web server for internet-facing deployments.
|
||||
|
||||
=item FU_FCGI=addr
|
||||
|
||||
|
|
@ -717,6 +770,27 @@ Please use FastCGI instead for internet-facing deployments.
|
|||
Like the HTTP counterpart above, but listen on a FastCGI socket instead. If
|
||||
this option is set, it takes precedence over the HTTP option.
|
||||
|
||||
Nginx and Apache will, in their default configuration, use a separate
|
||||
connection per request. If you have a more esoteric setup, you should probably
|
||||
be aware of the following: this implementation does not support multiplexing or
|
||||
pipelining. It does support keepalive, but this come with a few caveats:
|
||||
|
||||
=over
|
||||
|
||||
=item * You should not attempt to keep more connections alive than the
|
||||
configured number of worker processes, otherwise new connection attempts will
|
||||
stall indefinitely.
|
||||
|
||||
=item * When using C<--monitor> mode, the file modification check is performed
|
||||
I<after> each request rather than before, so clients may get a response from
|
||||
stale code.
|
||||
|
||||
=item * When worker processes shut down, either through C<--max-reqs> or in
|
||||
response to a signal, there is the possibility that an incoming request on an
|
||||
existing connection gets interrupted.
|
||||
|
||||
=back
|
||||
|
||||
=item FU_PROC=n
|
||||
|
||||
=item --proc=n
|
||||
|
|
@ -737,8 +811,8 @@ significant cost in performance - better not enable this in production.
|
|||
|
||||
Worker processes can automatically restart after handling a number of requests.
|
||||
Set to 0 (the default) to disable this feature. This option can be useful when
|
||||
your worker processes keep accumulating memory over time. A little pruning here
|
||||
and there can never hurt.
|
||||
your worker processes keep accumulating memory over time. A little pruning now
|
||||
and then can never hurt.
|
||||
|
||||
=item FU_DEBUG=0/1
|
||||
|
||||
|
|
|
|||
42
FU.xs
42
FU.xs
|
|
@ -25,6 +25,7 @@
|
|||
#include "c/jsonfmt.c"
|
||||
#include "c/jsonparse.c"
|
||||
#include "c/fdpass.c"
|
||||
#include "c/fcgi.c"
|
||||
#include "c/libpq.h"
|
||||
#include "c/pgtypes.c"
|
||||
#include "c/pgconn.c"
|
||||
|
|
@ -55,11 +56,16 @@ PROTOTYPES: DISABLE
|
|||
|
||||
TYPEMAP: <<EOT
|
||||
TYPEMAP
|
||||
fufcgi * FUFCGI
|
||||
fupg_conn * FUPG_CONN
|
||||
fupg_txn * FUPG_TXN
|
||||
fupg_st * FUPG_ST
|
||||
|
||||
INPUT
|
||||
FUFCGI
|
||||
if (sv_derived_from($arg, \"FU::fcgi\")) $var = (fufcgi *)SvIVX(SvRV($arg));
|
||||
else fu_confess(\"invalid FastCGI object\");
|
||||
|
||||
FUPG_CONN
|
||||
if (sv_derived_from($arg, \"FU::Pg::conn\")) $var = (fupg_conn *)SvIVX(SvRV($arg));
|
||||
else fu_confess(\"invalid connection object\");
|
||||
|
|
@ -96,6 +102,42 @@ void fdpass_recv(int socket, UV len)
|
|||
XSRETURN(fufdpass_recv(aTHX_ ax, socket, len));
|
||||
|
||||
|
||||
|
||||
MODULE = FU PACKAGE = FU::fcgi
|
||||
|
||||
void new(int fd, int maxproc)
|
||||
CODE:
|
||||
fufcgi *ctx = safemalloc(sizeof(*ctx));
|
||||
ctx->fd = fd;
|
||||
ctx->maxproc = maxproc;
|
||||
ctx->reqid = ctx->keepconn = ctx->len = ctx->off = 0;
|
||||
ST(0) = fu_selfobj(ctx, "FU::fcgi");
|
||||
|
||||
void read_req(fufcgi *ctx, SV *headers, SV *params)
|
||||
CODE:
|
||||
ST(0) = sv_2mortal(newSViv(fufcgi_read_req(aTHX_ ctx, headers, params)));
|
||||
ctx->off = 8;
|
||||
|
||||
void keepalive(fufcgi *ctx)
|
||||
CODE:
|
||||
ST(0) = ctx->keepconn ? &PL_sv_yes : &PL_sv_no;
|
||||
|
||||
void print(fufcgi *ctx, SV *sv)
|
||||
CODE:
|
||||
STRLEN len;
|
||||
const char *buf = SvPVbyte(sv, len);
|
||||
fufcgi_print(ctx, buf, len);
|
||||
|
||||
void flush(fufcgi *ctx)
|
||||
CODE:
|
||||
fufcgi_flush(ctx);
|
||||
ctx->off = ctx->len = ctx->reqid = 0;
|
||||
|
||||
void DESTROY(fufcgi *ctx)
|
||||
CODE:
|
||||
safefree(ctx);
|
||||
|
||||
|
||||
MODULE = FU PACKAGE = FU::Pg
|
||||
|
||||
void _load_libpq()
|
||||
|
|
|
|||
427
c/fcgi.c
Normal file
427
c/fcgi.c
Normal file
|
|
@ -0,0 +1,427 @@
|
|||
#define FCGI_BEGIN_REQUEST 1
|
||||
#define FCGI_ABORT_REQUEST 2
|
||||
#define FCGI_END_REQUEST 3
|
||||
#define FCGI_PARAMS 4
|
||||
#define FCGI_STDIN 5
|
||||
#define FCGI_STDOUT 6
|
||||
#define FCGI_STDERR 7
|
||||
#define FCGI_DATA 8
|
||||
#define FCGI_GET_VALUES 9
|
||||
#define FCGI_GET_VALUES_RESULT 10
|
||||
#define FCGI_UNKNOWN_TYPE 11
|
||||
|
||||
#define FUFE_OK 0
|
||||
#define FUFE_EOF -1 /* protocol-level EOF */
|
||||
#define FUFE_IO -2
|
||||
#define FUFE_PROTO -3
|
||||
#define FUFE_PLEN -4
|
||||
#define FUFE_CLEN -5
|
||||
#define FUFE_ABORT -6 /* explicit abort or client-level EOF */
|
||||
|
||||
typedef struct {
|
||||
SV *self;
|
||||
int fd;
|
||||
int maxproc;
|
||||
int keepconn;
|
||||
|
||||
int reqid;
|
||||
HV *headers;
|
||||
HV *params;
|
||||
|
||||
/* Single buffer for reading & writing, we only do one thing at a time */
|
||||
char buf[8 + 65536 + 256]; /* fits a maximum-length fcgi record */
|
||||
int len; /* total number of bytes in the buffer */
|
||||
int off; /* number of bytes consumed */
|
||||
} fufcgi;
|
||||
|
||||
typedef struct {
|
||||
unsigned char type;
|
||||
unsigned short id;
|
||||
int len;
|
||||
char *data;
|
||||
} fufcgi_rec;
|
||||
|
||||
|
||||
/* Incremental param length & name parser */
|
||||
typedef enum {
|
||||
FUFC_INIT, FUFC_L1, FUFC_L2, FUFC_L3,
|
||||
FUFC_V0, FUFC_V1, FUFC_V2, FUFC_V3,
|
||||
FUFC_N0, FUFC_NX
|
||||
} fufcgi_paramstate;
|
||||
|
||||
typedef struct {
|
||||
int namelen;
|
||||
int vallen;
|
||||
int state;
|
||||
int namerd;
|
||||
char *name;
|
||||
char namebuf[128]; /* We don't support longer param names */
|
||||
} fufcgi_param;
|
||||
|
||||
/* Returns NULL on error or ptr to value (or 'end' if !done) */
|
||||
static char *fufcgi_param_parse(fufcgi_param *p, char *buf, char *end) {
|
||||
while (buf < end) {
|
||||
switch (p->state) {
|
||||
case FUFC_INIT:
|
||||
p->vallen = p->namerd = 0;
|
||||
if (*buf & 0x80) {
|
||||
p->namelen = (*buf & 0x1f) << 24;
|
||||
p->state = FUFC_L1;
|
||||
} else {
|
||||
p->namelen = *buf;
|
||||
p->state = FUFC_V0;
|
||||
}
|
||||
break;
|
||||
case FUFC_L1:
|
||||
p->namelen |= ((unsigned char)*buf) << 16;
|
||||
p->state = FUFC_L2;
|
||||
break;
|
||||
case FUFC_L2:
|
||||
p->namelen |= ((unsigned char)*buf) << 8;
|
||||
p->state = FUFC_L3;
|
||||
break;
|
||||
case FUFC_L3:
|
||||
p->namelen |= (unsigned char)*buf;
|
||||
p->state = FUFC_V0;
|
||||
if (p->namelen > (int)sizeof(p->namebuf)) return NULL;
|
||||
break;
|
||||
case FUFC_V0:
|
||||
if (*buf & 0x80) {
|
||||
p->vallen = (*buf & 0x1f) << 24;
|
||||
p->state = FUFC_V1;
|
||||
} else {
|
||||
p->vallen = *buf;
|
||||
p->state = p->namelen ? FUFC_N0 : FUFC_INIT;
|
||||
}
|
||||
break;
|
||||
case FUFC_V1:
|
||||
p->vallen |= ((unsigned char)*buf) << 16;
|
||||
if (p->vallen) return NULL; /* Let's just disallow param values > 64 KiB */
|
||||
p->state = FUFC_V2;
|
||||
break;
|
||||
case FUFC_V2:
|
||||
p->vallen |= ((unsigned char)*buf) << 8;
|
||||
p->state = FUFC_V3;
|
||||
break;
|
||||
case FUFC_V3:
|
||||
p->vallen |= (unsigned char)*buf;
|
||||
p->state = FUFC_N0;
|
||||
break;
|
||||
case FUFC_N0:
|
||||
if (p->namelen <= end - buf) {
|
||||
p->name = buf;
|
||||
p->state = FUFC_INIT;
|
||||
return buf + p->namelen;
|
||||
} else {
|
||||
p->name = p->namebuf;
|
||||
p->name[0] = *buf;
|
||||
p->namerd = 1;
|
||||
p->state = FUFC_NX;
|
||||
}
|
||||
break;
|
||||
case FUFC_NX:
|
||||
p->name[p->namerd++] = *buf;
|
||||
if (p->namerd == p->namelen) {
|
||||
p->state = FUFC_INIT;
|
||||
return buf + 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
buf++;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
static int fufcgi_fill(fufcgi *ctx, int len) {
|
||||
if ((int)sizeof(ctx->buf) - ctx->off < len) {
|
||||
memmove(ctx->buf, ctx->buf+ctx->off, ctx->len - ctx->off);
|
||||
ctx->len -= ctx->off;
|
||||
ctx->off = 0;
|
||||
}
|
||||
while (ctx->len - ctx->off < len) {
|
||||
ssize_t r = read(ctx->fd, ctx->buf+ctx->len, sizeof(ctx->buf) - ctx->len);
|
||||
if (r <= 0) return r == 0 ? FUFE_EOF : FUFE_IO;
|
||||
ctx->len += r;
|
||||
}
|
||||
return FUFE_OK;
|
||||
}
|
||||
|
||||
static int fufcgi_read_record(fufcgi *ctx, fufcgi_rec *rec) {
|
||||
int r;
|
||||
if ((r = fufcgi_fill(ctx, 8)) != FUFE_OK) return r;
|
||||
|
||||
if (ctx->buf[ctx->off] != 1) return FUFE_PROTO; /* version */
|
||||
rec->type = ctx->buf[ctx->off+1];
|
||||
rec->id = fu_frombeU(16, ctx->buf+ctx->off+2);
|
||||
rec->len = fu_frombeU(16, ctx->buf+ctx->off+4);
|
||||
int pad = ctx->buf[ctx->off+6];
|
||||
ctx->off += 8;
|
||||
|
||||
if ((r = fufcgi_fill(ctx, rec->len + pad)) != FUFE_OK) return r;
|
||||
rec->data = ctx->buf + ctx->off;
|
||||
ctx->off += rec->len + pad;
|
||||
return FUFE_OK;
|
||||
}
|
||||
|
||||
/* Unbuffered write of a single record, first 8 bytes of 'buf' are filled out
|
||||
* by this function, record contents must come after. */
|
||||
static int fufcgi_write_record(fufcgi *ctx, fufcgi_rec *hdr, char *buf) {
|
||||
buf[0] = 1;
|
||||
buf[1] = hdr->type;
|
||||
fu_tobeU(16, buf+2, hdr->id);
|
||||
fu_tobeU(16, buf+4, hdr->len);
|
||||
buf[6] = 0;
|
||||
buf[7] = 0;
|
||||
int len = hdr->len + 8;
|
||||
while (len > 0) {
|
||||
int r = write(ctx->fd, buf, len);
|
||||
if (r <= 0) return r == 0 ? FUFE_EOF : FUFE_IO;
|
||||
buf += r;
|
||||
len -= r;
|
||||
}
|
||||
return FUFE_OK;
|
||||
}
|
||||
|
||||
static int fufcgi_handle_values(fufcgi *ctx, fufcgi_rec *rec, char *buf) {
|
||||
int reslen = 8;
|
||||
char *param = rec->data;
|
||||
char *end = rec->data + rec->len;
|
||||
fufcgi_param p;
|
||||
p.state = FUFC_INIT;
|
||||
|
||||
while (param < end) {
|
||||
if ((param = fufcgi_param_parse(&p, param, end)) == NULL) return FUFE_PLEN;
|
||||
if (p.state != FUFC_INIT) return FUFE_PROTO;
|
||||
if (p.vallen > end - param) return FUFE_PROTO;
|
||||
if (reslen >= 100) return FUFE_PROTO; /* implies requested params were duplicated */
|
||||
|
||||
if (p.namelen == 14 && memcmp(p.name, "FCGI_MAX_CONNS", 14) == 0) {
|
||||
memcpy(buf+reslen, "\x0e\0FCGI_MAX_CONNS", 16);
|
||||
int l = sprintf(buf+reslen+16, "%d", ctx->maxproc);
|
||||
buf[reslen+1] = l;
|
||||
reslen += 16 + l;
|
||||
|
||||
} else if (p.namelen == 13 && memcmp(p.name, "FCGI_MAX_REQS", 13) == 0) {
|
||||
memcpy(buf+reslen, "\x0d\0FCGI_MAX_REQS", 15);
|
||||
int l = sprintf(buf+reslen+15, "%d", ctx->maxproc);
|
||||
buf[reslen+1] = l;
|
||||
reslen += 15 + l;
|
||||
|
||||
} else if (p.namelen == 15 && memcmp(p.name, "FCGI_MPXS_CONNS", 15) == 0) {
|
||||
memcpy(buf+reslen, "\x0f\1FCGI_MPXS_CONNS0", 18);
|
||||
reslen += 18;
|
||||
}
|
||||
|
||||
param += p.vallen;
|
||||
}
|
||||
rec->type = FCGI_GET_VALUES_RESULT;
|
||||
rec->len = reslen - 8;
|
||||
return fufcgi_write_record(ctx, rec, buf);
|
||||
}
|
||||
|
||||
/* Read a PARAMS/STDIN/ABORT record corresponding to the current id, starts
|
||||
* reading a new request if id=0. */
|
||||
static int fufcgi_read_req_record(fufcgi *ctx, fufcgi_rec *rec) {
|
||||
int r;
|
||||
char tmp[128]; /* Large enough for a FCGI_GET_VALUES_RESULT */
|
||||
while (1) {
|
||||
if ((r = fufcgi_read_record(ctx, rec)) != FUFE_OK) return r;
|
||||
|
||||
switch (rec->type) {
|
||||
case FCGI_PARAMS:
|
||||
case FCGI_STDIN:
|
||||
case FCGI_ABORT_REQUEST:
|
||||
if (rec->id != ctx->reqid) return FUFE_PROTO;
|
||||
return FUFE_OK;
|
||||
case FCGI_BEGIN_REQUEST:
|
||||
if (!rec->id || rec->id == ctx->reqid) return FUFE_PROTO;
|
||||
if (rec->len != 8) return FUFE_PROTO;
|
||||
ctx->keepconn = rec->data[2] & 1;
|
||||
if (rec->data[0] != 0 || rec->data[1] != 1) { /* FCGI_RESPONDER */
|
||||
memcpy(tmp+8, "\0\0\0\0\3\0\0\0", 8); /* FCGI_UNKNOWN_ROLE */
|
||||
rec->type = FCGI_END_REQUEST;
|
||||
rec->len = 8;
|
||||
if ((r = fufcgi_write_record(ctx, rec, tmp)) != FUFE_OK) return r;
|
||||
if (!ctx->keepconn) return FUFE_EOF;
|
||||
} else if (ctx->reqid) {
|
||||
memcpy(tmp+8, "\0\0\0\0\1\0\0\0", 8); /* FCGI_CANT_MPX_CONN */
|
||||
rec->type = FCGI_END_REQUEST;
|
||||
rec->len = 8;
|
||||
if ((r = fufcgi_write_record(ctx, rec, tmp)) != FUFE_OK) return r;
|
||||
if (!ctx->keepconn) return FUFE_EOF;
|
||||
} else {
|
||||
ctx->reqid = rec->id;
|
||||
}
|
||||
break;
|
||||
case FCGI_GET_VALUES:
|
||||
if (rec->id) return FUFE_PROTO;
|
||||
if ((r = fufcgi_handle_values(ctx, rec, tmp)) != FUFE_OK) return r;
|
||||
break;
|
||||
default:
|
||||
memset(tmp+8, 0, 8);
|
||||
tmp[8] = rec->type;
|
||||
rec->type = FCGI_UNKNOWN_TYPE;
|
||||
rec->len = 8;
|
||||
rec->id = 0;
|
||||
if ((r = fufcgi_write_record(ctx, rec, tmp)) != FUFE_OK) return r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int fufcgi_read_params(pTHX_ fufcgi *ctx, fufcgi_rec *rec) {
|
||||
int r;
|
||||
fufcgi_param p;
|
||||
p.state = FUFC_INIT;
|
||||
|
||||
SV *valsv = NULL;
|
||||
char *val = NULL;
|
||||
int valleft = 0;
|
||||
|
||||
while (1) {
|
||||
if ((r = fufcgi_read_req_record(ctx, rec)) != FUFE_OK) return r;
|
||||
if (rec->type == FCGI_ABORT_REQUEST) return FUFE_OK;
|
||||
if (rec->type != FCGI_PARAMS) return FUFE_PROTO;
|
||||
if (rec->len == 0) return p.state != FUFC_INIT || valleft ? FUFE_PROTO : FUFE_OK;
|
||||
|
||||
char *buf = rec->data;
|
||||
char *end = rec->data + rec->len;
|
||||
while (buf < end) {
|
||||
if (valleft) {
|
||||
r = valleft > end - buf ? end - buf : valleft;
|
||||
if (val) {
|
||||
memcpy(val, buf, r);
|
||||
val += r;
|
||||
}
|
||||
valleft -= r;
|
||||
buf += r;
|
||||
if (val && !valleft) {
|
||||
*val = 0;
|
||||
SvCUR_set(valsv, p.vallen);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ((buf = fufcgi_param_parse(&p, buf, end)) == NULL) return FUFE_PLEN;
|
||||
if (p.state != FUFC_INIT) break;
|
||||
|
||||
valsv = NULL;
|
||||
val = NULL;
|
||||
valleft = p.vallen;
|
||||
|
||||
/* https://www.rfc-editor.org/rfc/rfc3875 */
|
||||
|
||||
/* Request header */
|
||||
if (p.namelen > 5 && memcmp(p.name, "HTTP_", 5) == 0) {
|
||||
p.namelen -= 5;
|
||||
p.name += 5;
|
||||
for (r=0; r<p.namelen; r++)
|
||||
p.name[r] = p.name[r] == '_' ? '-' : p.name[r] >= 'A' && p.name[r] <= 'Z' ? p.name[r] | 0x20 : p.name[r];
|
||||
valsv = newSV(p.vallen+1);
|
||||
hv_store(ctx->headers, p.name, p.namelen, valsv, 0);
|
||||
|
||||
} else if (p.namelen == 14 && memcmp(p.name, "CONTENT_LENGTH", 14) == 0) {
|
||||
valsv = newSV(p.vallen+1);
|
||||
hv_stores(ctx->headers, "content-length", valsv);
|
||||
|
||||
} else if (p.namelen == 12 && memcmp(p.name, "CONTENT_TYPE", 12) == 0) {
|
||||
valsv = newSV(p.vallen+1);
|
||||
hv_stores(ctx->headers, "content-type", valsv);
|
||||
|
||||
} else if (p.namelen == 11 && memcmp(p.name, "REMOTE_ADDR", 11) == 0) {
|
||||
valsv = newSV(p.vallen+1);
|
||||
hv_stores(ctx->params, "ip", valsv);
|
||||
|
||||
} else if (p.namelen == 12 && memcmp(p.name, "QUERY_STRING", 12) == 0) {
|
||||
valsv = newSV(p.vallen+1);
|
||||
hv_stores(ctx->params, "qs", valsv);
|
||||
|
||||
} else if (p.namelen == 14 && memcmp(p.name, "REQUEST_METHOD", 14) == 0) {
|
||||
valsv = newSV(p.vallen+1);
|
||||
hv_stores(ctx->params, "method", valsv);
|
||||
|
||||
/* Not in rfc3875; there's no standardized parameter for the URI,
|
||||
* but every FastCGI-capable web server includes this one */
|
||||
} else if (p.namelen == 11 && memcmp(p.name, "REQUEST_URI", 11) == 0) {
|
||||
valsv = newSV(p.vallen+1);
|
||||
hv_stores(ctx->params, "path", valsv);
|
||||
|
||||
} else { /* ignore */ }
|
||||
|
||||
if (valsv) {
|
||||
SvPOK_only(valsv);
|
||||
val = SvPVX(valsv);
|
||||
*val = 0; /* in case vallen = 0 */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int fufcgi_read_req(pTHX_ fufcgi *ctx, SV *headers, SV *params) {
|
||||
if (ctx->reqid) fu_confess("Invalid attempt to read FastCGI request before finishing the previous one");
|
||||
fufcgi_rec rec;
|
||||
int r;
|
||||
|
||||
ctx->off = ctx->len = 0;
|
||||
ctx->headers = (HV *)SvRV(headers);
|
||||
ctx->params = (HV *)SvRV(params);
|
||||
if ((r = fufcgi_read_params(aTHX_ ctx, &rec)) != FUFE_OK) return r;
|
||||
|
||||
int stdinlen = 0;
|
||||
SV **contentlength = hv_fetchs(ctx->headers, "content-length", 0);
|
||||
if (contentlength && *contentlength) {
|
||||
UV uv = 0;
|
||||
char *v = SvPV_nolen(*contentlength);
|
||||
if (*v && !grok_atoUV(v, &uv, NULL)) return FUFE_CLEN;
|
||||
if (uv >= INT_MAX) return FUFE_CLEN;
|
||||
stdinlen = uv;
|
||||
}
|
||||
|
||||
SV *sv = newSV(stdinlen+1);
|
||||
hv_stores(ctx->params, "body", sv);
|
||||
SvPOK_only(sv);
|
||||
char *stdinbuf = SvPVX(sv);
|
||||
int stdinleft = stdinlen;
|
||||
|
||||
while (1) {
|
||||
if (rec.type == FCGI_ABORT_REQUEST) return FUFE_ABORT;
|
||||
else if (rec.type == FCGI_PARAMS) {
|
||||
if (rec.len != 0) return FUFE_PROTO;
|
||||
} else if (rec.type == FCGI_STDIN) {
|
||||
if (rec.len == 0) {
|
||||
*stdinbuf = 0;
|
||||
SvCUR_set(sv, stdinlen - stdinleft);
|
||||
return stdinleft == 0 ? FUFE_OK : FUFE_ABORT;
|
||||
}
|
||||
if (rec.len > stdinleft) return FUFE_PROTO;
|
||||
memcpy(stdinbuf, rec.data, rec.len);
|
||||
stdinbuf += rec.len;
|
||||
stdinleft -= rec.len;
|
||||
} else {
|
||||
return FUFE_PROTO;
|
||||
}
|
||||
if ((r = fufcgi_read_req_record(ctx, &rec)) != FUFE_OK) return r;
|
||||
}
|
||||
}
|
||||
|
||||
static void fufcgi_flush(fufcgi *ctx) {
|
||||
fufcgi_rec hdr;
|
||||
if (ctx->off > 8) {
|
||||
hdr.len = ctx->off - 8;
|
||||
hdr.type = FCGI_STDOUT;
|
||||
hdr.id = ctx->reqid;
|
||||
fufcgi_write_record(ctx, &hdr, ctx->buf);
|
||||
ctx->off = 8;
|
||||
}
|
||||
}
|
||||
|
||||
static void fufcgi_print(fufcgi *ctx, const char *buf, int len) {
|
||||
int r;
|
||||
while (len > 0) {
|
||||
r = len > 65535 - ctx->off ? 65535 - ctx->off : len;
|
||||
memcpy(ctx->buf+ctx->off, buf, r);
|
||||
ctx->off += r;
|
||||
len -= r;
|
||||
buf += r;
|
||||
if (ctx->off >= 65535) fufcgi_flush(ctx);
|
||||
}
|
||||
}
|
||||
162
t/fcgi.t
Normal file
162
t/fcgi.t
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
use v5.36;
|
||||
use Test::More;
|
||||
use IO::Socket qw/AF_UNIX SOCK_STREAM PF_UNSPEC/;
|
||||
use FU::XS;
|
||||
|
||||
my($f, $local, $remote);
|
||||
|
||||
sub start {
|
||||
($local, $remote) = IO::Socket->socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC);
|
||||
$f = FU::fcgi::new(fileno $local, 123);
|
||||
}
|
||||
|
||||
sub record($id, $type, $data) {
|
||||
my $pad = rand > 0.5 ? int rand(50) : 0;
|
||||
my $msg = pack('CCnnCC', 1, $type, $id, length($data), $pad, 0) . $data . ("\0"x$pad);
|
||||
is $remote->syswrite($msg, length($msg)), length($msg);
|
||||
}
|
||||
|
||||
sub begin($id=1, $role=1, $keep=0) {
|
||||
record $id, 1, "\0".chr($role).($keep?"\1":"\0")."\0\0\0\0\0"
|
||||
}
|
||||
|
||||
sub iserr($code) {
|
||||
is $f->read_req({}, {}), $code;
|
||||
}
|
||||
|
||||
sub isrec($hdr, $par, $code=0) {
|
||||
is $f->read_req(my $rhdr = {}, my $rpar = {}), $code;
|
||||
is_deeply $rhdr, $hdr;
|
||||
is_deeply $rpar, $par;
|
||||
}
|
||||
|
||||
sub isrecv($data) {
|
||||
is $remote->sysread(my $buf, length $data), length $data;
|
||||
is $buf, $data;
|
||||
}
|
||||
|
||||
|
||||
start;
|
||||
$remote->close;
|
||||
iserr -1;
|
||||
|
||||
start;
|
||||
is $remote->syswrite("\0\0\0\0\0\0\0\0", 8), 8;
|
||||
iserr -3;
|
||||
|
||||
start;
|
||||
begin 1, 2;
|
||||
record 1, 4, "";
|
||||
|
||||
start;
|
||||
begin 3, 2, 1;
|
||||
begin 1, 1, 1;
|
||||
begin 2, 1, 1;
|
||||
record 1, 4, "";
|
||||
record 0, 10, "";
|
||||
record 1, 5, "";
|
||||
isrec {}, {body => ''};
|
||||
isrecv "\1\3\0\3\0\x08\0\0"."\0\0\0\0\3\0\0\0"; # end request 3, unknown role
|
||||
isrecv "\1\3\0\2\0\x08\0\0"."\0\0\0\0\1\0\0\0"; # end request 2, can't multiplex
|
||||
isrecv "\1\x0b\0\0\0\x08\0\0"."\x0a\0\0\0\0\0\0\0"; # unknown type, 10
|
||||
|
||||
start;
|
||||
begin;
|
||||
record 1, 4, "\x0e\2C";
|
||||
record 1, 4, "ONTENT_";
|
||||
record 1, 4, "LENGTH";
|
||||
record 1, 4, "1";
|
||||
record 1, 4, "2\x80\x00";
|
||||
record 1, 4, "\x00\x09";
|
||||
record 1, 4, "\x80";
|
||||
record 1, 4, "\x00\x00";
|
||||
record 1, 4, "\x04HTTP_H_S";
|
||||
record 1, 4, "T";
|
||||
record 1, 4, "tes";
|
||||
record 1, 4, "t";
|
||||
record 1, 4, "";
|
||||
record 1, 5, "012";
|
||||
record 1, 5, "34567890";
|
||||
record 1, 5, "1";
|
||||
record 1, 5, "";
|
||||
isrec {'content-length',12, 'h-st' => 'test'}, {body => '012345678901'};
|
||||
|
||||
start;
|
||||
begin 5, 1, 1;
|
||||
record 5, 4, "\x0e\x01CONTENT_LENGTH5\x0c\x05CONTENT_TYPEtext/";
|
||||
record 5, 4, "\x0b\x04REMOTE_ADDRaddr\x0c\x05QUERY_STRINGquery";
|
||||
record 5, 4, "\x0e\x04REQUEST_METHODPOST\x0b\x06REQUEST_URI/p\x81t\x55/";
|
||||
record 5, 4, "";
|
||||
record 5, 5, "hello";
|
||||
record 5, 5, "";
|
||||
isrec
|
||||
{ 'content-length', 5, 'content-type', 'text/' },
|
||||
{ ip => 'addr', body => 'hello', qs => 'query', path => "/p\x81t\x55/", method => 'POST' };
|
||||
$f->print("Status: 200\r\n");
|
||||
$f->print("Something else");
|
||||
$f->flush;
|
||||
isrecv "\1\6\0\5\0\x1b\0\0"."Status: 200\r\nSomething else";
|
||||
# Same connection:
|
||||
begin;
|
||||
record 1, 4, "\x00\x00\x06\x00HTTP_x\x00\x00";
|
||||
record 1, 4, "";
|
||||
record 1, 5, "";
|
||||
isrec { x => '' }, { body => ''};
|
||||
|
||||
start;
|
||||
begin;
|
||||
record 1, 4, "\x40\x01this is too short";
|
||||
record 1, 4, "";
|
||||
iserr -3;
|
||||
|
||||
start;
|
||||
begin;
|
||||
record 1, 4, "\x01\x40this is too short";
|
||||
record 1, 4, "";
|
||||
iserr -3;
|
||||
|
||||
start;
|
||||
begin;
|
||||
record 1, 5, "";
|
||||
iserr -3;
|
||||
|
||||
start;
|
||||
begin;
|
||||
record 1, 4, "\x0e\x03CONTENT_LENGTH123";
|
||||
record 1, 4, "";
|
||||
record 1, 5, "too short";
|
||||
record 1, 5, "";
|
||||
isrec {'content-length',123}, {body=>'too short'}, -6;
|
||||
|
||||
start;
|
||||
begin;
|
||||
record 1, 4, "\x0e\x00CONTENT_LENGTH";
|
||||
record 1, 4, "";
|
||||
record 1, 5, "";
|
||||
isrec {'content-length',''}, {body=>''};
|
||||
|
||||
start;
|
||||
begin;
|
||||
record 1, 4, "\x80\x00\x01\x00\x00".('A'x256);
|
||||
iserr -4;
|
||||
|
||||
start;
|
||||
begin;
|
||||
record 1, 4, "\x01\x80\x01\x00\x00".('A'x256);
|
||||
iserr -4;
|
||||
|
||||
start;
|
||||
begin;
|
||||
record 1, 4, "";
|
||||
record 0, 9, "\x0d\0FCGI_MAX_REQS\x0e\0FCGI_MAX_CONNS\2\3hi987\x0f\0FCGI_MPXS_CONNS";
|
||||
record 1, 5, "";
|
||||
isrec {}, {body => ''};
|
||||
isrecv "\1\x0a\0\0\0\x37\0\0"."\x0d\3FCGI_MAX_REQS123\x0e\3FCGI_MAX_CONNS123\x0f\1FCGI_MPXS_CONNS0";
|
||||
|
||||
start;
|
||||
begin;
|
||||
record 1, 4, "\x0c\x05CONTENT_TYPEsomet";
|
||||
record 1, 2, "";
|
||||
isrec {'content-type','somet'}, {body => ''}, -6;
|
||||
|
||||
done_testing;
|
||||
Loading…
Add table
Add a link
Reference in a new issue