comparison h3_headers.t @ 1876:74cb9454a13e

Tests: HTTP/3 header tests.
author Sergey Kandaurov <pluknet@nginx.com>
date Tue, 30 Aug 2022 14:29:39 +0400
parents
children ff50c265a5ac
comparison
equal deleted inserted replaced
1875:f50c2a65ccc0 1876:74cb9454a13e
1 #!/usr/bin/perl
2
3 # (C) Sergey Kandaurov
4 # (C) Nginx, Inc.
5
6 # Tests for HTTP/3 headers.
7
8 ###############################################################################
9
10 use warnings;
11 use strict;
12
13 use Test::More;
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 eval { require Crypt::Misc; die if $Crypt::Misc::VERSION < 0.067; };
27 plan(skip_all => 'CryptX version >= 0.067 required') if $@;
28
29 my $t = Test::Nginx->new()->has(qw/http http_v3 proxy rewrite/)
30 ->has_daemon('openssl')->plan(66)
31 ->write_file_expand('nginx.conf', <<'EOF');
32
33 %%TEST_GLOBALS%%
34
35 daemon off;
36
37 events {
38 }
39
40 http {
41 %%TEST_GLOBALS_HTTP%%
42
43 ssl_certificate_key localhost.key;
44 ssl_certificate localhost.crt;
45
46 server {
47 listen 127.0.0.1:%%PORT_8980_UDP%% quic;
48 listen 127.0.0.1:8081;
49 server_name localhost;
50
51 location / {
52 add_header X-Sent-Foo $http_x_foo;
53 add_header X-Referer $http_referer;
54 add_header X-Path $uri;
55 return 200;
56 }
57
58 location /proxy/ {
59 add_header X-UC-a $upstream_cookie_a;
60 add_header X-UC-c $upstream_cookie_c;
61 proxy_pass http://127.0.0.1:8083/;
62 proxy_set_header X-Cookie-a $cookie_a;
63 proxy_set_header X-Cookie-c $cookie_c;
64 }
65
66 location /proxy2/ {
67 proxy_pass http://127.0.0.1:8081/;
68 }
69
70 location /set-cookie {
71 add_header Set-Cookie a=b;
72 add_header Set-Cookie c=d;
73 return 200;
74 }
75
76 location /cookie {
77 add_header X-Cookie $http_cookie;
78 add_header X-Cookie-a $cookie_a;
79 add_header X-Cookie-c $cookie_c;
80 return 200;
81 }
82 }
83
84 server {
85 listen 127.0.0.1:%%PORT_8984_UDP%% quic;
86 server_name localhost;
87
88 large_client_header_buffers 4 512;
89 }
90
91 server {
92 listen 127.0.0.1:%%PORT_8985_UDP%% quic;
93 server_name localhost;
94
95 large_client_header_buffers 1 512;
96 }
97
98 server {
99 listen 127.0.0.1:%%PORT_8986_UDP%% quic;
100 server_name localhost;
101
102 underscores_in_headers on;
103 add_header X-Sent-Foo $http_x_foo always;
104 }
105
106 server {
107 listen 127.0.0.1:%%PORT_8987_UDP%% quic;
108 server_name localhost;
109
110 ignore_invalid_headers off;
111 add_header X-Sent-Foo $http_x_foo always;
112 }
113 }
114
115 EOF
116
117 $t->write_file('openssl.conf', <<EOF);
118 [ req ]
119 default_bits = 2048
120 encrypt_key = no
121 distinguished_name = req_distinguished_name
122 [ req_distinguished_name ]
123 EOF
124
125 my $d = $t->testdir();
126
127 foreach my $name ('localhost') {
128 system('openssl req -x509 -new '
129 . "-config $d/openssl.conf -subj /CN=$name/ "
130 . "-out $d/$name.crt -keyout $d/$name.key "
131 . ">>$d/openssl.out 2>&1") == 0
132 or die "Can't create certificate for $name: $!\n";
133 }
134
135 $t->run_daemon(\&http_daemon);
136 $t->run()->waitforsocket('127.0.0.1:' . port(8083));
137
138 $t->write_file('t2.html', 'SEE-THIS');
139
140 ###############################################################################
141
142 my ($s, $sid, $frames, $frame);
143
144 # 4.5.2. Indexed Field Line
145
146 $s = Test::Nginx::HTTP3->new();
147 $sid = $s->new_stream({ headers => [
148 { name => ':method', value => 'GET', mode => 0 },
149 { name => ':scheme', value => 'http', mode => 0 },
150 { name => ':path', value => '/', mode => 0 },
151 { name => ':authority', value => 'localhost', mode => 4 }]});
152 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
153
154 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
155 is($frame->{headers}->{'x-path'}, '/', 'indexed');
156
157 $s->insert_literal(':path', '/foo');
158 $sid = $s->new_stream({ headers => [
159 { name => ':method', value => 'GET', mode => 0 },
160 { name => ':scheme', value => 'http', mode => 0 },
161 { name => ':path', value => '/foo', mode => 0, dyn => 1 },
162 { name => ':authority', value => 'localhost', mode => 4 }]});
163 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
164
165 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
166 is($frame->{headers}->{'x-path'}, '/foo', 'indexed dynamic');
167
168 $s->insert_literal(':path', '/bar', huff => 1);
169 $sid = $s->new_stream({ headers => [
170 { name => ':method', value => 'GET', mode => 0 },
171 { name => ':scheme', value => 'http', mode => 0 },
172 { name => ':path', value => '/bar', mode => 0, dyn => 1 },
173 { name => ':authority', value => 'localhost', mode => 4 }]});
174 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
175
176 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
177 is($frame->{headers}->{'x-path'}, '/bar', 'indexed dynamic huffman');
178
179 $sid = $s->new_stream({ headers => [
180 { name => ':method', value => 'GET', mode => 0 },
181 { name => ':scheme', value => 'http', mode => 0 },
182 { name => ':path', value => '/foo', mode => 0, dyn => 1 },
183 { name => ':authority', value => 'localhost', mode => 4 }]});
184 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
185
186 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
187 is($frame->{headers}->{'x-path'}, '/foo', 'indexed dynamic previous');
188
189 $s->insert_reference(':path', '/qux');
190 $sid = $s->new_stream({ headers => [
191 { name => ':method', value => 'GET', mode => 0 },
192 { name => ':scheme', value => 'http', mode => 0 },
193 { name => ':path', value => '/qux', mode => 0, dyn => 1 },
194 { name => ':authority', value => 'localhost', mode => 4 }]});
195 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
196
197 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
198 is($frame->{headers}->{'x-path'}, '/qux', 'indexed reference');
199
200 $s->insert_reference(':path', '/corge', dyn => 1);
201 $sid = $s->new_stream({ headers => [
202 { name => ':method', value => 'GET', mode => 0 },
203 { name => ':scheme', value => 'http', mode => 0 },
204 { name => ':path', value => '/corge', mode => 0, dyn => 1 },
205 { name => ':authority', value => 'localhost', mode => 4 }]});
206 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
207
208 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
209 is($frame->{headers}->{'x-path'}, '/corge', 'indexed reference dynamic');
210
211 $s->insert_reference(':path', '/grault', huff => 1);
212 $sid = $s->new_stream({ headers => [
213 { name => ':method', value => 'GET', mode => 0 },
214 { name => ':scheme', value => 'http', mode => 0 },
215 { name => ':path', value => '/grault', mode => 0, dyn => 1 },
216 { name => ':authority', value => 'localhost', mode => 4 }]});
217 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
218
219 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
220 is($frame->{headers}->{'x-path'}, '/grault', 'indexed reference huffman');
221
222 # 4.5.3. Indexed Field Line with Post-Base Index
223
224 $s = Test::Nginx::HTTP3->new();
225 $s->insert_literal(':path', '/foo');
226 $sid = $s->new_stream({ base => -1, headers => [
227 { name => ':method', value => 'GET', mode => 0 },
228 { name => ':scheme', value => 'http', mode => 0 },
229 { name => ':path', value => '/foo', mode => 1 },
230 { name => ':authority', value => 'localhost', mode => 4 }]});
231 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
232
233 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
234 is($frame->{headers}->{'x-path'}, '/foo', 'post-base index');
235
236 # 4.5.4. Literal Field Line with Name Reference
237
238 $s = Test::Nginx::HTTP3->new();
239 $sid = $s->new_stream({ headers => [
240 { name => ':method', value => 'GET', mode => 2 },
241 { name => ':scheme', value => 'http', mode => 2 },
242 { name => ':path', value => '/', mode => 2 },
243 { name => ':authority', value => 'localhost', mode => 2 }]});
244 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
245
246 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
247 is($frame->{headers}->{':status'}, 200, 'reference');
248
249 $s = Test::Nginx::HTTP3->new();
250 $sid = $s->new_stream({ headers => [
251 { name => ':method', value => 'GET', mode => 2, huff => 1 },
252 { name => ':scheme', value => 'http', mode => 2, huff => 1 },
253 { name => ':path', value => '/', mode => 2, huff => 1 },
254 { name => ':authority', value => 'localhost', mode => 2, huff => 1 }]});
255 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
256
257 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
258 is($frame->{headers}->{':status'}, 200, 'reference huffman');
259
260 $s = Test::Nginx::HTTP3->new();
261 $sid = $s->new_stream({ headers => [
262 { name => ':method', value => 'GET', mode => 2, ni => 1 },
263 { name => ':scheme', value => 'http', mode => 2, ni => 1 },
264 { name => ':path', value => '/', mode => 2, ni => 1 },
265 { name => ':authority', value => 'localhost', mode => 2, ni => 1 }]});
266 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
267
268 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
269 is($frame->{headers}->{':status'}, 200, 'reference never indexed');
270
271 $s->insert_literal('x-foo', 'X-Bar');
272 $sid = $s->new_stream({ headers => [
273 { name => ':method', value => 'GET', mode => 2 },
274 { name => ':scheme', value => 'http', mode => 2 },
275 { name => ':path', value => '/', mode => 2 },
276 { name => ':authority', value => 'localhost', mode => 2 },
277 { name => 'x-foo', value => 'X-Baz', mode => 2, dyn => 1 }]});
278 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
279
280 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
281 is($frame->{headers}->{'x-sent-foo'}, 'X-Baz', 'reference dynamic');
282
283 # 4.5.5. Literal Field Line with Post-Base Name Reference
284
285 $s = Test::Nginx::HTTP3->new();
286 $s->insert_literal('x-foo', 'X-Bar');
287 $sid = $s->new_stream({ base => -1, headers => [
288 { name => ':method', value => 'GET', mode => 0 },
289 { name => ':scheme', value => 'http', mode => 0 },
290 { name => ':path', value => '/', mode => 0 },
291 { name => ':authority', value => 'localhost', mode => 4 },
292 { name => 'x-foo', value => 'X-Baz', mode => 3 }]});
293 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
294
295 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
296 is($frame->{headers}->{'x-sent-foo'}, 'X-Baz', 'base-base ref');
297
298 $sid = $s->new_stream({ base => -1, headers => [
299 { name => ':method', value => 'GET', mode => 0 },
300 { name => ':scheme', value => 'http', mode => 0 },
301 { name => ':path', value => '/', mode => 0 },
302 { name => ':authority', value => 'localhost', mode => 4 },
303 { name => 'x-foo', value => 'X-Baz', mode => 3, huff => 1 }]});
304 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
305
306 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
307 is($frame->{headers}->{'x-sent-foo'}, 'X-Baz', 'post-base ref huffman');
308
309 $sid = $s->new_stream({ base => -1, headers => [
310 { name => ':method', value => 'GET', mode => 0 },
311 { name => ':scheme', value => 'http', mode => 0 },
312 { name => ':path', value => '/', mode => 0 },
313 { name => ':authority', value => 'localhost', mode => 4 },
314 { name => 'x-foo', value => 'X-Baz', mode => 3, ni => 1 }]});
315 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
316
317 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
318 is($frame->{headers}->{'x-sent-foo'}, 'X-Baz', 'post-base ref never indexed');
319
320 # 4.5.6. Literal Field Line with Literal Name
321
322 $s = Test::Nginx::HTTP3->new();
323 $sid = $s->new_stream({ headers => [
324 { name => ':method', value => 'GET', mode => 4 },
325 { name => ':scheme', value => 'http', mode => 4 },
326 { name => ':path', value => '/', mode => 4 },
327 { name => ':authority', value => 'localhost', mode => 4 }]});
328 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
329
330 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
331 is($frame->{headers}->{':status'}, 200, 'literal');
332
333 $s = Test::Nginx::HTTP3->new();
334 $sid = $s->new_stream({ headers => [
335 { name => ':method', value => 'GET', mode => 4, huff => 1 },
336 { name => ':scheme', value => 'http', mode => 4, huff => 1 },
337 { name => ':path', value => '/', mode => 4, huff => 1 },
338 { name => ':authority', value => 'localhost', mode => 4, huff => 1 }]});
339 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
340
341 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
342 is($frame->{headers}->{':status'}, 200, 'literal huffman');
343
344 $s = Test::Nginx::HTTP3->new();
345 $sid = $s->new_stream({ headers => [
346 { name => ':method', value => 'GET', mode => 4, ni => 1 },
347 { name => ':scheme', value => 'http', mode => 4, ni => 1 },
348 { name => ':path', value => '/', mode => 4, ni => 1 },
349 { name => ':authority', value => 'localhost', mode => 4, ni => 1 }]});
350 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
351
352 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
353 is($frame->{headers}->{':status'}, 200, 'literal never indexed');
354
355 # response header field with characters not suitable for huffman encoding
356
357 $s = Test::Nginx::HTTP3->new();
358 $sid = $s->new_stream({ headers => [
359 { name => ':method', value => 'GET', mode => 0 },
360 { name => ':scheme', value => 'http', mode => 0 },
361 { name => ':path', value => '/', mode => 0 },
362 { name => ':authority', value => 'localhost', mode => 2 },
363 { name => 'x-foo', value => '{{{{{', mode => 4 }]});
364 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
365
366 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
367 is($frame->{headers}->{'x-sent-foo'}, '{{{{{', 'rare chars');
368 like($s->{headers}, qr/\Q{{{{{/, 'rare chars - no huffman encoding');
369
370 # response header field with huffman encoding
371 # NB: implementation detail, not obligated
372
373 $s = Test::Nginx::HTTP3->new();
374 $sid = $s->new_stream({ headers => [
375 { name => ':method', value => 'GET', mode => 0 },
376 { name => ':scheme', value => 'http', mode => 0 },
377 { name => ':path', value => '/', mode => 0 },
378 { name => ':authority', value => 'localhost', mode => 2 },
379 { name => 'x-foo', value => 'aaaaa', mode => 4 }]});
380 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
381
382 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
383 is($frame->{headers}->{'x-sent-foo'}, 'aaaaa', 'well known chars');
384 unlike($s->{headers}, qr/aaaaa/, 'well known chars - huffman encoding');
385
386 # response header field with huffman encoding - complete table mod \0, CR, LF
387 # first saturate with short-encoded characters (NB: implementation detail)
388
389 my $field = pack "C*", ((map { 97 } (1 .. 862)), 1 .. 9, 11, 12, 14 .. 255);
390
391 $s = Test::Nginx::HTTP3->new();
392 $sid = $s->new_stream({ headers => [
393 { name => ':method', value => 'GET', mode => 0 },
394 { name => ':scheme', value => 'http', mode => 0 },
395 { name => ':path', value => '/', mode => 0 },
396 { name => ':authority', value => 'localhost', mode => 2 },
397 { name => 'x-foo', value => $field, mode => 4 }]});
398 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
399
400 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
401 is($frame->{headers}->{'x-sent-foo'}, $field, 'all chars');
402 unlike($s->{headers}, qr/abcde/, 'all chars - huffman encoding');
403
404 # 3.2.2. Dynamic Table Capacity and Eviction
405
406 # remove some indexed headers from the dynamic table
407 # by maintaining dynamic table space only for index 0
408
409 $s = Test::Nginx::HTTP3->new(undef, capacity => 64);
410 $s->insert_literal('x-foo', 'X-Bar');
411 $sid = $s->new_stream({ headers => [
412 { name => ':method', value => 'GET', mode => 0 },
413 { name => ':scheme', value => 'http', mode => 0 },
414 { name => ':path', value => '/', mode => 0 },
415 { name => ':authority', value => 'localhost', mode => 2 },
416 { name => 'x-foo', value => 'X-Bar', mode => 0, dyn => 1 }]});
417 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
418
419 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
420 is($frame->{headers}->{'x-sent-foo'}, 'X-Bar', 'capacity insert');
421
422 $s->insert_literal('x-foo', 'X-Baz');
423 $sid = $s->new_stream({ headers => [
424 { name => ':method', value => 'GET', mode => 0 },
425 { name => ':scheme', value => 'http', mode => 0 },
426 { name => ':path', value => '/', mode => 0 },
427 { name => ':authority', value => 'localhost', mode => 2 },
428 { name => 'x-foo', value => 'X-Baz', mode => 0, dyn => 1 }]});
429 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
430
431 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
432 is($frame->{headers}->{'x-sent-foo'}, 'X-Baz', 'capacity replace');
433
434 $sid = $s->new_stream({ headers => [
435 { name => ':method', value => 'GET', mode => 0 },
436 { name => ':scheme', value => 'http', mode => 0 },
437 { name => ':path', value => '/', mode => 0 },
438 { name => ':authority', value => 'localhost', mode => 2 },
439 { name => 'x-foo', value => 'X-Bar', mode => 0, dyn => 1 }]});
440 $frames = $s->read(all => [{ type => 'DECODER_C' }]);
441
442 ($frame) = grep { $_->{type} eq "DECODER_C" } @$frames;
443 is($frame->{'val'}, $sid, 'capacity eviction');
444
445 # insert with referenced entry eviction
446
447 $s = Test::Nginx::HTTP3->new(undef, capacity => 64);
448 $s->insert_literal('x-foo', 'X-Bar');
449 $s->insert_reference('x-foo', 'X-Baz', dyn => 1);
450 $sid = $s->new_stream({ headers => [
451 { name => ':method', value => 'GET', mode => 0 },
452 { name => ':scheme', value => 'http', mode => 0 },
453 { name => ':path', value => '/', mode => 0 },
454 { name => ':authority', value => 'localhost', mode => 2 },
455 { name => 'x-foo', value => 'X-Baz', mode => 0, dyn => 1 }]});
456 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
457
458 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
459 is($frame->{headers}->{'x-sent-foo'}, 'X-Baz', 'insert ref eviction');
460
461 $s = Test::Nginx::HTTP3->new(undef, capacity => 64);
462 $s->insert_literal('x-foo', 'X-Bar');
463 $s->duplicate('x-foo');
464 $sid = $s->new_stream({ headers => [
465 { name => ':method', value => 'GET', mode => 0 },
466 { name => ':scheme', value => 'http', mode => 0 },
467 { name => ':path', value => '/', mode => 0 },
468 { name => ':authority', value => 'localhost', mode => 2 },
469 { name => 'x-foo', value => 'X-Bar', mode => 0, dyn => 1 }]});
470 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
471
472 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
473 is($frame->{headers}->{'x-sent-foo'}, 'X-Bar', 'duplicate eviction');
474
475 # invalid capacity
476
477 $s = Test::Nginx::HTTP3->new(undef, capacity => 4097);
478 $frames = $s->read(all => [{ type => 'CONNECTION_CLOSE' }]);
479
480 ($frame) = grep { $_->{type} eq "CONNECTION_CLOSE" } @$frames;
481 is($frame->{'phrase'}, 'stream error', 'capacity invalid');
482
483 # request header field with multiple values
484
485 # 4.2.1. Field Compression
486 # To allow for better compression efficiency, the Cookie header field
487 # MAY be split into separate field lines <..>.
488
489 $s = Test::Nginx::HTTP3->new();
490 $sid = $s->new_stream({ headers => [
491 { name => ':method', value => 'GET', mode => 0 },
492 { name => ':scheme', value => 'http', mode => 0 },
493 { name => ':path', value => '/cookie', mode => 2 },
494 { name => ':authority', value => 'localhost', mode => 2 },
495 { name => 'cookie', value => 'a=b', mode => 2 },
496 { name => 'cookie', value => 'c=d', mode => 2 }]});
497 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
498
499 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
500 is($frame->{headers}->{'x-cookie-a'}, 'b',
501 'multiple request header fields - cookie');
502 is($frame->{headers}->{'x-cookie-c'}, 'd',
503 'multiple request header fields - cookie 2');
504 is($frame->{headers}->{'x-cookie'}, 'a=b; c=d',
505 'multiple request header fields - semi-colon');
506
507 # request header field with multiple values to HTTP backend
508
509 # 4.2.1. Field Compression
510 # these MUST be concatenated into a single byte string
511 # using the two-byte delimiter of "; " (ASCII 0x3b, 0x20)
512 # before being passed into a context other than HTTP/2 or
513 # HTTP/3, such as an HTTP/1.1 connection <..>
514
515 $s = Test::Nginx::HTTP3->new();
516 $sid = $s->new_stream({ headers => [
517 { name => ':method', value => 'GET', mode => 0 },
518 { name => ':scheme', value => 'http', mode => 0 },
519 { name => ':path', value => '/proxy/cookie', mode => 2 },
520 { name => ':authority', value => 'localhost', mode => 2 },
521 { name => 'cookie', value => 'a=b', mode => 2 },
522 { name => 'cookie', value => 'c=d', mode => 2 }]});
523 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
524
525 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
526 is($frame->{headers}->{'x-sent-cookie'}, 'a=b; c=d',
527 'multiple request header fields proxied - semi-colon');
528 is($frame->{headers}->{'x-sent-cookie2'}, '',
529 'multiple request header fields proxied - dublicate cookie');
530 is($frame->{headers}->{'x-sent-cookie-a'}, 'b',
531 'multiple request header fields proxied - cookie 1');
532 is($frame->{headers}->{'x-sent-cookie-c'}, 'd',
533 'multiple request header fields proxied - cookie 2');
534
535 # response header field with multiple values
536
537 $s = Test::Nginx::HTTP3->new();
538 $sid = $s->new_stream({ path => '/set-cookie' });
539 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
540
541 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
542 is($frame->{headers}->{'set-cookie'}[0], 'a=b',
543 'multiple response header fields - cookie');
544 is($frame->{headers}->{'set-cookie'}[1], 'c=d',
545 'multiple response header fields - cookie 2');
546
547 # response header field with multiple values from HTTP backend
548
549 $s = Test::Nginx::HTTP3->new();
550 $sid = $s->new_stream({ path => '/proxy/set-cookie' });
551 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
552
553 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
554 is($frame->{headers}->{'set-cookie'}[0], 'a=b',
555 'multiple response header proxied - cookie');
556 is($frame->{headers}->{'set-cookie'}[1], 'c=d',
557 'multiple response header proxied - cookie 2');
558 is($frame->{headers}->{'x-uc-a'}, 'b',
559 'multiple response header proxied - upstream cookie');
560 is($frame->{headers}->{'x-uc-c'}, 'd',
561 'multiple response header proxied - upstream cookie 2');
562
563 # max_field_size - header field name
564
565 $s = Test::Nginx::HTTP3->new(8984, capacity => 2048);
566 $s->insert_literal('x' x 511, 'value');
567 $sid = $s->new_stream({ headers => [
568 { name => ':method', value => 'GET', mode => 0 },
569 { name => ':scheme', value => 'http', mode => 0 },
570 { name => ':path', value => '/t2.html', mode => 2 },
571 { name => ':authority', value => 'localhost', mode => 2 },
572 { name => 'x' x 511, value => 'value', mode => 0, dyn => 1 }]});
573 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
574
575 ($frame) = grep { $_->{type} eq "DATA" } @$frames;
576 ok($frame, 'field name size less');
577
578 $s = Test::Nginx::HTTP3->new(8984, capacity => 2048);
579 $s->insert_literal('x' x 512, 'value');
580 $sid = $s->new_stream({ headers => [
581 { name => ':method', value => 'GET', mode => 0 },
582 { name => ':scheme', value => 'http', mode => 0 },
583 { name => ':path', value => '/t2.html', mode => 2 },
584 { name => ':authority', value => 'localhost', mode => 2 },
585 { name => 'x' x 512, value => 'value', mode => 0, dyn => 1 }]});
586 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
587
588 ($frame) = grep { $_->{type} eq "DATA" } @$frames;
589 ok($frame, 'field name size equal');
590
591 $s = Test::Nginx::HTTP3->new(8984, capacity => 2048);
592 $s->insert_literal('x' x 513, 'value');
593 $sid = $s->new_stream({ headers => [
594 { name => ':method', value => 'GET', mode => 0 },
595 { name => ':scheme', value => 'http', mode => 0 },
596 { name => ':path', value => '/t2.html', mode => 2 },
597 { name => ':authority', value => 'localhost', mode => 2 },
598 { name => 'x' x 513, value => 'value', mode => 0, dyn => 1 }]});
599 $frames = $s->read(all => [{ type => 'CONNECTION_CLOSE' }]);
600
601 ($frame) = grep { $_->{type} eq "CONNECTION_CLOSE" } @$frames;
602 is($frame->{'phrase'}, 'stream error', 'field name size greater');
603
604 # max_field_size - header field value
605
606 $s = Test::Nginx::HTTP3->new(8984, capacity => 2048);
607 $s->insert_literal('name', 'x' x 511);
608 $sid = $s->new_stream({ headers => [
609 { name => ':method', value => 'GET', mode => 0 },
610 { name => ':scheme', value => 'http', mode => 0 },
611 { name => ':path', value => '/t2.html', mode => 2 },
612 { name => ':authority', value => 'localhost', mode => 2 },
613 { name => 'name', value => 'x' x 511, mode => 0, dyn => 1 }]});
614 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
615
616 ($frame) = grep { $_->{type} eq "DATA" } @$frames;
617 ok($frame, 'field value size less');
618
619 $s = Test::Nginx::HTTP3->new(8984, capacity => 2048);
620 $s->insert_literal('name', 'x' x 512);
621 $sid = $s->new_stream({ headers => [
622 { name => ':method', value => 'GET', mode => 0 },
623 { name => ':scheme', value => 'http', mode => 0 },
624 { name => ':path', value => '/t2.html', mode => 2 },
625 { name => ':authority', value => 'localhost', mode => 2 },
626 { name => 'name', value => 'x' x 512, mode => 0, dyn => 1 }]});
627 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
628
629 ($frame) = grep { $_->{type} eq "DATA" } @$frames;
630 ok($frame, 'field value size equal');
631
632 $s = Test::Nginx::HTTP3->new(8984, capacity => 2048);
633 $s->insert_literal('name', 'x' x 513);
634 $frames = $s->read(all => [{ type => 'CONNECTION_CLOSE' }]);
635
636 ($frame) = grep { $_->{type} eq "CONNECTION_CLOSE" } @$frames;
637 is($frame->{'phrase'}, 'stream error', 'field value size greater');
638
639 # max_header_size
640
641 $s = Test::Nginx::HTTP3->new(8985, capacity => 2048);
642 $s->insert_literal('longname', 'x' x 450);
643 $sid = $s->new_stream({ headers => [
644 { name => ':method', value => 'GET', mode => 0 },
645 { name => ':scheme', value => 'http', mode => 0 },
646 { name => ':path', value => '/t2.html', mode => 2 },
647 { name => ':authority', value => 'localhost', mode => 2 },
648 { name => 'longname', value => 'x' x 450, mode => 0, dyn => 1 }]});
649 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
650
651 ($frame) = grep { $_->{type} eq "DATA" } @$frames;
652 ok($frame, 'header size less');
653
654 $s = Test::Nginx::HTTP3->new(8985, capacity => 2048);
655 $s->insert_literal('longname', 'x' x 451);
656 $sid = $s->new_stream({ headers => [
657 { name => ':method', value => 'GET', mode => 0 },
658 { name => ':scheme', value => 'http', mode => 0 },
659 { name => ':path', value => '/t2.html', mode => 2 },
660 { name => ':authority', value => 'localhost', mode => 2 },
661 { name => 'longname', value => 'x' x 451, mode => 0, dyn => 1 }]});
662 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
663
664 ($frame) = grep { $_->{type} eq "DATA" } @$frames;
665 ok($frame, 'header size equal');
666
667 $s = Test::Nginx::HTTP3->new(8985, capacity => 2048);
668 $s->insert_literal('longname', 'x' x 452);
669 $sid = $s->new_stream({ headers => [
670 { name => ':method', value => 'GET', mode => 0 },
671 { name => ':scheme', value => 'http', mode => 0 },
672 { name => ':path', value => '/t2.html', mode => 2 },
673 { name => ':authority', value => 'localhost', mode => 2 },
674 { name => 'longname', value => 'x' x 452, mode => 0, dyn => 1 }]});
675 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
676
677 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
678 is($frame->{headers}->{':status'}, 400, 'header size greater');
679
680 # header size is based on (decompressed) header list
681 # two extra 1-byte indices would otherwise fit in max_header_size
682
683 $s = Test::Nginx::HTTP3->new(8985, capacity => 2048);
684 $s->insert_literal('longname', 'x' x 400);
685 $sid = $s->new_stream({ headers => [
686 { name => ':method', value => 'GET', mode => 0 },
687 { name => ':scheme', value => 'http', mode => 0 },
688 { name => ':path', value => '/t2.html', mode => 2 },
689 { name => ':authority', value => 'localhost', mode => 2 },
690 { name => 'longname', value => 'x' x 400, mode => 0, dyn => 1 }]});
691 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
692
693 ($frame) = grep { $_->{type} eq "DATA" } @$frames;
694 ok($frame, 'header size indexed');
695
696 $sid = $s->new_stream({ headers => [
697 { name => ':method', value => 'GET', mode => 0 },
698 { name => ':scheme', value => 'http', mode => 0 },
699 { name => ':path', value => '/t2.html', mode => 2 },
700 { name => ':authority', value => 'localhost', mode => 2 },
701 { name => 'longname', value => 'x' x 400, mode => 0, dyn => 1 },
702 { name => 'longname', value => 'x' x 400, mode => 0, dyn => 1 }]});
703 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
704
705 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
706 is($frame->{headers}->{':status'}, 400, 'header size indexed greater');
707
708 # ensure that request header field value with newline doesn't get split
709 #
710 # 10.3. Intermediary-Encapsulation Attacks
711 # Requests or responses containing invalid field names MUST be treated
712 # as malformed.
713
714 $s = Test::Nginx::HTTP3->new();
715 $sid = $s->new_stream({ headers => [
716 { name => ':method', value => 'GET', mode => 0 },
717 { name => ':scheme', value => 'http', mode => 0 },
718 { name => ':path', value => '/proxy2/', mode => 2 },
719 { name => ':authority', value => 'localhost', mode => 2 },
720 { name => 'x-foo', value => "x-bar\r\nreferer:see-this", mode => 4 }]});
721 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
722
723 # 10.3. Intermediary Encapsulation Attacks
724 # Therefore, an intermediary cannot translate an HTTP/3 request or response
725 # containing an invalid field name into an HTTP/1.1 message.
726
727 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
728 isnt($frame->{headers}->{'x-referer'}, 'see-this', 'newline in request header');
729
730 is($frame->{headers}->{':status'}, 400, 'newline in request header - bad request');
731
732 # invalid header name as seen with underscore should not lead to ignoring rest
733
734 $s = Test::Nginx::HTTP3->new();
735 $sid = $s->new_stream({ headers => [
736 { name => ':method', value => 'GET', mode => 0 },
737 { name => ':scheme', value => 'http', mode => 0 },
738 { name => ':path', value => '/', mode => 0 },
739 { name => ':authority', value => 'localhost', mode => 2 },
740 { name => 'x_foo', value => "x-bar", mode => 4 },
741 { name => 'referer', value => "see-this", mode => 2 }]});
742 $frames = $s->read(all => [{ type => 'HEADERS' }]);
743
744 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
745 is($frame->{headers}->{'x-referer'}, 'see-this', 'after invalid header name');
746
747 # other invalid header name characters as seen with ':'
748
749 $s = Test::Nginx::HTTP3->new();
750 $sid = $s->new_stream({ headers => [
751 { name => ':method', value => 'GET', mode => 0 },
752 { name => ':scheme', value => 'http', mode => 0 },
753 { name => ':path', value => '/', mode => 0 },
754 { name => ':authority', value => 'localhost', mode => 2 },
755 { name => 'x:foo', value => "x-bar", mode => 4 },
756 { name => 'referer', value => "see-this", mode => 2 }]});
757 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
758
759 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
760 is($frame->{headers}->{':status'}, 400, 'colon in header name');
761
762 $s = Test::Nginx::HTTP3->new();
763 $sid = $s->new_stream({ headers => [
764 { name => ':method', value => 'GET', mode => 0 },
765 { name => ':scheme', value => 'http', mode => 0 },
766 { name => ':path', value => '/', mode => 0 },
767 { name => ':authority', value => 'localhost', mode => 2 },
768 { name => 'x foo', value => "bar", mode => 4 }]});
769 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
770
771 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
772 is($frame->{headers}->{':status'}, 400, 'space in header name');
773
774 $s = Test::Nginx::HTTP3->new();
775 $sid = $s->new_stream({ headers => [
776 { name => ':method', value => 'GET', mode => 0 },
777 { name => ':scheme', value => 'http', mode => 0 },
778 { name => ':path', value => '/', mode => 0 },
779 { name => ':authority', value => 'localhost', mode => 2 },
780 { name => "foo\x02", value => "bar", mode => 4 }]});
781 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
782
783 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
784 is($frame->{headers}->{':status'}, 400, 'control in header name');
785
786 # header name with underscore - underscores_in_headers on
787
788 $s = Test::Nginx::HTTP3->new(8986);
789 $sid = $s->new_stream({ headers => [
790 { name => ':method', value => 'GET', mode => 0 },
791 { name => ':scheme', value => 'http', mode => 0 },
792 { name => ':path', value => '/', mode => 0 },
793 { name => ':authority', value => 'localhost', mode => 2 },
794 { name => 'x_foo', value => "x-bar", mode => 4 },
795 { name => 'referer', value => "see-this", mode => 2 }]});
796 $frames = $s->read(all => [{ type => 'HEADERS' }]);
797
798 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
799 is($frame->{headers}->{'x-sent-foo'}, 'x-bar',
800 'underscore in header name - underscores_in_headers');
801
802 # header name with underscore - ignore_invalid_headers off
803
804 $s = Test::Nginx::HTTP3->new(8987);
805 $sid = $s->new_stream({ headers => [
806 { name => ':method', value => 'GET', mode => 0 },
807 { name => ':scheme', value => 'http', mode => 0 },
808 { name => ':path', value => '/', mode => 0 },
809 { name => ':authority', value => 'localhost', mode => 2 },
810 { name => 'x_foo', value => "x-bar", mode => 4 },
811 { name => 'referer', value => "see-this", mode => 2 }]});
812 $frames = $s->read(all => [{ type => 'HEADERS' }]);
813
814 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
815 is($frame->{headers}->{'x-sent-foo'}, 'x-bar',
816 'underscore in header name - ignore_invalid_headers');
817
818 # missing mandatory request header ':scheme'
819
820 $s = Test::Nginx::HTTP3->new();
821 $sid = $s->new_stream({ headers => [
822 { name => ':method', value => 'GET', mode => 0 },
823 { name => ':path', value => '/', mode => 0 },
824 { name => ':authority', value => 'localhost', mode => 2 }]});
825 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
826
827 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
828 is($frame->{headers}->{':status'}, 400, 'incomplete headers');
829
830 # empty request header ':authority'
831
832 $s = Test::Nginx::HTTP3->new();
833 $sid = $s->new_stream({ headers => [
834 { name => ':method', value => 'GET', mode => 0 },
835 { name => ':scheme', value => 'http', mode => 0 },
836 { name => ':path', value => '/', mode => 0 },
837 { name => ':authority', value => '', mode => 0 }]});
838 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
839
840 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
841 is($frame->{headers}->{':status'}, 400, 'empty authority');
842
843 # client sent invalid :path header
844
845 $sid = $s->new_stream({ path => 't2.html' });
846 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
847
848 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
849 is($frame->{headers}->{':status'}, 400, 'invalid path');
850
851 $sid = $s->new_stream({ path => "/t2.html\x02" });
852 $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
853
854 ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
855 is($frame->{headers}->{':status'}, 400, 'invalid path control');
856
857 ###############################################################################
858
859 sub http_daemon {
860 my $server = IO::Socket::INET->new(
861 Proto => 'tcp',
862 LocalHost => '127.0.0.1',
863 LocalPort => port(8083),
864 Listen => 5,
865 Reuse => 1
866 )
867 or die "Can't create listening socket: $!\n";
868
869 local $SIG{PIPE} = 'IGNORE';
870
871 while (my $client = $server->accept()) {
872 $client->autoflush(1);
873
874 my $headers = '';
875 my $uri = '';
876
877 while (<$client>) {
878 $headers .= $_;
879 last if (/^\x0d?\x0a?$/);
880 }
881
882 next if $headers eq '';
883 $uri = $1 if $headers =~ /^\S+\s+([^ ]+)\s+HTTP/i;
884
885 if ($uri eq '/cookie') {
886
887 my ($cookie, $cookie2) = $headers =~ /Cookie: (.+)/ig;
888 $cookie2 = '' unless defined $cookie2;
889
890 my ($cookie_a, $cookie_c) = ('', '');
891 $cookie_a = $1 if $headers =~ /X-Cookie-a: (.+)/i;
892 $cookie_c = $1 if $headers =~ /X-Cookie-c: (.+)/i;
893
894 print $client <<EOF;
895 HTTP/1.1 200 OK
896 Connection: close
897 X-Sent-Cookie: $cookie
898 X-Sent-Cookie2: $cookie2
899 X-Sent-Cookie-a: $cookie_a
900 X-Sent-Cookie-c: $cookie_c
901
902 EOF
903
904 } elsif ($uri eq '/set-cookie') {
905
906 print $client <<EOF;
907 HTTP/1.1 200 OK
908 Connection: close
909 Set-Cookie: a=b
910 Set-Cookie: c=d
911
912 EOF
913
914 }
915 }
916 }
917
918 ###############################################################################