Mercurial > hg > nginx-tests
diff h2_headers.t @ 876:a6abbfed42c0
Tests: split HTTP/2 tests, HTTP2 package introduced.
author | Andrey Zelenkov <zelenkov@nginx.com> |
---|---|
date | Wed, 23 Mar 2016 17:23:08 +0300 |
parents | |
children | 3b90649691cc |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/h2_headers.t Wed Mar 23 17:23:08 2016 +0300 @@ -0,0 +1,1085 @@ +#!/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; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::HTTP2 qw/ :DEFAULT :frame /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http http_v2 proxy rewrite/)->plan(92) + ->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); + +open OLDERR, ">&", \*STDERR; close STDERR; +$t->run(); +open STDERR, ">&", \*OLDERR; + +$t->waitforsocket('127.0.0.1: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 $sess = new_session(); +my $sid = new_stream($sess, { 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 = h2_read($sess, 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 + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{':status'}, 200, 'literal with indexing'); + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, 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 + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{':status'}, 200, 'literal with indexing - new'); + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, 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 + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{':status'}, 200, 'literal without indexing'); + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{':status'}, 200, 'literal without indexing - huffman'); + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, 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 + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{':status'}, 200, 'literal without indexing - new'); + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, 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 + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{':status'}, 200, 'literal never indexed'); + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{':status'}, 200, 'literal never indexed - huffman'); + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, 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 + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{':status'}, 200, 'literal never indexed - new'); + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, 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 + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'x-referer'}, 'foo', 'value with indexing - new'); + +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'x-referer'}, 'foo', 'value with indexing - indexed'); + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, 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 = new_stream($sess, { 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 = h2_read($sess, 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 = new_stream($sess, { 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 = h2_read($sess, 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 + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'x-sent-foo'}, '{{{{{', 'rare chars'); +like($sess->{headers}, qr/\Q{{{{{/, 'rare chars - no huffman encoding'); + +# response header field with huffman encoding +# NB: implementation detail, not obligated + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'x-sent-foo'}, 'aaaaa', 'well known chars'); + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.9.12'); + +unlike($sess->{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); + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'x-sent-foo'}, $field, 'all chars'); + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.9.12'); + +unlike($sess->{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 + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +isnt($frame, undef, 'updated table size - remaining index'); + +$sid = new_stream($sess, { 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 = h2_read($sess, 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. + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +ok($frame, 'zero index - GOAWAY'); +is($frame->{code}, 9, 'zero index - GOAWAY COMPRESSION_ERROR'); + +# invalid table size update + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, 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 <..>. + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, 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 <..> + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, 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 + +$sess = new_session(); +$sid = new_stream($sess, { path => '/set-cookie' }); +$frames = h2_read($sess, 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 + +$sess = new_session(); +$sid = new_stream($sess, { path => '/proxy/set-cookie' }); +$frames = h2_read($sess, 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 + +$sess = new_session(); +$sid = new_stream($sess, { path => '/continuation?h=' . 'x' x 2**13 }); + +$frames = h2_read($sess, all => [{ sid => $sid, fin => 0x4 }]); +my @data = grep { $_->{type} =~ "HEADERS|CONTINUATION" } @$frames; +is(@{$data[-1]->{headers}{'x-longheader'}}, 3, + 'response CONTINUATION - headers'); +is($data[-1]->{headers}{'x-longheader'}[0], 'x' x 2**13, + 'response CONTINUATION - header 1'); +is($data[-1]->{headers}{'x-longheader'}[1], 'x' x 2**13, + 'response CONTINUATION - header 2'); +is($data[-1]->{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 + +$sess = new_session(); +$sid = new_stream($sess, { path => '/continuation/204?h=' . 'x' x 2**13 }); + +$frames = h2_read($sess, all => [{ sid => $sid, fin => 0x4 }]); +@data = grep { $_->{type} =~ "HEADERS|CONTINUATION" } @$frames; +is(@{$data[-1]->{headers}{'x-longheader'}}, 3, + 'no body CONTINUATION - headers'); +is($data[-1]->{headers}{'x-longheader'}[0], 'x' x 2**13, + 'no body CONTINUATION - header 1'); +is($data[-1]->{headers}{'x-longheader'}[1], 'x' x 2**13, + 'no body CONTINUATION - header 2'); +is($data[-1]->{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 + +$sess = new_session(); +$sid = new_stream($sess, { path => '/continuation?h=' . 'x' x 2**15 }); + +$frames = h2_read($sess, 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 + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.9.7'); + +$sess = new_session(8082); +h2_settings($sess, 0, 0x5 => 2**17); + +$sid = new_stream($sess, { path => '/frame_size?h=' . 'x' x 2**15 }); +$frames = h2_read($sess, all => [{ sid => $sid, fin => 0x4 }]); + +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +ok($frame, 'response header - parts'); + +SKIP: { +skip 'response header failed', 1 unless $frame; + +is(length join('', @{$frame->{headers}->{'x-longheader'}}), 98304, + 'response header - headers'); + +} + +# response header block split and sent in parts + +$sess = new_session(8082); +$sid = new_stream($sess, { path => '/continuation?h=' . 'x' x 2**15 }); +$frames = h2_read($sess, all => [{ sid => $sid, fin => 0x4 }]); + +@data = grep { $_->{type} =~ "HEADERS|CONTINUATION" } @$frames; +my ($lengths) = sort { $b <=> $a } map { $_->{length} } @data; +cmp_ok($lengths, '<=', 16384, 'response header split - max size'); + +is(length join('', @{@$frames[-1]->{headers}->{'x-longheader'}}), 98304, + 'response header split - headers'); + +} + +# max_field_size - header field name + +$sess = new_session(8084); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq 'DATA' } @$frames; +ok($frame, 'field name size less'); + +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq 'DATA' } @$frames; +ok($frame, 'field name size second'); + +$sess = new_session(8084); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq 'DATA' } @$frames; +ok($frame, 'field name size equal'); + +$sess = new_session(8084); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq 'DATA' } @$frames; +is($frame, undef, 'field name size greater'); + +# max_field_size - header field value + +$sess = new_session(8084); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq 'DATA' } @$frames; +ok($frame, 'field value size less'); + +$sess = new_session(8084); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq 'DATA' } @$frames; +ok($frame, 'field value size equal'); + +$sess = new_session(8084); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq 'DATA' } @$frames; +is($frame, undef, 'field value size greater'); + +# max_header_size + +$sess = new_session(8085); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq 'DATA' } @$frames; +ok($frame, 'header size less'); + +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq 'DATA' } @$frames; +ok($frame, 'header size second'); + +$sess = new_session(8085); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq 'DATA' } @$frames; +ok($frame, 'header size equal'); + +$sess = new_session(8085); +$sid = new_stream($sess, { 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 = h2_read($sess, 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 + +$sess = new_session(8085); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq 'DATA' } @$frames; +ok($frame, 'header size new index'); + +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq 'DATA' } @$frames; +ok($frame, 'header size indexed'); + +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq 'GOAWAY' } @$frames; +is($frame->{code}, 0xb, 'header size indexed greater'); + +# HPACK table boundary + +$sess = new_session(); +h2_read($sess, all => [{ sid => new_stream($sess, { 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 = h2_read($sess, all => [{ sid => new_stream($sess, { 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'); + +h2_read($sess, all => [{ sid => new_stream($sess, { 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 = h2_read($sess, all => [{ sid => new_stream($sess, { 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'); + +h2_read($sess, all => [{ sid => new_stream($sess, { 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 = h2_read($sess, all => [{ sid => new_stream($sess, { 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. + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, 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 + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.9.7'); + +$sess = new_session(); +$sid = new_stream($sess, { 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 = h2_read($sess, all => [{ type => 'RST_STREAM' }]); + +($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'; + +$sess = new_session(); +$sid = new_stream($sess, { headers => [ + { name => ':method', value => 'GET', mode => 0 }, + { name => ':path', value => '/', mode => 0 }, + { name => ':authority', value => 'localhost', mode => 1 }]}); +$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{':status'}, 400, 'incomplete headers'); + +} + +# empty request header ':authority' + +$sess = new_session(); +$sid = new_stream($sess, { headers => [ + { name => ':method', value => 'GET', mode => 0 }, + { name => ':scheme', value => 'http', mode => 0 }, + { name => ':path', value => '/', mode => 0 }, + { name => ':authority', value => '', mode => 0 }]}); +$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{':status'}, 400, 'empty authority'); + +# client sent invalid :path header + +$sid = new_stream($sess, { path => 't1.html' }); +$frames = h2_read($sess, all => [{ type => 'RST_STREAM' }]); + +($frame) = grep { $_->{type} eq "RST_STREAM" } @$frames; +is($frame->{code}, 1, 'invalid path'); + +############################################################################### + +sub read_body_file { + my ($path) = @_; + open FILE, $path or return "$!"; + local $/; + my $content = <FILE>; + close FILE; + return $content; +} + +############################################################################### + +sub http_daemon { + my $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => '127.0.0.1', + LocalPort => 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; + } +} + +###############################################################################