view h2_headers.t @ 1248:70192b1baf01

Tests: added exception test to stream_js.t using 'require'. The stream js tests introduced in edf5a3c9e36a fail on njs 0.1.14. It doesn't currently provide an easy way to check its version, whilst we are obligated to gracefully handle such cases somehow. With such an addition of 'require', now the tests are skipped instead on the previous versions.
author Sergey Kandaurov <pluknet@nginx.com>
date Tue, 21 Nov 2017 13:16:39 +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;
	}
}

###############################################################################