Mercurial > hg > nginx-tests
view h2_headers.t @ 1209:0d9df274e3a3
Tests: ssl_client_escaped_cert variable tests.
author | Sergey Kandaurov <pluknet@nginx.com> |
---|---|
date | Tue, 22 Aug 2017 17:14:43 +0300 |
parents | 366d789f3cbf |
children | 9b737ad4260b |
line wrap: on
line source
#!/usr/bin/perl # (C) Sergey Kandaurov # (C) Nginx, Inc. # Tests for HTTP/2 protocol with headers. # various HEADERS compression/encoding, see hpack() for mode details. ############################################################################### use warnings; use strict; use Test::More; use Config; 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/)->plan(93) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% daemon off; events { } http { %%TEST_GLOBALS_HTTP%% server { listen 127.0.0.1:8080 http2; listen 127.0.0.1:8081; listen 127.0.0.1:8082 http2 sndbuf=128; server_name localhost; http2_max_field_size 128k; http2_max_header_size 128k; location / { add_header X-Sent-Foo $http_x_foo; add_header X-Referer $http_referer; return 200; } location /frame_size { add_header X-LongHeader $arg_h; add_header X-LongHeader $arg_h; add_header X-LongHeader $arg_h; alias %%TESTDIR%%/t2.html; } location /continuation { add_header X-LongHeader $arg_h; add_header X-LongHeader $arg_h; add_header X-LongHeader $arg_h; return 200 body; location /continuation/204 { return 204; } } location /proxy/ { add_header X-UC-a $upstream_cookie_a; add_header X-UC-c $upstream_cookie_c; proxy_pass http://127.0.0.1:8083/; proxy_set_header X-Cookie-a $cookie_a; proxy_set_header X-Cookie-c $cookie_c; } location /proxy2/ { proxy_pass http://127.0.0.1:8081/; } location /set-cookie { add_header Set-Cookie a=b; add_header Set-Cookie c=d; return 200; } location /cookie { add_header X-Cookie $http_cookie; add_header X-Cookie-a $cookie_a; add_header X-Cookie-c $cookie_c; return 200; } } server { listen 127.0.0.1:8084 http2; server_name localhost; http2_max_field_size 22; } server { listen 127.0.0.1:8085 http2; server_name localhost; http2_max_header_size 64; } } EOF $t->run_daemon(\&http_daemon); $t->run()->waitforsocket('127.0.0.1:' . port(8083)); # file size is slightly beyond initial window size: 2**16 + 80 bytes $t->write_file('t1.html', join('', map { sprintf "X%04dXXX", $_ } (1 .. 8202))); $t->write_file('t2.html', 'SEE-THIS'); ############################################################################### # 6.1. Indexed Header Field Representation my $s = Test::Nginx::HTTP2->new(); my $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/', mode => 0 }, { name => ':authority', value => 'localhost', mode => 1 }]}); my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{':status'}, 200, 'indexed header field'); # 6.2.1. Literal Header Field with Incremental Indexing $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 1, huff => 0 }, { name => ':scheme', value => 'http', mode => 1, huff => 0 }, { name => ':path', value => '/', mode => 1, huff => 0 }, { name => ':authority', value => 'localhost', mode => 1, huff => 0 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{':status'}, 200, 'literal with indexing'); $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 1, huff => 1 }, { name => ':scheme', value => 'http', mode => 1, huff => 1 }, { name => ':path', value => '/', mode => 1, huff => 1 }, { name => ':authority', value => 'localhost', mode => 1, huff => 1 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{':status'}, 200, 'literal with indexing - huffman'); # 6.2.1. Literal Header Field with Incremental Indexing -- New Name $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 2, huff => 0 }, { name => ':scheme', value => 'http', mode => 2, huff => 0 }, { name => ':path', value => '/', mode => 2, huff => 0 }, { name => ':authority', value => 'localhost', mode => 2, huff => 0 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{':status'}, 200, 'literal with indexing - new'); $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 2, huff => 1 }, { name => ':scheme', value => 'http', mode => 2, huff => 1 }, { name => ':path', value => '/', mode => 2, huff => 1 }, { name => ':authority', value => 'localhost', mode => 2, huff => 1 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{':status'}, 200, 'literal with indexing - new huffman'); # 6.2.2. Literal Header Field without Indexing $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 3, huff => 0 }, { name => ':scheme', value => 'http', mode => 3, huff => 0 }, { name => ':path', value => '/', mode => 3, huff => 0 }, { name => ':authority', value => 'localhost', mode => 3, huff => 0 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{':status'}, 200, 'literal without indexing'); $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 3, huff => 1 }, { name => ':scheme', value => 'http', mode => 3, huff => 1 }, { name => ':path', value => '/', mode => 3, huff => 1 }, { name => ':authority', value => 'localhost', mode => 3, huff => 1 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{':status'}, 200, 'literal without indexing - huffman'); $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 3, huff => 0 }, { name => ':scheme', value => 'http', mode => 3, huff => 0 }, { name => ':path', value => '/', mode => 3, huff => 0 }, { name => ':authority', value => 'localhost', mode => 3, huff => 0 }, { name => 'referer', value => 'foo', mode => 3, huff => 0 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{':status'}, 200, 'literal without indexing - multibyte index'); is($frame->{headers}->{'x-referer'}, 'foo', 'literal without indexing - multibyte index value'); # 6.2.2. Literal Header Field without Indexing -- New Name $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 4, huff => 0 }, { name => ':scheme', value => 'http', mode => 4, huff => 0 }, { name => ':path', value => '/', mode => 4, huff => 0 }, { name => ':authority', value => 'localhost', mode => 4, huff => 0 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{':status'}, 200, 'literal without indexing - new'); $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 4, huff => 1 }, { name => ':scheme', value => 'http', mode => 4, huff => 1 }, { name => ':path', value => '/', mode => 4, huff => 1 }, { name => ':authority', value => 'localhost', mode => 4, huff => 1 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{':status'}, 200, 'literal without indexing - new huffman'); # 6.2.3. Literal Header Field Never Indexed $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 5, huff => 0 }, { name => ':scheme', value => 'http', mode => 5, huff => 0 }, { name => ':path', value => '/', mode => 5, huff => 0 }, { name => ':authority', value => 'localhost', mode => 5, huff => 0 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{':status'}, 200, 'literal never indexed'); $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 5, huff => 1 }, { name => ':scheme', value => 'http', mode => 5, huff => 1 }, { name => ':path', value => '/', mode => 5, huff => 1 }, { name => ':authority', value => 'localhost', mode => 5, huff => 1 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{':status'}, 200, 'literal never indexed - huffman'); $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 5, huff => 0 }, { name => ':scheme', value => 'http', mode => 5, huff => 0 }, { name => ':path', value => '/', mode => 5, huff => 0 }, { name => ':authority', value => 'localhost', mode => 5, huff => 0 }, { name => 'referer', value => 'foo', mode => 5, huff => 0 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{':status'}, 200, 'literal never indexed - multibyte index'); is($frame->{headers}->{'x-referer'}, 'foo', 'literal never indexed - multibyte index value'); # 6.2.3. Literal Header Field Never Indexed -- New Name $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 6, huff => 0 }, { name => ':scheme', value => 'http', mode => 6, huff => 0 }, { name => ':path', value => '/', mode => 6, huff => 0 }, { name => ':authority', value => 'localhost', mode => 6, huff => 0 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{':status'}, 200, 'literal never indexed - new'); $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 6, huff => 1 }, { name => ':scheme', value => 'http', mode => 6, huff => 1 }, { name => ':path', value => '/', mode => 6, huff => 1 }, { name => ':authority', value => 'localhost', mode => 6, huff => 1 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{':status'}, 200, 'literal never indexed - new huffman'); # reuse literal with multibyte indexing $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/', mode => 0 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'referer', value => 'foo', mode => 1 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{'x-referer'}, 'foo', 'value with indexing - new'); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/', mode => 0 }, { name => ':authority', value => 'localhost', mode => 0 }, { name => 'referer', value => 'foo', mode => 0 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{'x-referer'}, 'foo', 'value with indexing - indexed'); $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/', mode => 0 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'x-foo', value => 'X-Bar', mode => 2 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{'x-sent-foo'}, 'X-Bar', 'name with indexing - new'); # reuse literal with multibyte indexing - reused name $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/', mode => 0 }, { name => ':authority', value => 'localhost', mode => 0 }, { name => 'x-foo', value => 'X-Bar', mode => 0 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{'x-sent-foo'}, 'X-Bar', 'name with indexing - indexed'); # reuse literal with multibyte indexing - reused name only $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/', mode => 0 }, { name => ':authority', value => 'localhost', mode => 0 }, { name => 'x-foo', value => 'X-Baz', mode => 1 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{'x-sent-foo'}, 'X-Baz', 'name with indexing - indexed name'); # response header field with characters not suitable for huffman encoding $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/', mode => 0 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'x-foo', value => '{{{{{', mode => 2 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{'x-sent-foo'}, '{{{{{', 'rare chars'); like($s->{headers}, qr/\Q{{{{{/, 'rare chars - no huffman encoding'); # response header field with huffman encoding # NB: implementation detail, not obligated $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/', mode => 0 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'x-foo', value => 'aaaaa', mode => 2 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{'x-sent-foo'}, 'aaaaa', 'well known chars'); unlike($s->{headers}, qr/aaaaa/, 'well known chars - huffman encoding'); # response header field with huffman encoding - complete table mod \0, CR, LF # first saturate with short-encoded characters (NB: implementation detail) my $field = pack "C*", ((map { 97 } (1 .. 862)), 1 .. 9, 11, 12, 14 .. 255); $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/', mode => 0 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'x-foo', value => $field, mode => 2 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{'x-sent-foo'}, $field, 'all chars'); unlike($s->{headers}, qr/abcde/, 'all chars - huffman encoding'); # 6.3. Dynamic Table Size Update # remove some indexed headers from the dynamic table # by maintaining dynamic table space only for index 0 # 'x-foo' has index 0, and 'referer' has index 1 $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/', mode => 0 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'referer', value => 'foo', mode => 1 }, { name => 'x-foo', value => 'X-Bar', mode => 2 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); $sid = $s->new_stream({ table_size => 61, headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/', mode => 0 }, { name => 'x-foo', value => 'X-Bar', mode => 0 }, { name => ':authority', value => 'localhost', mode => 1 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; isnt($frame, undef, 'updated table size - remaining index'); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/', mode => 0 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'referer', value => 'foo', mode => 0 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame, undef, 'invalid index'); # 5.4.1. Connection Error Handling # An endpoint that encounters a connection error SHOULD first send a # GOAWAY frame <..> ($frame) = grep { $_->{type} eq "GOAWAY" } @$frames; ok($frame, 'invalid index - GOAWAY'); # RFC 7541, 2.3.3. Index Address Space # Indices strictly greater than the sum of the lengths of both tables # MUST be treated as a decoding error. # 4.3. Header Compression and Decompression # A decoding error in a header block MUST be treated # as a connection error of type COMPRESSION_ERROR. is($frame->{last_sid}, $sid, 'invalid index - GOAWAY last stream'); is($frame->{code}, 9, 'invalid index - GOAWAY COMPRESSION_ERROR'); # HPACK zero index # RFC 7541, 6.1 Indexed Header Field Representation # The index value of 0 is not used. It MUST be treated as a decoding # error if found in an indexed header field representation. $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/', mode => 0 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => '', value => '', mode => 0 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ok($frame, 'zero index - GOAWAY'); is($frame->{code}, 9, 'zero index - GOAWAY COMPRESSION_ERROR'); # invalid table size update $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ table_size => 4097, headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/', mode => 0 }, { name => 'x-foo', value => 'X-Bar', mode => 0 }, { name => ':authority', value => 'localhost', mode => 1 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "GOAWAY" } @$frames; ok($frame, 'invalid table size - GOAWAY'); is($frame->{last_sid}, $sid, 'invalid table size - GOAWAY last stream'); is($frame->{code}, 9, 'invalid table size - GOAWAY COMPRESSION_ERROR'); # request header field with multiple values # 8.1.2.5. Compressing the Cookie Header Field # To allow for better compression efficiency, the Cookie header field # MAY be split into separate header fields <..>. $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/cookie', mode => 2 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'cookie', value => 'a=b', mode => 2}, { name => 'cookie', value => 'c=d', mode => 2}]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{'x-cookie-a'}, 'b', 'multiple request header fields - cookie'); is($frame->{headers}->{'x-cookie-c'}, 'd', 'multiple request header fields - cookie 2'); is($frame->{headers}->{'x-cookie'}, 'a=b; c=d', 'multiple request header fields - semi-colon'); # request header field with multiple values to HTTP backend # 8.1.2.5. Compressing the Cookie Header Field # these MUST be concatenated into a single octet string # using the two-octet delimiter of 0x3B, 0x20 (the ASCII string "; ") # before being passed into a non-HTTP/2 context, such as an HTTP/1.1 # connection <..> $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/proxy/cookie', mode => 2 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'cookie', value => 'a=b', mode => 2 }, { name => 'cookie', value => 'c=d', mode => 2 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{'x-sent-cookie'}, 'a=b; c=d', 'multiple request header fields proxied - semi-colon'); is($frame->{headers}->{'x-sent-cookie2'}, '', 'multiple request header fields proxied - dublicate cookie'); is($frame->{headers}->{'x-sent-cookie-a'}, 'b', 'multiple request header fields proxied - cookie 1'); is($frame->{headers}->{'x-sent-cookie-c'}, 'd', 'multiple request header fields proxied - cookie 2'); # response header field with multiple values $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ path => '/set-cookie' }); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{'set-cookie'}[0], 'a=b', 'multiple response header fields - cookie'); is($frame->{headers}->{'set-cookie'}[1], 'c=d', 'multiple response header fields - cookie 2'); # response header field with multiple values from HTTP backend $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ path => '/proxy/set-cookie' }); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{'set-cookie'}[0], 'a=b', 'multiple response header proxied - cookie'); is($frame->{headers}->{'set-cookie'}[1], 'c=d', 'multiple response header proxied - cookie 2'); is($frame->{headers}->{'x-uc-a'}, 'b', 'multiple response header proxied - upstream cookie'); is($frame->{headers}->{'x-uc-c'}, 'd', 'multiple response header proxied - upstream cookie 2'); # CONTINUATION in response # put three long header fields (not less than SETTINGS_MAX_FRAME_SIZE/2) # to break header block into separate frames, one such field per frame $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ path => '/continuation?h=' . 'x' x 2**13 }); $frames = $s->read(all => [{ sid => $sid, fin => 0x4 }]); my @data = grep { $_->{type} =~ "HEADERS|CONTINUATION" } @$frames; my $data = $data[-1]; is(@{$data->{headers}{'x-longheader'}}, 3, 'response CONTINUATION - headers'); is($data->{headers}{'x-longheader'}[0], 'x' x 2**13, 'response CONTINUATION - header 1'); is($data->{headers}{'x-longheader'}[1], 'x' x 2**13, 'response CONTINUATION - header 2'); is($data->{headers}{'x-longheader'}[2], 'x' x 2**13, 'response CONTINUATION - header 3'); @data = sort { $a <=> $b } map { $_->{length} } @data; cmp_ok($data[-1], '<=', 2**14, 'response CONTINUATION - max frame size'); # same but without response DATA frames $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ path => '/continuation/204?h=' . 'x' x 2**13 }); $frames = $s->read(all => [{ sid => $sid, fin => 0x4 }]); @data = grep { $_->{type} =~ "HEADERS|CONTINUATION" } @$frames; $data = $data[-1]; is(@{$data->{headers}{'x-longheader'}}, 3, 'no body CONTINUATION - headers'); is($data->{headers}{'x-longheader'}[0], 'x' x 2**13, 'no body CONTINUATION - header 1'); is($data->{headers}{'x-longheader'}[1], 'x' x 2**13, 'no body CONTINUATION - header 2'); is($data->{headers}{'x-longheader'}[2], 'x' x 2**13, 'no body CONTINUATION - header 3'); @data = sort { $a <=> $b } map { $_->{length} } @data; cmp_ok($data[-1], '<=', 2**14, 'no body CONTINUATION - max frame size'); # response header block is always split by SETTINGS_MAX_FRAME_SIZE $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ path => '/continuation?h=' . 'x' x 2**15 }); $frames = $s->read(all => [{ sid => $sid, fin => 0x4 }]); @data = grep { $_->{type} =~ "HEADERS|CONTINUATION" } @$frames; @data = sort { $a <=> $b } map { $_->{length} } @data; cmp_ok($data[-1], '<=', 2**14, 'response header frames limited'); # response header frame sent in parts $s = Test::Nginx::HTTP2->new(port(8082)); $s->h2_settings(0, 0x5 => 2**17); $sid = $s->new_stream({ path => '/frame_size?h=' . 'x' x 2**15 }); $frames = $s->read(all => [{ sid => $sid, fin => 0x4 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; ok($frame, 'response header - parts'); SKIP: { skip 'response header failed', 1 unless $frame; skip 'broken sendfile', 1 if $^O eq 'freebsd' and $Config{osvers} =~ '11.0-release' and $t->read_file('nginx.conf') =~ /sendfile on/; is(length join('', @{$frame->{headers}->{'x-longheader'}}), 98304, 'response header - headers'); } # response header block split and sent in parts $s = Test::Nginx::HTTP2->new(port(8082)); $sid = $s->new_stream({ path => '/continuation?h=' . 'x' x 2**15 }); $frames = $s->read(all => [{ sid => $sid, fin => 0x4 }]); @data = grep { $_->{type} =~ "HEADERS|CONTINUATION" } @$frames; ok(@data, 'response header split'); SKIP: { skip 'response header split failed', 2 unless @data; my ($lengths) = sort { $b <=> $a } map { $_->{length} } @data; cmp_ok($lengths, '<=', 16384, 'response header split - max size'); is(length join('', @{$data[-1]->{headers}->{'x-longheader'}}), 98304, 'response header split - headers'); } # max_field_size - header field name $s = Test::Nginx::HTTP2->new(port(8084)); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/t2.html', mode => 1 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'longname10' x 2 . 'x', value => 'value', mode => 2 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq 'DATA' } @$frames; ok($frame, 'field name size less'); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/t2.html', mode => 1 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'longname10' x 2 . 'x', value => 'value', mode => 2 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq 'DATA' } @$frames; ok($frame, 'field name size second'); $s = Test::Nginx::HTTP2->new(port(8084)); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/t2.html', mode => 1 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'longname10' x 2 . 'xx', value => 'value', mode => 2 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq 'DATA' } @$frames; ok($frame, 'field name size equal'); $s = Test::Nginx::HTTP2->new(port(8084)); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/t2.html', mode => 1 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'longname10' x 2 . 'xxx', value => 'value', mode => 2 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq 'DATA' } @$frames; is($frame, undef, 'field name size greater'); # max_field_size - header field value $s = Test::Nginx::HTTP2->new(port(8084)); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/t2.html', mode => 1 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'name', value => 'valu5' x 4 . 'x', mode => 2 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq 'DATA' } @$frames; ok($frame, 'field value size less'); $s = Test::Nginx::HTTP2->new(port(8084)); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/t2.html', mode => 1 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'name', value => 'valu5' x 4 . 'xx', mode => 2 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq 'DATA' } @$frames; ok($frame, 'field value size equal'); $s = Test::Nginx::HTTP2->new(port(8084)); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/t2.html', mode => 1 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'name', value => 'valu5' x 4 . 'xxx', mode => 2 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq 'DATA' } @$frames; is($frame, undef, 'field value size greater'); # max_header_size $s = Test::Nginx::HTTP2->new(port(8085)); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/t2.html', mode => 1 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'longname9', value => 'x', mode => 2 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq 'DATA' } @$frames; ok($frame, 'header size less'); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/t2.html', mode => 1 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'longname9', value => 'x', mode => 2 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq 'DATA' } @$frames; ok($frame, 'header size second'); $s = Test::Nginx::HTTP2->new(port(8085)); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/t2.html', mode => 1 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'longname9', value => 'xx', mode => 2 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq 'DATA' } @$frames; ok($frame, 'header size equal'); $s = Test::Nginx::HTTP2->new(port(8085)); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/t2.html', mode => 1 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'longname9', value => 'xxx', mode => 2 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq 'DATA' } @$frames; is($frame, undef, 'header size greater'); # header size is based on (decompressed) header list # two extra 1-byte indices would otherwise fit in max_header_size $s = Test::Nginx::HTTP2->new(port(8085)); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/t2.html', mode => 1 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'longname9', value => 'x', mode => 2 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq 'DATA' } @$frames; ok($frame, 'header size new index'); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/t2.html', mode => 1 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'longname9', value => 'x', mode => 0 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq 'DATA' } @$frames; ok($frame, 'header size indexed'); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/t2.html', mode => 1 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'longname9', value => 'x', mode => 0 }, { name => 'longname9', value => 'x', mode => 0 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq 'GOAWAY' } @$frames; is($frame->{code}, 0xb, 'header size indexed greater'); # HPACK table boundary $s = Test::Nginx::HTTP2->new(); $s->read(all => [{ sid => $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/', mode => 0 }, { name => ':authority', value => '', mode => 0 }, { name => 'x' x 2016, value => 'x' x 2048, mode => 2 }]}), fin => 1 }]); $frames = $s->read(all => [{ sid => $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/', mode => 0 }, { name => ':authority', value => '', mode => 0 }, { name => 'x' x 2016, value => 'x' x 2048, mode => 0 }]}), fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; ok($frame, 'HPACK table boundary'); $s->read(all => [{ sid => $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/', mode => 0 }, { name => ':authority', value => '', mode => 0 }, { name => 'x' x 33, value => 'x' x 4031, mode => 2 }]}), fin => 1 }]); $frames = $s->read(all => [{ sid => $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/', mode => 0 }, { name => ':authority', value => '', mode => 0 }, { name => 'x' x 33, value => 'x' x 4031, mode => 0 }]}), fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; ok($frame, 'HPACK table boundary - header field name'); $s->read(all => [{ sid => $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/', mode => 0 }, { name => ':authority', value => '', mode => 0 }, { name => 'x', value => 'x' x 64, mode => 2 }]}), fin => 1 }]); $frames = $s->read(all => [{ sid => $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/', mode => 0 }, { name => ':authority', value => '', mode => 0 }, { name => 'x', value => 'x' x 64, mode => 0 }]}), fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; ok($frame, 'HPACK table boundary - header field value'); # ensure that request header field value with newline doesn't get split # # 10.3. Intermediary Encapsulation Attacks # Any request or response that contains a character not permitted # in a header field value MUST be treated as malformed. $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/proxy2/', mode => 1 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'x-foo', value => "x-bar\r\nreferer:see-this", mode => 2 }]}); $frames = $s->read(all => [{ type => 'RST_STREAM' }]); # 10.3. Intermediary Encapsulation Attacks # An intermediary therefore cannot translate an HTTP/2 request or response # containing an invalid field name into an HTTP/1.1 message. ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; isnt($frame->{headers}->{'x-referer'}, 'see-this', 'newline in request header'); # 8.1.2.6. Malformed Requests and Responses # Malformed requests or responses that are detected MUST be treated # as a stream error (Section 5.4.2) of type PROTOCOL_ERROR. ($frame) = grep { $_->{type} eq "RST_STREAM" } @$frames; is($frame->{sid}, $sid, 'newline in request header - RST_STREAM sid'); is($frame->{length}, 4, 'newline in request header - RST_STREAM length'); is($frame->{flags}, 0, 'newline in request header - RST_STREAM flags'); is($frame->{code}, 1, 'newline in request header - RST_STREAM code'); # invalid header name as seen with underscore should not lead to ignoring rest $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/', mode => 0 }, { name => ':authority', value => 'localhost', mode => 1 }, { name => 'x_foo', value => "x-bar", mode => 2 }, { name => 'referer', value => "see-this", mode => 1 }]}); $frames = $s->read(all => [{ type => 'HEADERS' }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{'x-referer'}, 'see-this', 'after invalid header name'); # missing mandatory request header ':scheme' TODO: { local $TODO = 'not yet' unless $t->has_version('1.13.2'); $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':path', value => '/', mode => 0 }, { name => ':authority', value => 'localhost', mode => 1 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{':status'}, 400, 'incomplete headers'); } # empty request header ':authority' $s = Test::Nginx::HTTP2->new(); $sid = $s->new_stream({ headers => [ { name => ':method', value => 'GET', mode => 0 }, { name => ':scheme', value => 'http', mode => 0 }, { name => ':path', value => '/', mode => 0 }, { name => ':authority', value => '', mode => 0 }]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{':status'}, 400, 'empty authority'); # client sent invalid :path header $sid = $s->new_stream({ path => 't1.html' }); $frames = $s->read(all => [{ type => 'RST_STREAM' }]); ($frame) = grep { $_->{type} eq "RST_STREAM" } @$frames; is($frame->{code}, 1, 'invalid path'); ############################################################################### sub http_daemon { my $server = IO::Socket::INET->new( Proto => 'tcp', LocalHost => '127.0.0.1', LocalPort => port(8083), Listen => 5, Reuse => 1 ) or die "Can't create listening socket: $!\n"; local $SIG{PIPE} = 'IGNORE'; while (my $client = $server->accept()) { $client->autoflush(1); my $headers = ''; my $uri = ''; while (<$client>) { $headers .= $_; last if (/^\x0d?\x0a?$/); } next if $headers eq ''; $uri = $1 if $headers =~ /^\S+\s+([^ ]+)\s+HTTP/i; if ($uri eq '/cookie') { my ($cookie, $cookie2) = $headers =~ /Cookie: (.+)/ig; $cookie2 = '' unless defined $cookie2; my ($cookie_a, $cookie_c) = ('', ''); $cookie_a = $1 if $headers =~ /X-Cookie-a: (.+)/i; $cookie_c = $1 if $headers =~ /X-Cookie-c: (.+)/i; print $client <<EOF; HTTP/1.1 200 OK Connection: close X-Sent-Cookie: $cookie X-Sent-Cookie2: $cookie2 X-Sent-Cookie-a: $cookie_a X-Sent-Cookie-c: $cookie_c EOF } elsif ($uri eq '/set-cookie') { print $client <<EOF; HTTP/1.1 200 OK Connection: close Set-Cookie: a=b Set-Cookie: c=d EOF } } continue { close $client; } } ###############################################################################