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
new file mode 100644
--- /dev/null
+++ b/h2_headers.t
@@ -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;
+	}
+}
+
+###############################################################################