view fastcgi_request_buffering_chunked.t @ 1938:e1059682aeef

Tests: fixed ClientHello with resending Initial QUIC packets. Previously it was rebuilt each time using distinct ClientHello.random resulting in different CRYPTO payload. As such, it led to TLS digest hash and derived secrets mismatch when resending Initial packet. Now ClientHello is built once and reused when resending Initial packets. Additionally, this required to preserve a generated secret value used in shared secret calculation as part of TLS key schedule. Previously it was regenerated when receiving a Retry packet, but this won't work with reused ClientHello as the resulting shared secrets won't match.
author Sergey Kandaurov <pluknet@nginx.com>
date Wed, 30 Aug 2023 02:22:58 +0400
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);
}

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