comparison h3_request_body_discard.t @ 1961:fe6f22da53ec

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).
author Maxim Dounin <mdounin@mdounin.ru>
date Sat, 27 Apr 2024 18:55:50 +0300
parents
children
comparison
equal deleted inserted replaced
1960:e44ee916b959 1961:fe6f22da53ec
1 #!/usr/bin/perl
2
3 # (C) Maxim Dounin
4
5 # Tests for discarding request body with HTTP/3.
6
7 ###############################################################################
8
9 use warnings;
10 use strict;
11
12 use Test::More;
13 use Socket qw/ CRLF /;
14
15 BEGIN { use FindBin; chdir($FindBin::Bin); }
16
17 use lib 'lib';
18 use Test::Nginx;
19 use Test::Nginx::HTTP3;
20
21 ###############################################################################
22
23 select STDERR; $| = 1;
24 select STDOUT; $| = 1;
25
26 my $t = Test::Nginx->new()
27 ->has(qw/http http_v3 proxy rewrite addition memcached cryptx/)
28 ->has_daemon('openssl');
29
30 plan(skip_all => 'not yet') unless $t->has_version('1.27.0');
31
32 $t->plan(37)->write_file_expand('nginx.conf', <<'EOF');
33
34 %%TEST_GLOBALS%%
35
36 daemon off;
37
38 events {
39 }
40
41 http {
42 %%TEST_GLOBALS_HTTP%%
43
44 ssl_certificate localhost.crt;
45 ssl_certificate_key localhost.key;
46
47 server {
48 listen 127.0.0.1:%%PORT_8980_UDP%% quic;
49 server_name localhost;
50
51 lingering_timeout 1s;
52 add_header X-Body body:$content_length:$request_body:;
53
54 client_max_body_size 1k;
55
56 location / {
57 error_page 413 /error413;
58 proxy_pass http://127.0.0.1:8082;
59 }
60
61 location /error413 {
62 return 200 "custom error 413";
63 }
64
65 location /add {
66 return 200 "main response";
67 add_before_body /add/before;
68 addition_types *;
69 client_max_body_size 1m;
70 }
71
72 location /add/before {
73 proxy_pass http://127.0.0.1:8081;
74 }
75
76 location /memcached {
77 client_max_body_size 1m;
78 error_page 502 /memcached/error502;
79 memcached_pass 127.0.0.1:8083;
80 set $memcached_key $request_uri;
81 }
82
83 location /memcached/error502 {
84 proxy_pass http://127.0.0.1:8081;
85 }
86
87 location /proxy {
88 client_max_body_size 3;
89 error_page 413 /proxy/error413;
90 error_page 400 /proxy/error400;
91 error_page 502 /proxy/error502;
92 proxy_pass http://127.0.0.1:8083;
93 }
94
95 location /proxy/error413 {
96 proxy_pass http://127.0.0.1:8081;
97 }
98
99 location /proxy/error400 {
100 proxy_pass http://127.0.0.1:8081;
101 }
102
103 location /proxy/error502 {
104 proxy_pass http://127.0.0.1:8081;
105 }
106
107 location /unbuf {
108 client_max_body_size 1m;
109 error_page 502 /unbuf/error502;
110 proxy_pass http://127.0.0.1:8083;
111 proxy_request_buffering off;
112 proxy_http_version 1.1;
113 }
114
115 location /unbuf/error502 {
116 client_max_body_size 1m;
117 proxy_pass http://127.0.0.1:8081;
118 }
119
120 location /unbuf2 {
121 client_max_body_size 1m;
122 error_page 400 /unbuf2/error400;
123 proxy_pass http://127.0.0.1:8081;
124 proxy_request_buffering off;
125 proxy_http_version 1.1;
126 }
127
128 location /unbuf2/error400 {
129 client_max_body_size 1m;
130 proxy_pass http://127.0.0.1:8081;
131 }
132
133 location /length {
134 client_max_body_size 1;
135 error_page 413 /length/error413;
136 error_page 502 /length/error502;
137 proxy_pass http://127.0.0.1:8083;
138 }
139
140 location /length/error413 {
141 return 200 "frontend body:$content_length:$request_body:";
142 }
143
144 location /length/error502 {
145 return 200 "frontend body:$content_length:$request_body:";
146 }
147 }
148
149 server {
150 listen 127.0.0.1:8081;
151 server_name localhost;
152
153 location / {
154 proxy_pass http://127.0.0.1:8082;
155 proxy_set_header X-Body body:$content_length:$request_body:;
156 }
157 }
158
159 server {
160 listen 127.0.0.1:8082;
161 server_name localhost;
162
163 return 200 "backend $http_x_body";
164 }
165
166 server {
167 listen 127.0.0.1:8083;
168 server_name localhost;
169
170 return 444;
171 }
172 }
173
174 EOF
175
176 $t->write_file('openssl.conf', <<EOF);
177 [ req ]
178 default_bits = 2048
179 encrypt_key = no
180 distinguished_name = req_distinguished_name
181 [ req_distinguished_name ]
182 EOF
183
184 my $d = $t->testdir();
185
186 foreach my $name ('localhost') {
187 system('openssl req -x509 -new '
188 . "-config $d/openssl.conf -subj /CN=$name/ "
189 . "-out $d/$name.crt -keyout $d/$name.key "
190 . ">>$d/openssl.out 2>&1") == 0
191 or die "Can't create certificate for $name: $!\n";
192 }
193
194 $t->run();
195
196 ###############################################################################
197
198 # error_page 413 should work without redefining client_max_body_size
199
200 like(http3_get_body('/', '0123456789' x 128),
201 qr/status: 413.*custom error 413/s, 'custom error 413');
202
203 # subrequest after discarding body
204
205 like(http3_get('/add'),
206 qr/backend body:::.*main response/s, 'add');
207 like(http3_get_body('/add', '0123456789'),
208 qr/backend body:::.*main response/s, 'add small');
209 like(http3_get_body_incomplete('/add', 10000, '0123456789'),
210 qr/backend body:::.*main response/s, 'add long');
211 like(http3_get_body_nolen('/add', '0123456789'),
212 qr/backend body:::.*main response/s, 'add nolen');
213 like(http3_get_body_nolen('/add', '0', '123456789'),
214 qr/backend body:::.*main response/s, 'add nolen multi');
215 like(http3_get_body_incomplete_nolen('/add', 10000, '0123456789'),
216 qr/backend body:::.*main response/s, 'add chunked long');
217
218 # error_page 502 with proxy_pass after discarding body
219
220 like(http3_get('/memcached'),
221 qr/backend body:::/s, 'memcached');
222 like(http3_get_body('/memcached', '0123456789'),
223 qr/status: 502.*backend body:::/s, 'memcached small');
224 like(http3_get_body_incomplete('/memcached', 10000, '0123456789'),
225 qr/status: 502.*backend body:::/s, 'memcached long');
226 like(http3_get_body_nolen('/memcached', '0123456789'),
227 qr/status: 502.*backend body:::/s, 'memcached nolen');
228 like(http3_get_body_nolen('/memcached', '0', '123456789'),
229 qr/status: 502.*backend body:::/s, 'memcached nolen multi');
230 like(http3_get_body_incomplete_nolen('/memcached', 10000, '0123456789'),
231 qr/status: 502.*backend body:::/s, 'memcached nolen long');
232
233 # error_page 413 with proxy_pass
234
235 like(http3_get('/proxy'),
236 qr/status: 502.*backend body:::/s, 'proxy');
237 like(http3_get_body('/proxy', '0123456789'),
238 qr/status: 413.*backend body:::/s, 'proxy small');
239 like(http3_get_body_incomplete('/proxy', 10000, '0123456789'),
240 qr/status: 413.*backend body:::/s, 'proxy long');
241 like(http3_get_body_nolen('/proxy', '0123456789'),
242 qr/status: 413.*backend body:::/s, 'proxy nolen');
243 like(http3_get_body_nolen('/proxy', '0', '123456789'),
244 qr/status: 413.*backend body:::/s, 'proxy nolen multi');
245 like(http3_get_body_incomplete_nolen('/proxy', '0123456789'),
246 qr/status: 413.*backend body:::/s, 'proxy nolen long');
247
248 # error_page 400 with proxy_pass
249
250 like(http3_get_body_custom('/proxy', 1, ''),
251 qr/status: 400.*backend body:::/s, 'proxy too short');
252 like(http3_get_body_custom('/proxy', 1, '01'),
253 qr/status: 400.*backend body:::/s, 'proxy too long');
254 like(http3_get_body_custom('/proxy', 1, '01', more => 1),
255 qr/status: 400.*backend body:::/s, 'proxy too long more');
256
257 # error_page 502 after proxy with request buffering disabled
258
259 like(http3_get('/unbuf'),
260 qr/status: 502.*backend body:::/s, 'unbuf proxy');
261 like(http3_get_body_custom('/unbuf', 10, '0123456789', sleep => 0.1),
262 qr/status: 502.*backend body:::/s, 'unbuf proxy small');
263 like(http3_get_body_incomplete('/unbuf', 10000, '0123456789'),
264 qr/status: 502.*backend body:::/s, 'unbuf proxy long');
265 like(http3_get_body_nolen('/unbuf', '0123456789'),
266 qr/status: 502.*backend body:::/s, 'unbuf proxy nolen');
267 like(http3_get_body_nolen('/unbuf', '0', '123456789'),
268 qr/status: 502.*backend body:::/s, 'unbuf proxy nolen multi');
269 like(http3_get_body_incomplete_nolen('/unbuf', 10000, '0123456789'),
270 qr/status: 502.*backend body:::/s, 'unbuf proxy nolen long');
271
272 # error_page 400 after proxy with request buffering disabled
273
274 like(http3_get_body_custom('/unbuf2', 1, '', sleep => 0.1),
275 qr/status: 400.*backend body:::/s, 'unbuf too short');
276 like(http3_get_body_custom('/unbuf2', 1, '01', sleep => 0.1),
277 qr/status: 400.*backend body:::/s, 'unbuf too long');
278 like(http3_get_body_custom('/unbuf2', 1, '01', sleep => 0.1, more => 1),
279 qr/status: 400.*backend body:::/s, 'unbuf too long more');
280
281 # error_page 413 and $content_length
282 # (used in fastcgi_pass, grpc_pass, uwsgi_pass)
283
284 like(http3_get('/length'),
285 qr/status: 502.*frontend body:::/s, '$content_length');
286 like(http3_get_body('/length', '0123456789'),
287 qr/status: 413.*frontend body:::/s, '$content_length small');
288 like(http3_get_body_incomplete('/length', 10000, '0123456789'),
289 qr/status: 413.*frontend body:::/s, '$content_length long');
290 like(http3_get_body_nolen('/length', '0123456789'),
291 qr/status: 413.*frontend body:::/s, '$content_length nolen');
292 like(http3_get_body_nolen('/length', '0', '123456789'),
293 qr/status: 413.*frontend body:::/s, '$content_length nolen multi');
294 like(http3_get_body_incomplete_nolen('/length', 10000, '0123456789'),
295 qr/status: 413.*frontend body:::/s, '$content_length nolen long');
296
297 ###############################################################################
298
299 sub http3_get {
300 my ($uri) = @_;
301
302 my $s = Test::Nginx::HTTP3->new();
303 my $sid = $s->new_stream({ path => $uri });
304 my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
305
306 my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
307 my (@data) = grep { $_->{type} eq "DATA" } @$frames;
308
309 return join("\n", map { "$_: " . $frame->{headers}->{$_}; }
310 keys %{$frame->{headers}}) . "\n\n"
311 . join("", map { $_->{data} } @data);
312 }
313
314 sub http3_get_body {
315 my ($uri, $body) = @_;
316
317 my $s = Test::Nginx::HTTP3->new();
318 my $sid = $s->new_stream({ path => $uri, body => $body });
319 my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
320
321 my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
322 my (@data) = grep { $_->{type} eq "DATA" } @$frames;
323
324 return join("\n", map { "$_: " . $frame->{headers}->{$_}; }
325 keys %{$frame->{headers}}) . "\n\n"
326 . join("", map { $_->{data} } @data);
327 }
328
329 sub http3_get_body_nolen {
330 my ($uri, $body, $body2) = @_;
331
332 my $s = Test::Nginx::HTTP3->new();
333 my $sid = $s->new_stream({ path => $uri, body_more => 1 });
334
335 if (defined $body2) {
336 select undef, undef, undef, 0.1;
337 $s->h3_body($body, $sid, { body_more => 1 });
338 select undef, undef, undef, 0.1;
339 $s->h3_body($body2, $sid);
340 } else {
341 select undef, undef, undef, 0.1;
342 $s->h3_body($body, $sid);
343 }
344
345 my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
346
347 my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
348 my (@data) = grep { $_->{type} eq "DATA" } @$frames;
349
350 return join("\n", map { "$_: " . $frame->{headers}->{$_}; }
351 keys %{$frame->{headers}}) . "\n\n"
352 . join("", map { $_->{data} } @data);
353 }
354
355 sub http3_get_body_incomplete {
356 my ($uri, $len, $body) = @_;
357
358 my $s = Test::Nginx::HTTP3->new();
359 my $sid = $s->new_stream({
360 headers => [
361 { name => ':method', value => 'GET' },
362 { name => ':scheme', value => 'http' },
363 { name => ':path', value => $uri },
364 { name => ':authority', value => 'localhost' },
365 { name => 'content-length', value => $len },
366 ],
367 body_more => 1
368 });
369 $s->h3_body($body, $sid, { body_more => 1 });
370
371 my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
372
373 my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
374 my (@data) = grep { $_->{type} eq "DATA" } @$frames;
375
376 return join("\n", map { "$_: " . $frame->{headers}->{$_}; }
377 keys %{$frame->{headers}}) . "\n\n"
378 . join("", map { $_->{data} } @data);
379 }
380
381 sub http3_get_body_incomplete_nolen {
382 my ($uri, $body) = @_;
383
384 my $s = Test::Nginx::HTTP3->new();
385 my $sid = $s->new_stream({ path => $uri, body_more => 1 });
386 $s->h3_body($body, $sid, { body_more => 1 });
387
388 my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
389
390 my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
391 my (@data) = grep { $_->{type} eq "DATA" } @$frames;
392
393 return join("\n", map { "$_: " . $frame->{headers}->{$_}; }
394 keys %{$frame->{headers}}) . "\n\n"
395 . join("", map { $_->{data} } @data);
396 }
397
398 sub http3_get_body_custom {
399 my ($uri, $len, $body, %extra) = @_;
400
401 my $s = Test::Nginx::HTTP3->new();
402 my $sid = $s->new_stream({
403 headers => [
404 { name => ':method', value => 'GET' },
405 { name => ':scheme', value => 'http' },
406 { name => ':path', value => $uri },
407 { name => ':authority', value => 'localhost' },
408 { name => 'content-length', value => $len },
409 ],
410 body_more => 1
411 });
412 select undef, undef, undef, $extra{sleep} if $extra{sleep};
413 $s->h3_body($body, $sid, { body_more => 1 });
414 $s->h3_body('', $sid) unless $extra{more};
415
416 my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
417
418 my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
419 my (@data) = grep { $_->{type} eq "DATA" } @$frames;
420
421 return join("\n", map { "$_: " . $frame->{headers}->{$_}; }
422 keys %{$frame->{headers}}) . "\n\n"
423 . join("", map { $_->{data} } @data);
424 }
425
426 ###############################################################################