view fastcgi_request_buffering_chunked.t @ 1248:70192b1baf01

Tests: added exception test to stream_js.t using 'require'. The stream js tests introduced in edf5a3c9e36a fail on njs 0.1.14. It doesn't currently provide an easy way to check its version, whilst we are obligated to gracefully handle such cases somehow. With such an addition of 'require', now the tests are skipped instead on the previous versions.
author Sergey Kandaurov <pluknet@nginx.com>
date Tue, 21 Nov 2017 13:16:39 +0300
parents 1e79a9613813
children
line wrap: on
line source

#!/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);
}

###############################################################################