blob: 51d438911db40e319bdc7e4ab01e4b04fb816dd3 [file] [log] [blame]
#!/usr/bin/perl
# (C) Maxim Dounin
# (C) Sergey Kandaurov
# (C) Nginx, Inc.
# Tests for unbuffered request body with fastcgi backend,
# chunked transfer-encoding.
###############################################################################
use warnings;
use strict;
use Test::More;
use Socket qw/ CRLF /;
BEGIN { use FindBin; chdir($FindBin::Bin); }
use lib 'lib';
use Test::Nginx;
###############################################################################
select STDERR; $| = 1;
select STDOUT; $| = 1;
eval { require FCGI; };
plan(skip_all => 'FCGI not installed') if $@;
plan(skip_all => 'win32') if $^O eq 'MSWin32';
my $t = Test::Nginx->new()->has(qw/http fastcgi rewrite/)->plan(19);
$t->write_file_expand('nginx.conf', <<'EOF');
%%TEST_GLOBALS%%
daemon off;
events {
}
http {
%%TEST_GLOBALS_HTTP%%
server {
listen 127.0.0.1:8080;
server_name localhost;
client_header_buffer_size 1k;
fastcgi_request_buffering off;
fastcgi_param REQUEST_URI $request_uri;
location / {
client_body_buffer_size 2k;
fastcgi_pass 127.0.0.1:8081;
}
location /single {
client_body_in_single_buffer on;
fastcgi_pass 127.0.0.1:8081;
}
location /preread {
fastcgi_pass 127.0.0.1:8082;
}
location /error_page {
fastcgi_pass 127.0.0.1:8081;
error_page 404 /404;
fastcgi_intercept_errors on;
}
location /404 {
return 200 "$request_body\n";
}
}
}
EOF
$t->run_daemon(\&fastcgi_daemon);
$t->run()->waitforsocket('127.0.0.1:' . port(8081));
###############################################################################
like(http_get('/'), qr/X-Body: \x0d\x0a?/ms, 'no body');
like(http_get_body('/', '0123456789'),
qr/X-Body: 0123456789\x0d?$/ms, 'body');
like(http_get_body('/', '0123456789' x 128),
qr/X-Body: (0123456789){128}\x0d?$/ms, 'body in two buffers');
like(http_get_body('/single', '0123456789' x 128),
qr/X-Body: (0123456789){128}\x0d?$/ms, 'body in single buffer');
like(http_get_body('/error_page', '0123456789'),
qr/^0123456789$/m, 'body in error page');
# pipelined requests
like(http_get_body('/', '0123456789', '0123456789' x 128, '0123456789' x 512,
'foobar'), qr/X-Body: foobar\x0d?$/ms, 'body pipelined');
like(http_get_body('/', '0123456789' x 128, '0123456789' x 512, '0123456789',
'foobar'), qr/X-Body: foobar\x0d?$/ms, 'body pipelined 2');
# interactive tests
my $s = get_body('/preread', port(8082));
ok($s, 'no preread');
SKIP: {
skip 'no preread failed', 3 unless $s;
is($s->{upload}('01234'), '01234', 'no preread - body part');
is($s->{upload}('56789', last => 1), '56789', 'no preread - body part 2');
like($s->{http_end}(), qr/200 OK/, 'no preread - response');
}
$s = get_body('/preread', port(8082), '01234');
ok($s, 'preread');
SKIP: {
skip 'preread failed', 3 unless $s;
is($s->{preread}, '01234', 'preread - preread');
is($s->{upload}('56789', last => 1), '56789', 'preread - body');
like($s->{http_end}(), qr/200 OK/, 'preread - response');
}
$s = get_body('/preread', port(8082), '01234', many => 1);
ok($s, 'chunks');
SKIP: {
skip 'chunks failed', 3 unless $s;
is($s->{preread}, '01234many', 'chunks - preread');
is($s->{upload}('56789', many => 1, last => 1), '56789many', 'chunks - body');
like($s->{http_end}(), qr/200 OK/, 'chunks - response');
}
###############################################################################
sub http_get_body {
my $uri = shift;
my $last = pop;
return http( join '', (map {
my $body = $_;
"GET $uri HTTP/1.1" . CRLF
. "Host: localhost" . CRLF
. "Transfer-Encoding: chunked" . CRLF . CRLF
. sprintf("%x", length $body) . CRLF
. $body . CRLF
. "0" . CRLF . CRLF
} @_),
"GET $uri HTTP/1.1" . CRLF
. "Host: localhost" . CRLF
. "Connection: close" . CRLF
. "Transfer-Encoding: chunked" . CRLF . CRLF
. sprintf("%x", length $last) . CRLF
. $last . CRLF
. "0" . CRLF . CRLF
);
}
# Simple FastCGI responder implementation.
# http://www.fastcgi.com/devkit/doc/fcgi-spec.html
sub fastcgi_read_record($) {
my ($buf) = @_;
my $h;
return undef unless length $$buf;
@{$h}{qw/ version type id clen plen /} = unpack("CCnnC", $$buf);
$h->{content} = substr $$buf, 8, $h->{clen};
$h->{padding} = substr $$buf, 8 + $h->{clen}, $h->{plen};
$$buf = substr $$buf, 8 + $h->{clen} + $h->{plen};
return $h;
}
sub fastcgi_respond($$$$) {
my ($socket, $version, $id, $body) = @_;
# stdout
$socket->write(pack("CCnnCx", $version, 6, $id, length($body), 8));
$socket->write($body);
select(undef, undef, undef, 0.1);
$socket->write(pack("xxxxxxxx"));
select(undef, undef, undef, 0.1);
# write some text to stdout and stderr split over multiple network
# packets to test if we correctly set pipe length in various places
my $tt = "test text, just for test";
$socket->write(pack("CCnnCx", $version, 6, $id,
length($tt . $tt), 0) . $tt);
select(undef, undef, undef, 0.1);
$socket->write($tt . pack("CC", $version, 7));
select(undef, undef, undef, 0.1);
$socket->write(pack("nnCx", $id, length($tt), 0));
select(undef, undef, undef, 0.1);
$socket->write($tt);
select(undef, undef, undef, 0.1);
# close stdout
$socket->write(pack("CCnnCx", $version, 6, $id, 0, 0));
select(undef, undef, undef, 0.1);
# end request
$socket->write(pack("CCnnCx", $version, 3, $id, 8, 0));
select(undef, undef, undef, 0.1);
$socket->write(pack("NCxxx", 0, 0));
}
sub get_body {
my ($url, $port, $body, %extra) = @_;
my ($server, $client, $s);
my ($last, $many) = (0, 0);
my ($version, $id);
$last = $extra{last} if defined $extra{last};
$many = $extra{many} if defined $extra{many};
$server = IO::Socket::INET->new(
Proto => 'tcp',
LocalHost => '127.0.0.1',
LocalPort => $port,
Listen => 5,
Reuse => 1
)
or die "Can't create listening socket: $!\n";
my $r = <<EOF;
GET $url HTTP/1.1
Host: localhost
Connection: close
Transfer-Encoding: chunked
EOF
if (defined $body) {
$r .= sprintf("%x", length $body) . CRLF;
$r .= $body . CRLF;
}
if (defined $body && $many) {
$r .= sprintf("%x", length 'many') . CRLF;
$r .= 'many' . CRLF;
}
if ($last) {
$r .= "0" . CRLF . CRLF;
}
$s = http($r, start => 1);
eval {
local $SIG{ALRM} = sub { die "timeout\n" };
local $SIG{PIPE} = sub { die "sigpipe\n" };
alarm(5);
$client = $server->accept();
log2c("(new connection $client)");
alarm(0);
};
alarm(0);
if ($@) {
log_in("died: $@");
return undef;
}
$client->sysread(my $buf, 1024);
log2i($buf);
$body = '';
while (my $h = fastcgi_read_record(\$buf)) {
$version = $h->{version};
$id = $h->{id};
# skip everything unless stdin
next if $h->{type} != 5;
$body .= $h->{content};
}
my $f = { preread => $body };
$f->{upload} = sub {
my ($body, %extra) = @_;
my ($last, $many) = (0, 0);
$last = $extra{last} if defined $extra{last};
$many = $extra{many} if defined $extra{many};
my $buf = sprintf("%x", length $body) . CRLF;
$buf .= $body . CRLF;
if ($many) {
$buf .= sprintf("%x", length 'many') . CRLF;
$buf .= 'many' . CRLF;
}
if ($last) {
$buf .= "0" . CRLF . CRLF;
}
eval {
local $SIG{ALRM} = sub { die "timeout\n" };
local $SIG{PIPE} = sub { die "sigpipe\n" };
alarm(5);
log_out($buf);
$s->write($buf);
$client->sysread($buf, 1024);
log2i($buf);
$body = '';
while (my $h = fastcgi_read_record(\$buf)) {
# skip everything unless stdin
next if $h->{type} != 5;
$body .= $h->{content};
}
alarm(0);
};
alarm(0);
if ($@) {
log_in("died: $@");
return undef;
}
return $body;
};
$f->{http_end} = sub {
my $buf = '';
eval {
local $SIG{ALRM} = sub { die "timeout\n" };
local $SIG{PIPE} = sub { die "sigpipe\n" };
alarm(5);
fastcgi_respond($client, $version, $id, <<EOF);
Status: 200 OK
Connection: close
X-Port: $port
OK
EOF
$client->close;
$s->sysread($buf, 1024);
log_in($buf);
$s->close();
alarm(0);
};
alarm(0);
if ($@) {
log_in("died: $@");
return undef;
}
return $buf;
};
return $f;
}
sub log2i { Test::Nginx::log_core('|| <<', @_); }
sub log2o { Test::Nginx::log_core('|| >>', @_); }
sub log2c { Test::Nginx::log_core('||', @_); }
###############################################################################
sub fastcgi_daemon {
my $socket = FCGI::OpenSocket('127.0.0.1:' . port(8081), 5);
my $request = FCGI::Request(\*STDIN, \*STDOUT, \*STDERR, \%ENV,
$socket);
my $count;
my ($body, $buf);
while( $request->Accept() >= 0 ) {
$count++;
$body = '';
do {
read(STDIN, $buf, 1024);
$body .= $buf;
} while (length $buf);
if ($ENV{REQUEST_URI} eq '/error_page') {
print "Status: 404 Not Found" . CRLF . CRLF;
next;
}
print <<EOF;
Location: http://localhost/redirect
Content-Type: text/html
X-Body: $body
SEE-THIS
$count
EOF
}
FCGI::CloseSocket($socket);
}
###############################################################################