From 6647488665cb1d0e210459bf628c0485d58e0412 Mon Sep 17 00:00:00 2001 From: Maxim Dounin Date: Sat, 27 Apr 2024 18:55:21 +0300 Subject: [PATCH 1/3] Tests: adjusted http_headers_multi.t for $content_length changes. The $content_length variable is going to be not available after discarding the request body. As such, the relevant location is now proxied, so the request body is not discarded. Co-Authored-By: Elijah Zupancic Cherry-Picked-From: https://freenginx.org/hg/nginx-tests/rev/e44ee916b9598eaf805aaa9aa7afaa35db2140a5 --- http_headers_multi.t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http_headers_multi.t b/http_headers_multi.t index 441f2497..ddc85d3a 100644 --- a/http_headers_multi.t +++ b/http_headers_multi.t @@ -97,7 +97,7 @@ http { add_header X-Host $host; add_header X-Remote-User $remote_user; - return 204; + proxy_pass http://127.0.0.1:8080/backend; } location /d { From cb3152b35f565c77a6d9be30cb6ac3ec4e5e4e2f Mon Sep 17 00:00:00 2001 From: Maxim Dounin Date: Sat, 27 Apr 2024 18:55:50 +0300 Subject: [PATCH 2/3] Tests: tests for usage of discarded body. The client_max_body_size limit should be ignored when the request body is already discarded. In HTTP/1.x, this is done by checking the r->discard_body flag when the body is being discarded, and because r->headers_in.content_length_n is 0 when it's already discarded. This, however, does not happen with HTTP/2 and HTTP/3, and therefore "error_page 413" does not work without relaxing the limit. Further, with proxy_pass, r->headers_in.content_length_n is used to determine length of the request body, and therefore is not correct if discarding of the request body isn't yet complete. While discarding the request body, r->headers_in.content_length_n contains the rest of the body to discard (or, in case of chunked request body, the rest of the current chunk to discard). Similarly, the $content_length variable uses r->headers_in.content_length if available, and also incorrect. The $content_length variable is used when proxying with fastcgi_pass, grpc_pass, and uwsgi_pass (scgi_pass uses the value calculated based on the actual request body buffers, and therefore works correctly). Co-Authored-By: Elijah Zupancic Cherry-Picked-From: https://freenginx.org/hg/nginx-tests/rev/fe6f22da53ec760f7ab138d1d32b7a03ea7bdea3 --- body_discard.t | 448 ++++++++++++++++++++++++++++++++++++++ h2_request_body_discard.t | 406 ++++++++++++++++++++++++++++++++++ h3_request_body_discard.t | 425 ++++++++++++++++++++++++++++++++++++ 3 files changed, 1279 insertions(+) create mode 100644 body_discard.t create mode 100644 h2_request_body_discard.t create mode 100644 h3_request_body_discard.t diff --git a/body_discard.t b/body_discard.t new file mode 100644 index 00000000..a8ff653d --- /dev/null +++ b/body_discard.t @@ -0,0 +1,448 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for discarding request body. + +############################################################################### + +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; + +my $t = Test::Nginx->new() + ->has(qw/http proxy rewrite addition memcached/); + + +$t->plan(33)->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; + + lingering_timeout 1s; + add_header X-Body body:$content_length:$request_body:; + + client_max_body_size 1k; + + error_page 400 /proxy/error400; + + location / { + error_page 413 /error413; + proxy_pass http://127.0.0.1:8082; + } + + location /error413 { + return 200 "custom error 413"; + } + + location /add { + return 200 "main response"; + add_before_body /add/before; + addition_types *; + client_max_body_size 1m; + } + + location /add/before { + proxy_pass http://127.0.0.1:8081; + } + + location /memcached { + client_max_body_size 1m; + error_page 502 /memcached/error502; + memcached_pass 127.0.0.1:8083; + set $memcached_key $request_uri; + } + + location /memcached/error502 { + proxy_pass http://127.0.0.1:8081; + } + + location /proxy { + client_max_body_size 1; + error_page 413 /proxy/error413; + error_page 400 /proxy/error400; + error_page 502 /proxy/error502; + proxy_pass http://127.0.0.1:8083; + } + + location /proxy/error413 { + proxy_pass http://127.0.0.1:8081; + } + + location /proxy/error400 { + proxy_pass http://127.0.0.1:8081; + } + + location /proxy/error502 { + proxy_pass http://127.0.0.1:8081; + } + + location /unbuf { + client_max_body_size 1m; + error_page 502 /unbuf/error502; + proxy_pass http://127.0.0.1:8083; + proxy_request_buffering off; + proxy_http_version 1.1; + } + + location /unbuf/error502 { + client_max_body_size 1m; + proxy_pass http://127.0.0.1:8081; + } + + location /length { + client_max_body_size 1; + error_page 413 /length/error413; + error_page 502 /length/error502; + proxy_pass http://127.0.0.1:8083; + } + + location /length/error413 { + return 200 "frontend body:$content_length:$request_body:"; + } + + location /length/error502 { + return 200 "frontend body:$content_length:$request_body:"; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + + location / { + proxy_pass http://127.0.0.1:8082; + proxy_set_header X-Body body:$content_length:$request_body:; + } + } + + server { + listen 127.0.0.1:8082; + server_name localhost; + + return 200 "backend $http_x_body"; + } + + server { + listen 127.0.0.1:8083; + server_name localhost; + + return 444; + } +} + +EOF + +$t->run(); + +############################################################################### + +# error_page 413 should work without redefining client_max_body_size + +like(http( + 'POST / HTTP/1.0' . CRLF . + 'Content-Length: 10000' . CRLF . CRLF . + '0123456789' +), qr/ 413 .*custom error 413/s, 'custom error 413'); + +# subrequest after discarding body + +like(http( + 'GET /add HTTP/1.0' . CRLF . CRLF +), qr/backend body:::.*main response/s, 'add'); + +like(http( + 'POST /add HTTP/1.0' . CRLF . + 'Content-Length: 10' . CRLF . CRLF . + '0123456789' +), qr/backend body:::.*main response/s, 'add small'); + +like(http( + 'POST /add HTTP/1.0' . CRLF . + 'Content-Length: 10000' . CRLF . CRLF . + '0123456789' +), qr/backend body:::.*main response/s, 'add long'); + +like(http( + 'POST /add HTTP/1.1' . CRLF . + 'Host: localhost' . CRLF . + 'Connection: close' . CRLF . + 'Transfer-Encoding: chunked' . CRLF . CRLF . + 'a' . CRLF . + '0123456789' . CRLF . + '0' . CRLF . CRLF +), qr/backend body:::.*main response/s, 'add chunked'); + +like(http( + 'POST /add HTTP/1.1' . CRLF . + 'Host: localhost' . CRLF . + 'Connection: close' . CRLF . + 'Transfer-Encoding: chunked' . CRLF . CRLF . + '1' . CRLF . + 'X' . CRLF . + '9' . CRLF . + '123456789' . CRLF . + '0' . CRLF . CRLF +), qr/backend body:::.*main response/s, 'add chunked multi'); + +like(http( + 'POST /add HTTP/1.1' . CRLF . + 'Host: localhost' . CRLF . + 'Connection: close' . CRLF . + 'Transfer-Encoding: chunked' . CRLF . CRLF . + 'ffff' . CRLF . + '0123456789' +), qr/backend body:::.*main response/s, 'add chunked long'); + +# error_page 502 with proxy_pass after discarding body + +like(http( + 'GET /memcached HTTP/1.0' . CRLF . CRLF +), qr/ 502 .*backend body:::/s, 'memcached'); + +like(http( + 'GET /memcached HTTP/1.0' . CRLF . + 'Content-Length: 10' . CRLF . CRLF . + '0123456789' +), qr/ 502 .*backend body:::/s, 'memcached small'); + +like(http( + 'GET /memcached HTTP/1.0' . CRLF . + 'Content-Length: 10000' . CRLF . CRLF . + '0123456789' +), qr/ 502 .*backend body:::/s, 'memcached long'); + +like(http( + 'GET /memcached HTTP/1.1' . CRLF . + 'Host: localhost' . CRLF . + 'Connection: close' . CRLF . + 'Transfer-Encoding: chunked' . CRLF . CRLF . + 'a' . CRLF . + '0123456789' . CRLF . + '0' . CRLF . CRLF +), qr/ 502 .*backend body:::/s, 'memcached chunked'); + +like(http( + 'GET /memcached HTTP/1.1' . CRLF . + 'Host: localhost' . CRLF . + 'Connection: close' . CRLF . + 'Transfer-Encoding: chunked' . CRLF . CRLF . + '1' . CRLF . + 'X' . CRLF . + '9' . CRLF . + '123456789' . CRLF . + '0' . CRLF . CRLF +), qr/ 502 .*backend body:::/s, 'memcached chunked multi'); + +like(http( + 'GET /memcached HTTP/1.1' . CRLF . + 'Host: localhost' . CRLF . + 'Connection: close' . CRLF . + 'Transfer-Encoding: chunked' . CRLF . CRLF . + 'ffff' . CRLF . + '0123456789' +), qr/ 502 .*backend body:::/s, 'memcached chunked long'); + +# error_page 413 with proxy_pass + +like(http( + 'GET /proxy HTTP/1.0' . CRLF . CRLF +), qr/ 502 .*backend body:::/s, 'proxy'); + +like(http( + 'POST /proxy HTTP/1.0' . CRLF . + 'Content-Length: 10' . CRLF . CRLF . + '0123456789' +), qr/ 413 .*backend body:::/s, 'proxy small'); + +like(http( + 'POST /proxy HTTP/1.0' . CRLF . + 'Content-Length: 10000' . CRLF . CRLF . + '0123456789' +), qr/ 413 .*backend body:::/s, 'proxy long'); + +like(http( + 'POST /proxy HTTP/1.1' . CRLF . + 'Host: localhost' . CRLF . + 'Connection: close' . CRLF . + 'Transfer-Encoding: chunked' . CRLF . CRLF . + 'a' . CRLF . + '0123456789' . CRLF . + '0' . CRLF . CRLF +), qr/ 413 .*backend body:::/s, 'proxy chunked'); + +like(http( + 'POST /proxy HTTP/1.1' . CRLF . + 'Host: localhost' . CRLF . + 'Connection: close' . CRLF . + 'Transfer-Encoding: chunked' . CRLF . CRLF . + '1' . CRLF . + 'X' . CRLF . + '9' . CRLF . + '123456789' . CRLF . + '0' . CRLF . CRLF +), qr/ 413 .*backend body:::/s, 'proxy chunked multi'); + +like(http( + 'POST /proxy HTTP/1.1' . CRLF . + 'Host: localhost' . CRLF . + 'Connection: close' . CRLF . + 'Transfer-Encoding: chunked' . CRLF . CRLF . + 'ffff' . CRLF . + '0123456789' +), qr/ 413 .*backend body:::/s, 'proxy chunked long'); + +# error_page 400 with proxy_pass + +# note that "chunked and length" test triggers 400 during parsing +# request headers, and therefore needs error_page at server level + +like(http( + 'POST /proxy HTTP/1.1' . CRLF . + 'Host: localhost' . CRLF . + 'Connection: close' . CRLF . + 'Transfer-Encoding: chunked' . CRLF . CRLF . + '1' . CRLF . + 'X' . CRLF . + 'X' . CRLF +), qr/ 400 .*backend body:::/s, 'proxy chunked bad'); + +like(http( + 'POST /proxy HTTP/1.1' . CRLF . + 'Host: localhost' . CRLF . + 'Connection: close' . CRLF . + 'Content-Length: 10' . CRLF . + 'Transfer-Encoding: chunked' . CRLF . CRLF . + '0' . CRLF . CRLF +), qr/ 400 .*backend body:::/s, 'proxy chunked and length'); + +# error_page 502 after proxy with request buffering disabled + +like(http( + 'GET /unbuf HTTP/1.0' . CRLF . CRLF +), qr/ 502 .*backend body:::/s, 'unbuf proxy'); + +like(http( + 'POST /unbuf HTTP/1.0' . CRLF . + 'Content-Length: 10' . CRLF . CRLF . + '0', + sleep => 0.1, + body => + '123456789' +), qr/ 502 .*backend body:::/s, 'unbuf proxy small'); + +like(http( + 'POST /unbuf HTTP/1.0' . CRLF . + 'Content-Length: 10000' . CRLF . CRLF . + '0123456789' +), qr/ 502 .*backend body:::/s, 'unbuf proxy long'); + +like(http( + 'POST /unbuf HTTP/1.1' . CRLF . + 'Host: localhost' . CRLF . + 'Connection: close' . CRLF . + 'Transfer-Encoding: chunked' . CRLF . CRLF, + sleep => 0.1, + body => + 'a' . CRLF . + '0123456789' . CRLF . + '0' . CRLF . CRLF +), qr/ 502 .*backend body:::/s, 'unbuf proxy chunked'); + +like(http( + 'POST /unbuf HTTP/1.1' . CRLF . + 'Host: localhost' . CRLF . + 'Connection: close' . CRLF . + 'Transfer-Encoding: chunked' . CRLF . CRLF . + '1' . CRLF . + 'X' . CRLF, + sleep => 0.1, + body => + '9' . CRLF . + '123456789' . CRLF . + '0' . CRLF . CRLF +), qr/ 502 .*backend body:::/s, 'unbuf proxy chunked multi'); + +like(http( + 'POST /unbuf HTTP/1.1' . CRLF . + 'Host: localhost' . CRLF . + 'Connection: close' . CRLF . + 'Transfer-Encoding: chunked' . CRLF . CRLF . + 'ffff' . CRLF . + '0123456789' +), qr/ 502 .*backend body:::/s, 'unbuf proxy chunked long'); + +# error_page 413 and $content_length +# (used in fastcgi_pass, grpc_pass, uwsgi_pass) + +like(http( + 'GET /length HTTP/1.0' . CRLF . CRLF +), qr/ 502 .*frontend body:::/s, '$content_length'); + +like(http( + 'POST /length HTTP/1.0' . CRLF . + 'Content-Length: 10' . CRLF . CRLF . + '0123456789' +), qr/ 413 .*frontend body:::/s, '$content_length small'); + +like(http( + 'POST /length HTTP/1.0' . CRLF . + 'Content-Length: 10000' . CRLF . CRLF . + '0123456789' +), qr/ 413 .*frontend body:::/s, '$content_length long'); + +like(http( + 'POST /length HTTP/1.1' . CRLF . + 'Host: localhost' . CRLF . + 'Connection: close' . CRLF . + 'Transfer-Encoding: chunked' . CRLF . CRLF . + 'a' . CRLF . + '0123456789' . CRLF . + '0' . CRLF . CRLF +), qr/ 413 .*frontend body:::/s, '$content_length chunked'); + +like(http( + 'POST /length HTTP/1.1' . CRLF . + 'Host: localhost' . CRLF . + 'Connection: close' . CRLF . + 'Transfer-Encoding: chunked' . CRLF . CRLF . + '1' . CRLF . + 'X' . CRLF . + '9' . CRLF . + '123456789' . CRLF . + '0' . CRLF . CRLF +), qr/ 413 .*frontend body:::/s, '$content_length chunked multi'); + +like(http( + 'POST /length HTTP/1.1' . CRLF . + 'Host: localhost' . CRLF . + 'Connection: close' . CRLF . + 'Transfer-Encoding: chunked' . CRLF . CRLF . + 'ffff' . CRLF . + '0123456789' +), qr/ 413 .*frontend body:::/s, '$content_length chunked long'); + +############################################################################### diff --git a/h2_request_body_discard.t b/h2_request_body_discard.t new file mode 100644 index 00000000..95e32e11 --- /dev/null +++ b/h2_request_body_discard.t @@ -0,0 +1,406 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for discarding request body with HTTP/2. + +############################################################################### + +use warnings; +use strict; + +use Test::More; +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::HTTP2; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new() + ->has(qw/http http_v2 proxy rewrite addition memcached/); + + +$t->plan(38)->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; + + http2 on; + + lingering_timeout 1s; + add_header X-Body body:$content_length:$request_body:; + + client_max_body_size 1k; + + error_page 400 /proxy/error400; + + location / { + error_page 413 /error413; + proxy_pass http://127.0.0.1:8082; + } + + location /error413 { + return 200 "custom error 413"; + } + + location /add { + return 200 "main response"; + add_before_body /add/before; + addition_types *; + client_max_body_size 1m; + } + + location /add/before { + proxy_pass http://127.0.0.1:8081; + } + + location /memcached { + client_max_body_size 1m; + error_page 502 /memcached/error502; + memcached_pass 127.0.0.1:8083; + set $memcached_key $request_uri; + } + + location /memcached/error502 { + proxy_pass http://127.0.0.1:8081; + } + + location /proxy { + client_max_body_size 3; + error_page 413 /proxy/error413; + error_page 400 /proxy/error400; + error_page 502 /proxy/error502; + proxy_pass http://127.0.0.1:8083; + } + + location /proxy/error413 { + proxy_pass http://127.0.0.1:8081; + } + + location /proxy/error400 { + proxy_pass http://127.0.0.1:8081; + } + + location /proxy/error502 { + proxy_pass http://127.0.0.1:8081; + } + + location /unbuf { + client_max_body_size 1m; + error_page 502 /unbuf/error502; + proxy_pass http://127.0.0.1:8083; + proxy_request_buffering off; + proxy_http_version 1.1; + } + + location /unbuf/error502 { + client_max_body_size 1m; + proxy_pass http://127.0.0.1:8081; + } + + location /unbuf2 { + client_max_body_size 1m; + error_page 400 /unbuf2/error400; + proxy_pass http://127.0.0.1:8081; + proxy_request_buffering off; + proxy_http_version 1.1; + } + + location /unbuf2/error400 { + client_max_body_size 1m; + proxy_pass http://127.0.0.1:8081; + } + + location /length { + client_max_body_size 1; + error_page 413 /length/error413; + error_page 502 /length/error502; + proxy_pass http://127.0.0.1:8083; + } + + location /length/error413 { + return 200 "frontend body:$content_length:$request_body:"; + } + + location /length/error502 { + return 200 "frontend body:$content_length:$request_body:"; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + + location / { + proxy_pass http://127.0.0.1:8082; + proxy_set_header X-Body body:$content_length:$request_body:; + } + } + + server { + listen 127.0.0.1:8082; + server_name localhost; + + return 200 "backend $http_x_body"; + } + + server { + listen 127.0.0.1:8083; + server_name localhost; + + return 444; + } +} + +EOF + +$t->run(); + +############################################################################### + +# error_page 413 should work without redefining client_max_body_size + +like(http2_get_body('/', '0123456789' x 128), + qr/status: 413.*custom error 413/s, 'custom error 413'); + +# subrequest after discarding body + +like(http2_get('/add'), + qr/backend body:::.*main response/s, 'add'); +like(http2_get_body('/add', '0123456789'), + qr/backend body:::.*main response/s, 'add small'); +like(http2_get_body_incomplete('/add', 10000, '0123456789'), + qr/backend body:::.*main response/s, 'add long'); +like(http2_get_body_nolen('/add', '0123456789'), + qr/backend body:::.*main response/s, 'add nolen'); +like(http2_get_body_nolen('/add', '0', '123456789'), + qr/backend body:::.*main response/s, 'add nolen multi'); +like(http2_get_body_incomplete_nolen('/add', 10000, '0123456789'), + qr/backend body:::.*main response/s, 'add chunked long'); + +# error_page 502 with proxy_pass after discarding body + +like(http2_get('/memcached'), + qr/backend body:::/s, 'memcached'); +like(http2_get_body('/memcached', '0123456789'), + qr/status: 502.*backend body:::/s, 'memcached small'); +like(http2_get_body_incomplete('/memcached', 10000, '0123456789'), + qr/status: 502.*backend body:::/s, 'memcached long'); +like(http2_get_body_nolen('/memcached', '0123456789'), + qr/status: 502.*backend body:::/s, 'memcached nolen'); +like(http2_get_body_nolen('/memcached', '0', '123456789'), + qr/status: 502.*backend body:::/s, 'memcached nolen multi'); +like(http2_get_body_incomplete_nolen('/memcached', 10000, '0123456789'), + qr/status: 502.*backend body:::/s, 'memcached nolen long'); + +# error_page 413 with proxy_pass + +like(http2_get('/proxy'), + qr/status: 502.*backend body:::/s, 'proxy'); +like(http2_get_body('/proxy', '0123456789'), + qr/status: 413.*backend body:::/s, 'proxy small'); +like(http2_get_body_incomplete('/proxy', 10000, '0123456789'), + qr/status: 413.*backend body:::/s, 'proxy long'); +like(http2_get_body_nolen('/proxy', '0123456789'), + qr/status: 413.*backend body:::/s, 'proxy nolen'); +like(http2_get_body_nolen('/proxy', '0', '123456789'), + qr/status: 413.*backend body:::/s, 'proxy nolen multi'); +like(http2_get_body_incomplete_nolen('/proxy', 10000, '0123456789'), + qr/status: 413.*backend body:::/s, 'proxy nolen long'); + +# error_page 400 with proxy_pass + +# note that "proxy too short" test triggers 400 during parsing +# request headers, and therefore needs error_page at server level + +like(http2_get_body_custom('/proxy', 1), + qr/status: 400.*backend body:::/s, 'proxy too short'); +like(http2_get_body_custom('/proxy', 1, ''), + qr/status: 400.*backend body:::/s, 'proxy too short body'); +like(http2_get_body_custom('/proxy', 1, '01'), + qr/status: 400.*backend body:::/s, 'proxy too long'); +like(http2_get_body_custom('/proxy', 1, '01', more => 1), + qr/status: 400.*backend body:::/s, 'proxy too long more'); + +# error_page 502 after proxy with request buffering disabled + +like(http2_get('/unbuf'), + qr/status: 502.*backend body:::/s, 'unbuf proxy'); +like(http2_get_body('/unbuf', '0123456789'), + qr/status: 502.*backend body:::/s, 'unbuf proxy small'); +like(http2_get_body_incomplete('/unbuf', 10000, '0123456789'), + qr/status: 502.*backend body:::/s, 'unbuf proxy long'); +like(http2_get_body_nolen('/unbuf', '0123456789'), + qr/status: 502.*backend body:::/s, 'unbuf proxy nolen'); +like(http2_get_body_nolen('/unbuf', '0', '123456789'), + qr/status: 502.*backend body:::/s, 'unbuf proxy nolen multi'); +like(http2_get_body_incomplete_nolen('/unbuf', 10000, '0123456789'), + qr/status: 502.*backend body:::/s, 'unbuf proxy nolen long'); + +# error_page 400 after proxy with request buffering disabled + +like(http2_get_body_custom('/unbuf2', 1, '', sleep => 0.1), + qr/status: 400.*backend body:::/s, 'unbuf too short'); +like(http2_get_body_custom('/unbuf2', 1, '01', sleep => 0.1), + qr/status: 400.*backend body:::/s, 'unbuf too long'); +like(http2_get_body_custom('/unbuf2', 1, '01', sleep => 0.1, more => 1), + qr/status: 400.*backend body:::/s, 'unbuf too long more'); + +# error_page 413 and $content_length +# (used in fastcgi_pass, grpc_pass, uwsgi_pass) + +like(http2_get('/length'), + qr/status: 502.*frontend body:::/s, '$content_length'); +like(http2_get_body('/length', '0123456789'), + qr/status: 413.*frontend body:::/s, '$content_length small'); +like(http2_get_body_incomplete('/length', 10000, '0123456789'), + qr/status: 413.*frontend body:::/s, '$content_length long'); +like(http2_get_body_nolen('/length', '0123456789'), + qr/status: 413.*frontend body:::/s, '$content_length nolen'); +like(http2_get_body_nolen('/length', '0', '123456789'), + qr/status: 413.*frontend body:::/s, '$content_length nolen multi'); +like(http2_get_body_incomplete_nolen('/length', 10000, '0123456789'), + qr/status: 413.*frontend body:::/s, '$content_length nolen long'); + +############################################################################### + +sub http2_get { + my ($uri) = @_; + + my $s = Test::Nginx::HTTP2->new(); + my $sid = $s->new_stream({ path => $uri }); + my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + my ($data) = grep { $_->{type} eq "DATA" } @$frames; + + return join("\n", map { "$_: " . $frame->{headers}->{$_}; } + keys %{$frame->{headers}}) . "\n\n" . $data->{data}; +} + +sub http2_get_body { + my ($uri, $body) = @_; + + my $s = Test::Nginx::HTTP2->new(); + my $sid = $s->new_stream({ path => $uri, body => $body }); + my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + my ($data) = grep { $_->{type} eq "DATA" } @$frames; + + return join("\n", map { "$_: " . $frame->{headers}->{$_}; } + keys %{$frame->{headers}}) . "\n\n" . $data->{data}; +} + +sub http2_get_body_nolen { + my ($uri, $body, $body2) = @_; + + my $s = Test::Nginx::HTTP2->new(); + my $sid = $s->new_stream({ path => $uri, body_more => 1 }); + + if (defined $body2) { + $s->h2_body($body, { body_more => 1 }); + $s->h2_body($body2); + } else { + $s->h2_body($body); + } + + my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + my ($data) = grep { $_->{type} eq "DATA" } @$frames; + + return join("\n", map { "$_: " . $frame->{headers}->{$_}; } + keys %{$frame->{headers}}) . "\n\n" . $data->{data}; +} + +sub http2_get_body_incomplete { + my ($uri, $len, $body) = @_; + + my $s = Test::Nginx::HTTP2->new(); + my $sid = $s->new_stream({ + headers => [ + { name => ':method', value => 'GET' }, + { name => ':scheme', value => 'http' }, + { name => ':path', value => $uri }, + { name => ':authority', value => 'localhost' }, + { name => 'content-length', value => $len }, + ], + body_more => 1 + }); + $s->h2_body($body, { body_more => 1 }); + + my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + my ($data) = grep { $_->{type} eq "DATA" } @$frames; + + return join("\n", map { "$_: " . $frame->{headers}->{$_}; } + keys %{$frame->{headers}}) . "\n\n" . $data->{data}; +} + +sub http2_get_body_incomplete_nolen { + my ($uri, $len, $body) = @_; + + my $s = Test::Nginx::HTTP2->new(); + my $sid = $s->new_stream({ path => $uri, body_more => 1 }); + $s->h2_body($body, { body_more => 1 }); + + my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + my ($data) = grep { $_->{type} eq "DATA" } @$frames; + + return join("\n", map { "$_: " . $frame->{headers}->{$_}; } + keys %{$frame->{headers}}) . "\n\n" . $data->{data}; +} + +sub http2_get_body_custom { + my ($uri, $len, $body, %extra) = @_; + + my $s = Test::Nginx::HTTP2->new(); + my $sid = $s->new_stream({ + headers => [ + { name => ':method', value => 'GET' }, + { name => ':scheme', value => 'http' }, + { name => ':path', value => $uri }, + { name => ':authority', value => 'localhost' }, + { name => 'content-length', value => $len }, + ], + body_more => (defined $body ? 1 : undef) + }); + + if (defined $body) { + select undef, undef, undef, $extra{sleep} if $extra{sleep}; + $s->h2_body($body, { body_more => 1 }); + $s->h2_body('') unless $extra{more}; + } + + my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + my ($data) = grep { $_->{type} eq "DATA" } @$frames; + + return join("\n", map { "$_: " . $frame->{headers}->{$_}; } + keys %{$frame->{headers}}) . "\n\n" . $data->{data}; +} + +############################################################################### diff --git a/h3_request_body_discard.t b/h3_request_body_discard.t new file mode 100644 index 00000000..83b616db --- /dev/null +++ b/h3_request_body_discard.t @@ -0,0 +1,425 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for discarding request body with HTTP/3. + +############################################################################### + +use warnings; +use strict; + +use Test::More; +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::HTTP3; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new() + ->has(qw/http http_v3 proxy rewrite addition memcached cryptx/) + ->has_daemon('openssl'); + + +$t->plan(37)->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + ssl_certificate localhost.crt; + ssl_certificate_key localhost.key; + + server { + listen 127.0.0.1:%%PORT_8980_UDP%% quic; + server_name localhost; + + lingering_timeout 1s; + add_header X-Body body:$content_length:$request_body:; + + client_max_body_size 1k; + + location / { + error_page 413 /error413; + proxy_pass http://127.0.0.1:8082; + } + + location /error413 { + return 200 "custom error 413"; + } + + location /add { + return 200 "main response"; + add_before_body /add/before; + addition_types *; + client_max_body_size 1m; + } + + location /add/before { + proxy_pass http://127.0.0.1:8081; + } + + location /memcached { + client_max_body_size 1m; + error_page 502 /memcached/error502; + memcached_pass 127.0.0.1:8083; + set $memcached_key $request_uri; + } + + location /memcached/error502 { + proxy_pass http://127.0.0.1:8081; + } + + location /proxy { + client_max_body_size 3; + error_page 413 /proxy/error413; + error_page 400 /proxy/error400; + error_page 502 /proxy/error502; + proxy_pass http://127.0.0.1:8083; + } + + location /proxy/error413 { + proxy_pass http://127.0.0.1:8081; + } + + location /proxy/error400 { + proxy_pass http://127.0.0.1:8081; + } + + location /proxy/error502 { + proxy_pass http://127.0.0.1:8081; + } + + location /unbuf { + client_max_body_size 1m; + error_page 502 /unbuf/error502; + proxy_pass http://127.0.0.1:8083; + proxy_request_buffering off; + proxy_http_version 1.1; + } + + location /unbuf/error502 { + client_max_body_size 1m; + proxy_pass http://127.0.0.1:8081; + } + + location /unbuf2 { + client_max_body_size 1m; + error_page 400 /unbuf2/error400; + proxy_pass http://127.0.0.1:8081; + proxy_request_buffering off; + proxy_http_version 1.1; + } + + location /unbuf2/error400 { + client_max_body_size 1m; + proxy_pass http://127.0.0.1:8081; + } + + location /length { + client_max_body_size 1; + error_page 413 /length/error413; + error_page 502 /length/error502; + proxy_pass http://127.0.0.1:8083; + } + + location /length/error413 { + return 200 "frontend body:$content_length:$request_body:"; + } + + location /length/error502 { + return 200 "frontend body:$content_length:$request_body:"; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + + location / { + proxy_pass http://127.0.0.1:8082; + proxy_set_header X-Body body:$content_length:$request_body:; + } + } + + server { + listen 127.0.0.1:8082; + server_name localhost; + + return 200 "backend $http_x_body"; + } + + server { + listen 127.0.0.1:8083; + server_name localhost; + + return 444; + } +} + +EOF + +$t->write_file('openssl.conf', <testdir(); + +foreach my $name ('localhost') { + system('openssl req -x509 -new ' + . "-config $d/openssl.conf -subj /CN=$name/ " + . "-out $d/$name.crt -keyout $d/$name.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create certificate for $name: $!\n"; +} + +$t->run(); + +############################################################################### + +# error_page 413 should work without redefining client_max_body_size + +like(http3_get_body('/', '0123456789' x 128), + qr/status: 413.*custom error 413/s, 'custom error 413'); + +# subrequest after discarding body + +like(http3_get('/add'), + qr/backend body:::.*main response/s, 'add'); +like(http3_get_body('/add', '0123456789'), + qr/backend body:::.*main response/s, 'add small'); +like(http3_get_body_incomplete('/add', 10000, '0123456789'), + qr/backend body:::.*main response/s, 'add long'); +like(http3_get_body_nolen('/add', '0123456789'), + qr/backend body:::.*main response/s, 'add nolen'); +like(http3_get_body_nolen('/add', '0', '123456789'), + qr/backend body:::.*main response/s, 'add nolen multi'); +like(http3_get_body_incomplete_nolen('/add', 10000, '0123456789'), + qr/backend body:::.*main response/s, 'add chunked long'); + +# error_page 502 with proxy_pass after discarding body + +like(http3_get('/memcached'), + qr/backend body:::/s, 'memcached'); +like(http3_get_body('/memcached', '0123456789'), + qr/status: 502.*backend body:::/s, 'memcached small'); +like(http3_get_body_incomplete('/memcached', 10000, '0123456789'), + qr/status: 502.*backend body:::/s, 'memcached long'); +like(http3_get_body_nolen('/memcached', '0123456789'), + qr/status: 502.*backend body:::/s, 'memcached nolen'); +like(http3_get_body_nolen('/memcached', '0', '123456789'), + qr/status: 502.*backend body:::/s, 'memcached nolen multi'); +like(http3_get_body_incomplete_nolen('/memcached', 10000, '0123456789'), + qr/status: 502.*backend body:::/s, 'memcached nolen long'); + +# error_page 413 with proxy_pass + +like(http3_get('/proxy'), + qr/status: 502.*backend body:::/s, 'proxy'); +like(http3_get_body('/proxy', '0123456789'), + qr/status: 413.*backend body:::/s, 'proxy small'); +like(http3_get_body_incomplete('/proxy', 10000, '0123456789'), + qr/status: 413.*backend body:::/s, 'proxy long'); +like(http3_get_body_nolen('/proxy', '0123456789'), + qr/status: 413.*backend body:::/s, 'proxy nolen'); +like(http3_get_body_nolen('/proxy', '0', '123456789'), + qr/status: 413.*backend body:::/s, 'proxy nolen multi'); +like(http3_get_body_incomplete_nolen('/proxy', '0123456789'), + qr/status: 413.*backend body:::/s, 'proxy nolen long'); + +# error_page 400 with proxy_pass + +like(http3_get_body_custom('/proxy', 1, ''), + qr/status: 400.*backend body:::/s, 'proxy too short'); +like(http3_get_body_custom('/proxy', 1, '01'), + qr/status: 400.*backend body:::/s, 'proxy too long'); +like(http3_get_body_custom('/proxy', 1, '01', more => 1), + qr/status: 400.*backend body:::/s, 'proxy too long more'); + +# error_page 502 after proxy with request buffering disabled + +like(http3_get('/unbuf'), + qr/status: 502.*backend body:::/s, 'unbuf proxy'); +like(http3_get_body_custom('/unbuf', 10, '0123456789', sleep => 0.1), + qr/status: 502.*backend body:::/s, 'unbuf proxy small'); +like(http3_get_body_incomplete('/unbuf', 10000, '0123456789'), + qr/status: 502.*backend body:::/s, 'unbuf proxy long'); +like(http3_get_body_nolen('/unbuf', '0123456789'), + qr/status: 502.*backend body:::/s, 'unbuf proxy nolen'); +like(http3_get_body_nolen('/unbuf', '0', '123456789'), + qr/status: 502.*backend body:::/s, 'unbuf proxy nolen multi'); +like(http3_get_body_incomplete_nolen('/unbuf', 10000, '0123456789'), + qr/status: 502.*backend body:::/s, 'unbuf proxy nolen long'); + +# error_page 400 after proxy with request buffering disabled + +like(http3_get_body_custom('/unbuf2', 1, '', sleep => 0.1), + qr/status: 400.*backend body:::/s, 'unbuf too short'); +like(http3_get_body_custom('/unbuf2', 1, '01', sleep => 0.1), + qr/status: 400.*backend body:::/s, 'unbuf too long'); +like(http3_get_body_custom('/unbuf2', 1, '01', sleep => 0.1, more => 1), + qr/status: 400.*backend body:::/s, 'unbuf too long more'); + +# error_page 413 and $content_length +# (used in fastcgi_pass, grpc_pass, uwsgi_pass) + +like(http3_get('/length'), + qr/status: 502.*frontend body:::/s, '$content_length'); +like(http3_get_body('/length', '0123456789'), + qr/status: 413.*frontend body:::/s, '$content_length small'); +like(http3_get_body_incomplete('/length', 10000, '0123456789'), + qr/status: 413.*frontend body:::/s, '$content_length long'); +like(http3_get_body_nolen('/length', '0123456789'), + qr/status: 413.*frontend body:::/s, '$content_length nolen'); +like(http3_get_body_nolen('/length', '0', '123456789'), + qr/status: 413.*frontend body:::/s, '$content_length nolen multi'); +like(http3_get_body_incomplete_nolen('/length', 10000, '0123456789'), + qr/status: 413.*frontend body:::/s, '$content_length nolen long'); + +############################################################################### + +sub http3_get { + my ($uri) = @_; + + my $s = Test::Nginx::HTTP3->new(); + my $sid = $s->new_stream({ path => $uri }); + my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + my (@data) = grep { $_->{type} eq "DATA" } @$frames; + + return join("\n", map { "$_: " . $frame->{headers}->{$_}; } + keys %{$frame->{headers}}) . "\n\n" + . join("", map { $_->{data} } @data); +} + +sub http3_get_body { + my ($uri, $body) = @_; + + my $s = Test::Nginx::HTTP3->new(); + my $sid = $s->new_stream({ path => $uri, body => $body }); + my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + my (@data) = grep { $_->{type} eq "DATA" } @$frames; + + return join("\n", map { "$_: " . $frame->{headers}->{$_}; } + keys %{$frame->{headers}}) . "\n\n" + . join("", map { $_->{data} } @data); +} + +sub http3_get_body_nolen { + my ($uri, $body, $body2) = @_; + + my $s = Test::Nginx::HTTP3->new(); + my $sid = $s->new_stream({ path => $uri, body_more => 1 }); + + if (defined $body2) { + select undef, undef, undef, 0.1; + $s->h3_body($body, $sid, { body_more => 1 }); + select undef, undef, undef, 0.1; + $s->h3_body($body2, $sid); + } else { + select undef, undef, undef, 0.1; + $s->h3_body($body, $sid); + } + + my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + my (@data) = grep { $_->{type} eq "DATA" } @$frames; + + return join("\n", map { "$_: " . $frame->{headers}->{$_}; } + keys %{$frame->{headers}}) . "\n\n" + . join("", map { $_->{data} } @data); +} + +sub http3_get_body_incomplete { + my ($uri, $len, $body) = @_; + + my $s = Test::Nginx::HTTP3->new(); + my $sid = $s->new_stream({ + headers => [ + { name => ':method', value => 'GET' }, + { name => ':scheme', value => 'http' }, + { name => ':path', value => $uri }, + { name => ':authority', value => 'localhost' }, + { name => 'content-length', value => $len }, + ], + body_more => 1 + }); + $s->h3_body($body, $sid, { body_more => 1 }); + + my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + my (@data) = grep { $_->{type} eq "DATA" } @$frames; + + return join("\n", map { "$_: " . $frame->{headers}->{$_}; } + keys %{$frame->{headers}}) . "\n\n" + . join("", map { $_->{data} } @data); +} + +sub http3_get_body_incomplete_nolen { + my ($uri, $body) = @_; + + my $s = Test::Nginx::HTTP3->new(); + my $sid = $s->new_stream({ path => $uri, body_more => 1 }); + $s->h3_body($body, $sid, { body_more => 1 }); + + my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + my (@data) = grep { $_->{type} eq "DATA" } @$frames; + + return join("\n", map { "$_: " . $frame->{headers}->{$_}; } + keys %{$frame->{headers}}) . "\n\n" + . join("", map { $_->{data} } @data); +} + +sub http3_get_body_custom { + my ($uri, $len, $body, %extra) = @_; + + my $s = Test::Nginx::HTTP3->new(); + my $sid = $s->new_stream({ + headers => [ + { name => ':method', value => 'GET' }, + { name => ':scheme', value => 'http' }, + { name => ':path', value => $uri }, + { name => ':authority', value => 'localhost' }, + { name => 'content-length', value => $len }, + ], + body_more => 1 + }); + select undef, undef, undef, $extra{sleep} if $extra{sleep}; + $s->h3_body($body, $sid, { body_more => 1 }); + $s->h3_body('', $sid) unless $extra{more}; + + my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + my (@data) = grep { $_->{type} eq "DATA" } @$frames; + + return join("\n", map { "$_: " . $frame->{headers}->{$_}; } + keys %{$frame->{headers}}) . "\n\n" + . join("", map { $_->{data} } @data); +} + +############################################################################### From c60e1f1422c548abe48c93d8ea33bbc4bc951d4e Mon Sep 17 00:00:00 2001 From: Maxim Dounin Date: Tue, 19 Aug 2025 03:25:52 +0300 Subject: [PATCH 3/3] Tests: adjusted delays in body discard tests. At least h3_request_body_discard.t occasionally fails on slow hosts due to too short delays, and using longer delays fixes this. Co-Authored-By: Elijah Zupancic Cherry-Picked-From: https://freenginx.org/hg/nginx-tests/rev/250fb78dd27079ed3cb3fd5d3b7a0132ad2ce89f --- body_discard.t | 6 +++--- h2_request_body_discard.t | 6 +++--- h3_request_body_discard.t | 14 +++++++------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/body_discard.t b/body_discard.t index a8ff653d..1e0e5fdf 100644 --- a/body_discard.t +++ b/body_discard.t @@ -349,7 +349,7 @@ like(http( 'POST /unbuf HTTP/1.0' . CRLF . 'Content-Length: 10' . CRLF . CRLF . '0', - sleep => 0.1, + sleep => 0.2, body => '123456789' ), qr/ 502 .*backend body:::/s, 'unbuf proxy small'); @@ -365,7 +365,7 @@ like(http( 'Host: localhost' . CRLF . 'Connection: close' . CRLF . 'Transfer-Encoding: chunked' . CRLF . CRLF, - sleep => 0.1, + sleep => 0.2, body => 'a' . CRLF . '0123456789' . CRLF . @@ -379,7 +379,7 @@ like(http( 'Transfer-Encoding: chunked' . CRLF . CRLF . '1' . CRLF . 'X' . CRLF, - sleep => 0.1, + sleep => 0.2, body => '9' . CRLF . '123456789' . CRLF . diff --git a/h2_request_body_discard.t b/h2_request_body_discard.t index 95e32e11..07aa503f 100644 --- a/h2_request_body_discard.t +++ b/h2_request_body_discard.t @@ -257,11 +257,11 @@ like(http2_get_body_incomplete_nolen('/unbuf', 10000, '0123456789'), # error_page 400 after proxy with request buffering disabled -like(http2_get_body_custom('/unbuf2', 1, '', sleep => 0.1), +like(http2_get_body_custom('/unbuf2', 1, '', sleep => 0.2), qr/status: 400.*backend body:::/s, 'unbuf too short'); -like(http2_get_body_custom('/unbuf2', 1, '01', sleep => 0.1), +like(http2_get_body_custom('/unbuf2', 1, '01', sleep => 0.2), qr/status: 400.*backend body:::/s, 'unbuf too long'); -like(http2_get_body_custom('/unbuf2', 1, '01', sleep => 0.1, more => 1), +like(http2_get_body_custom('/unbuf2', 1, '01', sleep => 0.2, more => 1), qr/status: 400.*backend body:::/s, 'unbuf too long more'); # error_page 413 and $content_length diff --git a/h3_request_body_discard.t b/h3_request_body_discard.t index 83b616db..a1ecaabe 100644 --- a/h3_request_body_discard.t +++ b/h3_request_body_discard.t @@ -257,7 +257,7 @@ like(http3_get_body_custom('/proxy', 1, '01', more => 1), like(http3_get('/unbuf'), qr/status: 502.*backend body:::/s, 'unbuf proxy'); -like(http3_get_body_custom('/unbuf', 10, '0123456789', sleep => 0.1), +like(http3_get_body_custom('/unbuf', 10, '0123456789', sleep => 0.2), qr/status: 502.*backend body:::/s, 'unbuf proxy small'); like(http3_get_body_incomplete('/unbuf', 10000, '0123456789'), qr/status: 502.*backend body:::/s, 'unbuf proxy long'); @@ -270,11 +270,11 @@ like(http3_get_body_incomplete_nolen('/unbuf', 10000, '0123456789'), # error_page 400 after proxy with request buffering disabled -like(http3_get_body_custom('/unbuf2', 1, '', sleep => 0.1), +like(http3_get_body_custom('/unbuf2', 1, '', sleep => 0.2), qr/status: 400.*backend body:::/s, 'unbuf too short'); -like(http3_get_body_custom('/unbuf2', 1, '01', sleep => 0.1), +like(http3_get_body_custom('/unbuf2', 1, '01', sleep => 0.2), qr/status: 400.*backend body:::/s, 'unbuf too long'); -like(http3_get_body_custom('/unbuf2', 1, '01', sleep => 0.1, more => 1), +like(http3_get_body_custom('/unbuf2', 1, '01', sleep => 0.2, more => 1), qr/status: 400.*backend body:::/s, 'unbuf too long more'); # error_page 413 and $content_length @@ -332,12 +332,12 @@ sub http3_get_body_nolen { my $sid = $s->new_stream({ path => $uri, body_more => 1 }); if (defined $body2) { - select undef, undef, undef, 0.1; + select undef, undef, undef, 0.2; $s->h3_body($body, $sid, { body_more => 1 }); - select undef, undef, undef, 0.1; + select undef, undef, undef, 0.2; $s->h3_body($body2, $sid); } else { - select undef, undef, undef, 0.1; + select undef, undef, undef, 0.2; $s->h3_body($body, $sid); }