blob: 2c533adaeede22a3e781006f3fab8801d952b442 [file] [log] [blame]
#!/usr/bin/perl
# (C) Sergey Kandaurov
# (C) Nginx, Inc.
# Tests for HTTP/2 protocol [RFC7540].
###############################################################################
use warnings;
use strict;
use Test::More;
use IO::Select;
use Socket qw/ CRLF /;
BEGIN { use FindBin; chdir($FindBin::Bin); }
use lib 'lib';
use Test::Nginx;
###############################################################################
select STDERR; $| = 1;
select STDOUT; $| = 1;
eval { require IO::Socket::SSL; };
plan(skip_all => 'IO::Socket::SSL not installed') if $@;
eval { IO::Socket::SSL::SSL_VERIFY_NONE(); };
plan(skip_all => 'IO::Socket::SSL too old') if $@;
my $t = Test::Nginx->new()->has(qw/http http_ssl http_v2 proxy cache/)
->has(qw/limit_conn rewrite realip shmem/)
->has_daemon('openssl')->plan(196);
# FreeBSD has a bug in not treating zero iovcnt as EINVAL
$t->todo_alerts() unless $^O eq 'freebsd';
$t->write_file_expand('nginx.conf', <<'EOF');
%%TEST_GLOBALS%%
daemon off;
events {
}
http {
%%TEST_GLOBALS_HTTP%%
proxy_cache_path %%TESTDIR%%/cache keys_zone=NAME:1m;
limit_conn_zone $binary_remote_addr zone=conn:1m;
server {
listen 127.0.0.1:8080 http2;
listen 127.0.0.1:8081;
listen 127.0.0.1:8082 proxy_protocol http2;
listen 127.0.0.1:8084 http2 ssl;
server_name localhost;
ssl_certificate_key localhost.key;
ssl_certificate localhost.crt;
http2_max_field_size 128k;
http2_max_header_size 128k;
location / {
add_header X-Header X-Foo;
add_header X-Sent-Foo $http_x_foo;
add_header X-Referer $http_referer;
return 200 'body';
}
location /t {
}
location /t3.html {
limit_conn conn 1;
}
location /gzip.html {
gzip on;
gzip_min_length 0;
alias %%TESTDIR%%/t2.html;
}
location /frame_size {
http2_chunk_size 64k;
alias %%TESTDIR%%/t1.html;
output_buffers 2 1m;
}
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 /pp {
set_real_ip_from 127.0.0.1/32;
real_ip_header proxy_protocol;
alias %%TESTDIR%%/t2.html;
add_header X-PP $remote_addr;
}
location /h2 {
return 200 $http2;
}
location /chunk_size {
http2_chunk_size 1;
return 200 'body';
}
location /redirect {
error_page 405 /;
return 405;
}
location /return301 {
return 301;
}
location /return301_absolute {
return 301 text;
}
location /return301_relative {
return 301 /;
}
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_cache NAME;
proxy_cache_valid 1m;
proxy_set_header X-Cookie-a $cookie_a;
proxy_set_header X-Cookie-c $cookie_c;
}
location /proxy2/ {
add_header X-Body "$request_body";
proxy_pass http://127.0.0.1:8081/;
proxy_cache NAME;
proxy_cache_valid 1m;
}
location /proxy_buffering_off {
proxy_pass http://127.0.0.1:8081/;
proxy_cache NAME;
proxy_cache_valid 1m;
proxy_buffering off;
}
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:8085 http2;
server_name localhost;
return 200 first;
}
server {
listen 127.0.0.1:8085 http2;
server_name localhost2;
return 200 second;
}
server {
listen 127.0.0.1:8086 http2;
server_name localhost;
http2_max_concurrent_streams 1;
}
server {
listen 127.0.0.1:8087 http2;
server_name localhost;
http2_max_field_size 32;
}
server {
listen 127.0.0.1:8088 http2;
server_name localhost;
http2_max_header_size 64;
}
}
EOF
$t->write_file('openssl.conf', <<EOF);
[ req ]
default_bits = 2048
encrypt_key = no
distinguished_name = req_distinguished_name
[ req_distinguished_name ]
EOF
my $d = $t->testdir();
foreach my $name ('localhost') {
system('openssl req -x509 -new '
. "-config '$d/openssl.conf' -subj '/CN=$name/' "
. "-out '$d/$name.crt' -keyout '$d/$name.key' "
. ">>$d/openssl.out 2>&1") == 0
or die "Can't create certificate for $name: $!\n";
}
$t->run_daemon(\&http_daemon);
$t->run()->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('tbig.html',
join('', map { sprintf "XX%06dXX", $_ } (1 .. 100000)));
$t->write_file('t2.html', 'SEE-THIS');
$t->write_file('t3.html', 'SEE-THIS');
my %cframe = (
0 => { name => 'DATA', value => \&data },
1 => { name => 'HEADERS', value => \&headers },
# 2 => { name => 'PRIORITY', value => \&priority },
3 => { name => 'RST_STREAM', value => \&rst_stream },
4 => { name => 'SETTINGS', value => \&settings },
# 5 => { name => 'PUSH_PROIMSE', value => \&push_promise },
6 => { name => 'PING', value => \&ping },
7 => { name => 'GOAWAY', value => \&goaway },
8 => { name => 'WINDOW_UPDATE', value => \&window_update },
9 => { name => 'CONTINUATION', value => \&headers },
);
###############################################################################
# SETTINGS
my $sess = new_session();
my $frames = h2_read($sess, all => [
{ type => 'WINDOW_UPDATE' },
{ type => 'SETTINGS'}
]);
my ($frame) = grep { $_->{type} eq 'WINDOW_UPDATE' } @$frames;
ok($frame, 'WINDOW_UPDATE frame');
is($frame->{flags}, 0, 'WINDOW_UPDATE zero flags');
is($frame->{sid}, 0, 'WINDOW_UPDATE zero sid');
is($frame->{length}, 4, 'WINDOW_UPDATE fixed length');
($frame) = grep { $_->{type} eq 'SETTINGS' } @$frames;
ok($frame, 'SETTINGS frame');
is($frame->{flags}, 0, 'SETTINGS flags');
is($frame->{sid}, 0, 'SETTINGS stream');
h2_settings($sess, 1);
h2_settings($sess, 0);
$frames = h2_read($sess, all => [{ type => 'SETTINGS' }]);
($frame) = grep { $_->{type} eq 'SETTINGS' } @$frames;
ok($frame, 'SETTINGS frame ack');
is($frame->{flags}, 1, 'SETTINGS flags ack');
# PING
h2_ping($sess, 'SEE-THIS');
$frames = h2_read($sess, all => [{ type => 'PING' }]);
($frame) = grep { $_->{type} eq "PING" } @$frames;
ok($frame, 'PING frame');
is($frame->{value}, 'SEE-THIS', 'PING payload');
is($frame->{flags}, 1, 'PING flags ack');
is($frame->{sid}, 0, 'PING stream');
# GET
my $sid = new_stream($sess);
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
ok($frame, 'HEADERS frame');
is($frame->{sid}, $sid, 'HEADERS stream');
is($frame->{headers}->{':status'}, 200, 'HEADERS status');
is($frame->{headers}->{'x-header'}, 'X-Foo', 'HEADERS header');
($frame) = grep { $_->{type} eq "DATA" } @$frames;
ok($frame, 'DATA frame');
is($frame->{length}, length 'body', 'DATA length');
is($frame->{data}, 'body', 'DATA payload');
# GET in the new stream on same connection
$sid = new_stream($sess);
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
is($frame->{sid}, $sid, 'HEADERS stream 2');
is($frame->{headers}->{':status'}, 200, 'HEADERS status 2');
is($frame->{headers}->{'x-header'}, 'X-Foo', 'HEADERS header 2');
($frame) = grep { $_->{type} eq "DATA" } @$frames;
ok($frame, 'DATA frame 2');
is($frame->{sid}, $sid, 'HEADERS stream 2');
is($frame->{length}, length 'body', 'DATA length 2');
is($frame->{data}, 'body', 'DATA payload 2');
# various HEADERS compression/encoding, see hpack() for mode details
# 6.1. Indexed Header Field Representation
$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 }]});
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
is($frame->{headers}->{':status'}, 200, 'indexed header field');
# 6.2.1. Literal Header Field with Incremental Indexing
$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');
$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
$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');
$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
$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');
$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');
# 6.2.2. Literal Header Field without Indexing -- New Name
$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');
$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
$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');
$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');
# 6.2.2. Literal Header Field Never Indexed -- New Name
$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');
$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 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');
$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 => 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 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');
# 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
$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');
# HEAD
$sess = new_session();
$sid = new_stream($sess, { method => 'HEAD' });
$frames = h2_read($sess, all => [{ sid => $sid, fin => 0x4 }]);
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
is($frame->{sid}, $sid, 'HEAD - HEADERS');
is($frame->{headers}->{':status'}, 200, 'HEAD - HEADERS status');
is($frame->{headers}->{'x-header'}, 'X-Foo', 'HEAD - HEADERS header');
($frame) = grep { $_->{type} eq "DATA" } @$frames;
is($frame, undef, 'HEAD - no body');
# GET with PROXY protocol
my $proxy = 'PROXY TCP4 192.0.2.1 192.0.2.2 1234 5678' . CRLF;
$sess = new_session(8082, proxy => $proxy);
$sid = new_stream($sess, { path => '/pp' });
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
ok($frame, 'PROXY HEADERS frame');
is($frame->{headers}->{'x-pp'}, '192.0.2.1', 'PROXY remote addr');
# range filter
$sess = new_session();
$sid = new_stream($sess, { headers => [
{ name => ':method', value => 'GET', mode => 0 },
{ name => ':scheme', value => 'http', mode => 0 },
{ name => ':path', value => '/t1.html', mode => 1 },
{ name => ':authority', value => 'localhost', mode => 1 },
{ name => 'range', value => 'bytes=10-19', mode => 1 }]});
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
is($frame->{headers}->{':status'}, 206, 'range - HEADERS status');
($frame) = grep { $_->{type} eq "DATA" } @$frames;
is($frame->{length}, 10, 'range - DATA length');
is($frame->{data}, '002XXXX000', 'range - DATA payload');
# $http2
$sess = new_session();
$sid = new_stream($sess, { path => '/h2' });
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "DATA" } @$frames;
is($frame->{data}, 'h2c', 'http variable - h2c');
# SSL/TLS connection, NPN
SKIP: {
eval { IO::Socket::SSL->can_npn() or die; };
skip 'OpenSSL NPN support required', 1 if $@;
$sess = new_session(8084, SSL => 1, npn => 'h2');
$sid = new_stream($sess, { path => '/h2' });
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "DATA" } @$frames;
is($frame->{data}, 'h2', 'http variable - npn');
}
# SSL/TLS connection, ALPN
SKIP: {
eval { IO::Socket::SSL->can_alpn() or die; };
skip 'OpenSSL ALPN support required', 1 if $@;
$sess = new_session(8084, SSL => 1, alpn => 'h2');
$sid = new_stream($sess, { path => '/h2' });
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "DATA" } @$frames;
is($frame->{data}, 'h2', 'http variable - alpn');
}
# http2_chunk_size=1
$sess = new_session();
$sid = new_stream($sess, { path => '/chunk_size' });
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
my @data = grep { $_->{type} eq "DATA" } @$frames;
is(@data, 4, 'chunk_size frames');
is(join(' ', map { $_->{data} } @data), 'b o d y', 'chunk_size data');
is(join(' ', map { $_->{flags} } @data), '0 0 0 1', 'chunk_size flags');
# CONTINUATION
$sess = new_session();
$sid = new_stream($sess, { continuation => 1, headers => [
{ name => ':method', value => 'HEAD', mode => 1 },
{ name => ':scheme', value => 'http', mode => 0 },
{ name => ':path', value => '/', mode => 0 },
{ name => ':authority', value => 'localhost', mode => 1 }]});
h2_continue($sess, $sid, { continuation => 1, headers => [
{ name => 'x-foo', value => 'X-Bar', mode => 2 }]});
h2_continue($sess, $sid, { headers => [
{ name => 'referer', value => 'foo', mode => 2 }]});
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "DATA" } @$frames;
is($frame, undef, 'CONTINUATION - fragment 1');
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
is($frame->{headers}->{'x-sent-foo'}, 'X-Bar', 'CONTINUATION - fragment 2');
is($frame->{headers}->{'x-referer'}, 'foo', 'CONTINUATION - fragment 3');
# CONTINUATION - in the middle of request header field
$sess = new_session();
$sid = new_stream($sess, { continuation => [ 2, 4, 1, 5 ], headers => [
{ name => ':method', value => 'HEAD', mode => 1 },
{ name => ':scheme', value => 'http', 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'}, 200, 'CONTINUATION - in header field');
# frame padding
$sess = new_session();
$sid = new_stream($sess, { padding => 42, headers => [
{ name => ':method', value => 'GET', mode => 0 },
{ name => ':scheme', value => 'http', 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'}, 200, 'padding - HEADERS status');
$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 }]});
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
is($frame->{headers}->{':status'}, 200, 'padding - next stream');
# 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');
# internal redirect
$sess = new_session();
$sid = new_stream($sess, { path => '/redirect' });
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
is($frame->{headers}->{':status'}, 405, 'redirect - HEADERS');
($frame) = grep { $_->{type} eq "DATA" } @$frames;
ok($frame, 'redirect - DATA');
is($frame->{data}, 'body', 'redirect - DATA payload');
# return 301 with absolute URI
$sess = new_session();
$sid = new_stream($sess, { path => '/return301_absolute' });
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
is($frame->{headers}->{':status'}, 301, 'return 301 absolute - status');
is($frame->{headers}->{'location'}, 'text', 'return 301 absolute - location');
# return 301 with relative URI
$sess = new_session();
$sid = new_stream($sess, { path => '/return301_relative' });
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
is($frame->{headers}->{':status'}, 301, 'return 301 relative - status');
is($frame->{headers}->{'location'}, 'http://127.0.0.1:8080/',
'return 301 relative - location');
# return 301 with relative URI and ':authority' request header field
$sess = new_session();
$sid = new_stream($sess, { headers => [
{ name => ':method', value => 'GET', mode => 0 },
{ name => ':scheme', value => 'http', mode => 0 },
{ name => ':path', value => '/return301_relative', mode => 2 },
{ name => ':authority', value => 'localhost', mode => 2 }]});
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
is($frame->{headers}->{':status'}, 301,
'return 301 relative - authority - status');
is($frame->{headers}->{'location'}, 'http://localhost:8080/',
'return 301 relative - authority - location');
# return 301 with relative URI and 'host' request header field
$sess = new_session();
$sid = new_stream($sess, { headers => [
{ name => ':method', value => 'GET', mode => 0 },
{ name => ':scheme', value => 'http', mode => 0 },
{ name => ':path', value => '/return301_relative', mode => 2 },
{ name => 'host', value => 'localhost', mode => 2 }]});
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
is($frame->{headers}->{':status'}, 301,
'return 301 relative - host - status');
is($frame->{headers}->{'location'}, 'http://localhost:8080/',
'return 301 relative - host - location');
# virtual host
$sess = new_session(8085);
$sid = new_stream($sess, { headers => [
{ name => ':method', value => 'GET', mode => 0 },
{ name => ':scheme', value => 'http', mode => 0 },
{ name => ':path', value => '/', mode => 0 },
{ name => 'host', value => 'localhost', mode => 2 }]});
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
is($frame->{headers}->{':status'}, 200,
'virtual host - host - status');
($frame) = grep { $_->{type} eq "DATA" } @$frames;
is($frame->{data}, 'first', 'virtual host - host - DATA');
$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 => 2 }]});
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
is($frame->{headers}->{':status'}, 200,
'virtual host - authority - status');
($frame) = grep { $_->{type} eq "DATA" } @$frames;
is($frame->{data}, 'first', 'virtual host - authority - DATA');
# virtual host - second
$sid = new_stream($sess, { headers => [
{ name => ':method', value => 'GET', mode => 0 },
{ name => ':scheme', value => 'http', mode => 0 },
{ name => ':path', value => '/', mode => 0 },
{ name => 'host', value => 'localhost2', mode => 2 }]});
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
is($frame->{headers}->{':status'}, 200,
'virtual host 2 - host - status');
($frame) = grep { $_->{type} eq "DATA" } @$frames;
is($frame->{data}, 'second', 'virtual host 2 - host - DATA');
$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 => 'localhost2', mode => 2 }]});
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
is($frame->{headers}->{':status'}, 200,
'virtual host 2 - authority - status');
($frame) = grep { $_->{type} eq "DATA" } @$frames;
is($frame->{data}, 'second', 'virtual host 2 - authority - DATA');
# gzip tests for internal nginx version
$sess = new_session();
$sid = new_stream($sess, { headers => [
{ name => ':method', value => 'GET', mode => 0 },
{ name => ':scheme', value => 'http', mode => 0 },
{ name => ':path', value => '/gzip.html' },
{ name => ':authority', value => 'localhost', mode => 1 },
{ name => 'accept-encoding', value => 'gzip' }]});
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
is($frame->{headers}->{'content-encoding'}, 'gzip', 'gzip - encoding');
($frame) = grep { $_->{type} eq "DATA" } @$frames;
gunzip_like($frame->{data}, qr/^SEE-THIS\Z/, 'gzip - DATA');
# simple proxy cache test
$sess = new_session();
$sid = new_stream($sess, { path => '/proxy2/t2.html?2' });
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
is($frame->{headers}->{':status'}, '200', 'proxy cache');
my $etag = $frame->{headers}->{'etag'};
($frame) = grep { $_->{type} eq "DATA" } @$frames;
is($frame->{length}, length 'SEE-THIS', 'proxy cache - DATA');
is($frame->{data}, 'SEE-THIS', 'proxy cache - DATA payload');
$sid = new_stream($sess, { headers => [
{ name => ':method', value => 'GET', mode => 0 },
{ name => ':scheme', value => 'http', mode => 0 },
{ name => ':path', value => '/proxy2/t2.html?2' },
{ name => ':authority', value => 'localhost', mode => 1 },
{ name => 'if-none-match', value => $etag }]});
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
is($frame->{headers}->{':status'}, 304, 'proxy cache conditional');
# HEADERS could be received with fin, followed by DATA
$sess = new_session();
$sid = new_stream($sess, { path => '/proxy2/t2.html', method => 'HEAD' });
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
push @$frames, $_ for @{h2_read($sess, all => [{ sid => $sid }])};
ok(!grep ({ $_->{type} eq "DATA" } @$frames), 'proxy cache HEAD - no body');
# HEAD on empty cache with proxy_buffering off
$sess = new_session();
$sid = new_stream($sess,
{ path => '/proxy_buffering_off/t2.html?1', method => 'HEAD' });
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
push @$frames, $_ for @{h2_read($sess, all => [{ sid => $sid }])};
ok(!grep ({ $_->{type} eq "DATA" } @$frames),
'proxy cache HEAD buffering off - no body');
# request body (uses proxied response)
$sess = new_session();
$sid = new_stream($sess, { path => '/proxy2/t2.html', body => 'TEST' });
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
is($frame->{headers}->{'x-body'}, 'TEST', 'request body');
# request body with padding (uses proxied response)
$sess = new_session();
$sid = new_stream($sess,
{ path => '/proxy2/t2.html', body => 'TEST', body_padding => 42 });
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
is($frame->{headers}->{'x-body'}, 'TEST', 'request body with padding');
$sid = new_stream($sess);
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
is($frame->{headers}->{':status'}, '200', 'request body with padding - next');
# initial window size, client side
# 6.9.2. Initial Flow-Control Window Size
# When an HTTP/2 connection is first established, new streams are
# created with an initial flow-control window size of 65,535 octets.
# The connection flow-control window is also 65,535 octets.
$sess = new_session();
$sid = new_stream($sess, { path => '/t1.html' });
$frames = h2_read($sess, all => [{ sid => $sid, length => 2**16 - 1 }]);
# with the default http2_chunk_size, data is divided into 8 data frames
@data = grep { $_->{type} eq "DATA" } @$frames;
my $lengths = join ' ', map { $_->{length} } @data;
is($lengths, '8192 8192 8192 8192 8192 8192 8192 8191',
'iws - stream blocked on initial window size');
h2_ping($sess, 'SEE-THIS');
$frames = h2_read($sess, all => [{ type => 'PING' }]);
($frame) = grep { $_->{type} eq "PING" && $_->{flags} & 0x1 } @$frames;
ok($frame, 'iws - PING not blocked');
h2_window($sess, 2**16, $sid);
$frames = h2_read($sess);
is(@$frames, 0, 'iws - updated stream window');
h2_window($sess, 2**16);
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
@data = grep { $_->{type} eq "DATA" } @$frames;
my $sum = eval join '+', map { $_->{length} } @data;
is($sum, 81, 'iws - updated connection window');
# SETTINGS (initial window size, client side)
# 6.9.2. Initial Flow-Control Window Size
# Both endpoints can adjust the initial window size for new streams by
# including a value for SETTINGS_INITIAL_WINDOW_SIZE in the SETTINGS
# frame that forms part of the connection preface. The connection
# flow-control window can only be changed using WINDOW_UPDATE frames.
$sess = new_session();
h2_settings($sess, 0, 0x4 => 2**17);
h2_window($sess, 2**17);
$sid = new_stream($sess, { path => '/t1.html' });
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
@data = grep { $_->{type} eq "DATA" } @$frames;
$sum = eval join '+', map { $_->{length} } @data;
is($sum, 2**16 + 80, 'iws - increased');
# probe for negative available space in a flow control window
# 6.9.2. Initial Flow-Control Window Size
# A change to SETTINGS_INITIAL_WINDOW_SIZE can cause the available
# space in a flow-control window to become negative. A sender MUST
# track the negative flow-control window and MUST NOT send new flow-
# controlled frames until it receives WINDOW_UPDATE frames that cause
# the flow-control window to become positive.
$sess = new_session();
$sid = new_stream($sess, { path => '/t1.html' });
h2_read($sess, all => [{ sid => $sid, length => 2**16 - 1 }]);
h2_window($sess, 1);
h2_settings($sess, 0, 0x4 => 42);
h2_window($sess, 1024, $sid);
$frames = h2_read($sess, all => [{ type => 'SETTINGS' }]);
($frame) = grep { $_->{type} eq 'SETTINGS' } @$frames;
ok($frame, 'negative window - SETTINGS frame ack');
is($frame->{flags}, 1, 'negative window - SETTINGS flags ack');
($frame) = grep { $_->{type} ne 'SETTINGS' } @$frames;
is($frame, undef, 'negative window - no data');
# predefined window size, minus new iws settings, minus window update
h2_window($sess, 2**16 - 1 - 42 - 1024, $sid);
$frames = h2_read($sess);
is(@$frames, 0, 'zero window - no data');
h2_window($sess, 1, $sid);
$frames = h2_read($sess, all => [{ sid => $sid, length => 1 }]);
is(@$frames, 1, 'positive window');
is(@$frames[0]->{type}, 'DATA', 'positive window - data');
is(@$frames[0]->{length}, 1, 'positive window - data length');
# ask write handler in sending large response
$sid = new_stream($sess, { path => '/tbig.html' });
h2_window($sess, 2**30, $sid);
h2_window($sess, 2**30);
sleep 1;
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
is($frame->{headers}->{':status'}, 200, 'large response - HEADERS');
@data = grep { $_->{type} eq "DATA" } @$frames;
$sum = eval join '+', map { $_->{length} } @data;
is($sum, 1000000, 'large response - DATA');
# SETTINGS_MAX_FRAME_SIZE
$sess = new_session();
$sid = new_stream($sess, { path => '/frame_size' });
h2_window($sess, 2**18, 1);
h2_window($sess, 2**18);
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
@data = grep { $_->{type} eq "DATA" } @$frames;
is($data[0]->{length}, 2**14, 'max frame size - default');
$sess = new_session();
h2_settings($sess, 0, 0x5 => 2**15);
$sid = new_stream($sess, { path => '/frame_size' });
h2_window($sess, 2**18, 1);
h2_window($sess, 2**18);
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
@data = grep { $_->{type} eq "DATA" } @$frames;
is($data[0]->{length}, 2**15, 'max frame size - custom');
# 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 }]);
@data = grep { $_->{type} =~ "HEADERS|CONTINUATION" } @$frames;
is(@data, 3, 'response CONTINUATION - header block frames');
is($data[0]->{type}, 'HEADERS', 'response CONTINUATION - first');
is($data[0]->{flags}, 0, 'response CONTINUATION - first flags');
is($data[1]->{type}, 'CONTINUATION', 'response CONTINUATION - second');
is($data[1]->{flags}, 0, 'response CONTINUATION - second flags');
is($data[2]->{type}, 'CONTINUATION', 'response CONTINUATION - third');
is($data[2]->{flags}, 4, 'response CONTINUATION - third flags');
# 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, 3, 'no body CONTINUATION - header block frames');
is($data[0]->{type}, 'HEADERS', 'no body CONTINUATION - first');
is($data[0]->{flags}, 1, 'no body CONTINUATION - first flags');
is($data[1]->{type}, 'CONTINUATION', 'no body CONTINUATION - second');
is($data[1]->{flags}, 0, 'no body CONTINUATION - second flags');
is($data[2]->{type}, 'CONTINUATION', 'no body CONTINUATION - third');
is($data[2]->{flags}, 4, 'no body CONTINUATION - third flags');
# response header block is always split by SETTINGS_MAX_FRAME_SIZE
TODO: {
local $TODO = 'not yet';
$sess = new_session();
$sid = new_stream($sess, { path => '/continuation?h=' . 'x' x 2**14 });
$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');
}
# max_field_size
$sess = new_session(8087);
$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', 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 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', 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 size second');
$sess = new_session(8087);
$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', 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 size equal');
$sess = new_session(8087);
$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', 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 size greater');
# max_header_size
$sess = new_session(8088);
$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(8088);
$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(8088);
$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');
# stream multiplexing + WINDOW_UPDATE
$sess = new_session();
$sid = new_stream($sess, { path => '/t1.html' });
$frames = h2_read($sess, all => [{ sid => $sid, length => 2**16 - 1 }]);
@data = grep { $_->{type} eq "DATA" } @$frames;
$sum = eval join '+', map { $_->{length} } @data;
is($sum, 2**16 - 1, 'multiple - stream1 data');
my $sid2 = new_stream($sess, { path => '/t1.html' });
$frames = h2_read($sess, all => [{ sid => $sid2, fin => 0x4 }]);
@data = grep { $_->{type} eq "DATA" } @$frames;
is(@data, 0, 'multiple - stream2 no data');
h2_window($sess, 2**17, $sid);
h2_window($sess, 2**17, $sid2);
h2_window($sess, 2**17);
$frames = h2_read($sess, all => [
{ sid => $sid, fin => 1 },
{ sid => $sid2, fin => 1 }
]);
@data = grep { $_->{type} eq "DATA" && $_->{sid} == $sid } @$frames;
$sum = eval join '+', map { $_->{length} } @data;
is($sum, 81, 'multiple - stream1 remain data');
@data = grep { $_->{type} eq "DATA" && $_->{sid} == $sid2 } @$frames;
$sum = eval join '+', map { $_->{length} } @data;
is($sum, 2**16 + 80, 'multiple - stream2 full data');
# stream muliplexing + PRIORITY frames
$sess = new_session();
$sid = new_stream($sess, { path => '/t1.html' });
h2_read($sess, all => [{ sid => $sid, length => 2**16 - 1 }]);
$sid2 = new_stream($sess, { path => '/t2.html' });
h2_read($sess, all => [{ sid => $sid2, fin => 0x4 }]);
h2_priority($sess, 0, $sid);
h2_priority($sess, 255, $sid2);
h2_window($sess, 2**17, $sid);
h2_window($sess, 2**17, $sid2);
h2_window($sess, 2**17);
$frames = h2_read($sess, all => [
{ sid => $sid, fin => 1 },
{ sid => $sid2, fin => 1 }
]);
@data = grep { $_->{type} eq "DATA" } @$frames;
is(join(' ', map { $_->{sid} } @data), "$sid2 $sid", 'weight - PRIORITY 1');
# and vice versa
$sess = new_session();
$sid = new_stream($sess, { path => '/t1.html' });
h2_read($sess, all => [{ sid => $sid, length => 2**16 - 1 }]);
$sid2 = new_stream($sess, { path => '/t2.html' });
h2_read($sess, all => [{ sid => $sid2, fin => 0x4 }]);
h2_priority($sess, 255, $sid);
h2_priority($sess, 0, $sid2);
h2_window($sess, 2**17, $sid);
h2_window($sess, 2**17, $sid2);
h2_window($sess, 2**17);
$frames = h2_read($sess, all => [
{ sid => $sid, fin => 1 },
{ sid => $sid2, fin => 1 }
]);
@data = grep { $_->{type} eq "DATA" } @$frames;
is(join(' ', map { $_->{sid} } @data), "$sid $sid2", 'weight - PRIORITY 2');
# stream muliplexing + HEADERS PRIORITY flag
$sess = new_session();
$sid = new_stream($sess, { path => '/t1.html', prio => 0 });
h2_read($sess, all => [{ sid => $sid, length => 2**16 - 1 }]);
$sid2 = new_stream($sess, { path => '/t2.html', prio => 255 });
h2_read($sess, all => [{ sid => $sid2, fin => 0x4 }]);
h2_window($sess, 2**17, $sid);
h2_window($sess, 2**17, $sid2);
h2_window($sess, 2**17);
$frames = h2_read($sess, all => [
{ sid => $sid, fin => 1 },
{ sid => $sid2, fin => 1 }
]);
@data = grep { $_->{type} eq "DATA" } @$frames;
my $sids = join ' ', map { $_->{sid} } @data;
is($sids, "$sid2 $sid", 'weight - HEADERS PRIORITY 1');
# and vice versa
$sess = new_session();
$sid = new_stream($sess, { path => '/t1.html', prio => 255 });
h2_read($sess, all => [{ sid => $sid, length => 2**16 - 1 }]);
$sid2 = new_stream($sess, { path => '/t2.html', prio => 0 });
h2_read($sess, all => [{ sid => $sid2, fin => 0x4 }]);
h2_window($sess, 2**17, $sid);
h2_window($sess, 2**17, $sid2);
h2_window($sess, 2**17);
$frames = h2_read($sess, all => [
{ sid => $sid, fin => 1 },
{ sid => $sid2, fin => 1 }
]);
@data = grep { $_->{type} eq "DATA" } @$frames;
$sids = join ' ', map { $_->{sid} } @data;
is($sids, "$sid $sid2", 'weight - HEADERS PRIORITY 2');
# 5.3.1. Stream Dependencies
# PRIORITY frame
$sess = new_session();
h2_priority($sess, 16, 3, 0);
h2_priority($sess, 16, 1, 3);
$sid = new_stream($sess, { path => '/t1.html' });
h2_read($sess, all => [{ sid => $sid, length => 2**16 - 1 }]);
$sid2 = new_stream($sess, { path => '/t2.html' });
h2_read($sess, all => [{ sid => $sid2, fin => 0x4 }]);
h2_window($sess, 2**17, $sid);
h2_window($sess, 2**17, $sid2);
h2_window($sess, 2**17);
$frames = h2_read($sess, all => [
{ sid => $sid, fin => 1 },
{ sid => $sid2, fin => 1 },
]);
@data = grep { $_->{type} eq "DATA" } @$frames;
$sids = join ' ', map { $_->{sid} } @data;
is($sids, "$sid2 $sid", 'dependency - PRIORITY 1');
# and vice versa
$sess = new_session();
h2_priority($sess, 16, 1, 0);
h2_priority($sess, 16, 3, 1);
$sid = new_stream($sess, { path => '/t1.html' });
h2_read($sess, all => [{ sid => $sid, length => 2**16 - 1 }]);
$sid2 = new_stream($sess, { path => '/t2.html' });
h2_read($sess, all => [{ sid => $sid2, fin => 0x4 }]);
h2_window($sess, 2**17, $sid);
h2_window($sess, 2**17, $sid2);
h2_window($sess, 2**17);
$frames = h2_read($sess, all => [
{ sid => $sid, fin => 1 },
{ sid => $sid2, fin => 1 },
]);
@data = grep { $_->{type} eq "DATA" } @$frames;
$sids = join ' ', map { $_->{sid} } @data;
is($sids, "$sid $sid2", 'dependency - PRIORITY 2');
# HEADERS PRIORITY flag, reprioritize prior PRIORITY frame records
$sess = new_session();
h2_priority($sess, 16, 1, 0);
h2_priority($sess, 16, 3, 0);
$sid = new_stream($sess, { path => '/t1.html', dep => 3 });
h2_read($sess, all => [{ sid => $sid, length => 2**16 - 1 }]);
$sid2 = new_stream($sess, { path => '/t2.html' });
h2_read($sess, all => [{ sid => $sid2, fin => 0x4 }]);
h2_window($sess, 2**17, $sid);
h2_window($sess, 2**17, $sid2);
h2_window($sess, 2**17);
$frames = h2_read($sess, all => [
{ sid => $sid, fin => 1 },
{ sid => $sid2, fin => 1 },
]);
@data = grep { $_->{type} eq "DATA" } @$frames;
$sids = join ' ', map { $_->{sid} } @data;
is($sids, "$sid2 $sid", 'dependency - HEADERS PRIORITY 1');
# and vice versa
$sess = new_session();
h2_priority($sess, 16, 1, 0);
h2_priority($sess, 16, 3, 0);
$sid = new_stream($sess, { path => '/t1.html' });
h2_read($sess, all => [{ sid => $sid, length => 2**16 - 1 }]);
$sid2 = new_stream($sess, { path => '/t2.html', dep => 1 });
h2_read($sess, all => [{ sid => $sid2, fin => 0x4 }]);
h2_window($sess, 2**17, $sid);
h2_window($sess, 2**17, $sid2);
h2_window($sess, 2**17);
$frames = h2_read($sess, all => [
{ sid => $sid, fin => 1 },
{ sid => $sid2, fin => 1 },
]);
@data = grep { $_->{type} eq "DATA" } @$frames;
$sids = join ' ', map { $_->{sid} } @data;
is($sids, "$sid $sid2", 'dependency - HEADERS PRIORITY 2');
# PRIORITY frame, weighted dependencies
$sess = new_session();
h2_priority($sess, 16, 5, 0);
h2_priority($sess, 255, 1, 5);
h2_priority($sess, 0, 3, 5);
$sid = new_stream($sess, { path => '/t1.html' });
h2_read($sess, all => [{ sid => $sid, length => 2**16 - 1 }]);
$sid2 = new_stream($sess, { path => '/t2.html' });
h2_read($sess, all => [{ sid => $sid2, fin => 0x4 }]);
my $sid3 = new_stream($sess, { path => '/t2.html' });
h2_read($sess, all => [{ sid => $sid2, fin => 0x4 }]);
h2_window($sess, 2**16, 1);
h2_window($sess, 2**16, 3);
h2_window($sess, 2**16, 5);
h2_window($sess, 2**16);
$frames = h2_read($sess, all => [
{ sid => $sid, fin => 1 },
{ sid => $sid2, fin => 1 },
{ sid => $sid3, fin => 1 },
]);
@data = grep { $_->{type} eq "DATA" } @$frames;
$sids = join ' ', map { $_->{sid} } @data;
is($sids, "$sid3 $sid $sid2", 'weighted dependency - PRIORITY 1');
# and vice versa
$sess = new_session();
h2_priority($sess, 16, 5, 0);
h2_priority($sess, 0, 1, 5);
h2_priority($sess, 255, 3, 5);
$sid = new_stream($sess, { path => '/t1.html' });
h2_read($sess, all => [{ sid => $sid, length => 2**16 - 1 }]);
$sid2 = new_stream($sess, { path => '/t2.html' });
h2_read($sess, all => [{ sid => $sid2, fin => 0x4 }]);
$sid3 = new_stream($sess, { path => '/t2.html' });
h2_read($sess, all => [{ sid => $sid2, fin => 0x4 }]);
h2_window($sess, 2**16, 1);
h2_window($sess, 2**16, 3);
h2_window($sess, 2**16, 5);
h2_window($sess, 2**16);
$frames = h2_read($sess, all => [
{ sid => $sid, fin => 1 },
{ sid => $sid2, fin => 1 },
{ sid => $sid3, fin => 1 },
]);
@data = grep { $_->{type} eq "DATA" } @$frames;
$sids = join ' ', map { $_->{sid} } @data;
is($sids, "$sid3 $sid2 $sid", 'weighted dependency - PRIORITY 2');
# limit_conn
$sess = new_session();
h2_settings($sess, 0, 0x4 => 1);
$sid = new_stream($sess, { path => '/t3.html' });
$frames = h2_read($sess, all => [{ sid => $sid, length => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" && $_->{sid} == $sid } @$frames;
is($frame->{headers}->{':status'}, 200, 'limit_conn first stream');
$sid2 = new_stream($sess, { path => '/t3.html' });
$frames = h2_read($sess, all => [{ sid => $sid2, fin => 0 }]);
($frame) = grep { $_->{type} eq "HEADERS" && $_->{sid} == $sid2 } @$frames;
is($frame->{headers}->{':status'}, 503, 'limit_conn rejected');
h2_settings($sess, 0, 0x4 => 2**16);
h2_read($sess, all => [
{ sid => $sid, fin => 1 },
{ sid => $sid2, fin => 1 }
]);
# limit_conn + client's RST_STREAM
$sess = new_session();
h2_settings($sess, 0, 0x4 => 1);
$sid = new_stream($sess, { path => '/t3.html' });
$frames = h2_read($sess, all => [{ sid => $sid, length => 1 }]);
h2_rst($sess, $sid, 5);
($frame) = grep { $_->{type} eq "HEADERS" && $_->{sid} == $sid } @$frames;
is($frame->{headers}->{':status'}, 200, 'RST_STREAM 1');
$sid2 = new_stream($sess, { path => '/t3.html' });
$frames = h2_read($sess, all => [{ sid => $sid2, fin => 0 }]);
($frame) = grep { $_->{type} eq "HEADERS" && $_->{sid} == $sid2 } @$frames;
is($frame->{headers}->{':status'}, 200, 'RST_STREAM 2');
# http2_max_concurrent_streams
$sess = new_session(8086);
$frames = h2_read($sess, all => [{ type => 'SETTINGS' }]);
($frame) = grep { $_->{type} eq 'SETTINGS' } @$frames;
is($frame->{3}, 1, 'http2_max_concurrent_streams SETTINGS');
h2_window($sess, 2**18);
$sid = new_stream($sess, { path => '/t1.html' });
$frames = h2_read($sess, all => [{ sid => $sid, length => 2 ** 16 - 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" && $_->{sid} == $sid } @$frames;
is($frame->{headers}->{':status'}, 200, 'http2_max_concurrent_streams');
$sid2 = new_stream($sess, { path => '/t1.html' });
$frames = h2_read($sess, all => [{ type => 'RST_STREAM' }]);
($frame) = grep { $_->{type} eq "HEADERS" && $_->{sid} == $sid2 } @$frames;
isnt($frame->{headers}->{':status'}, 200, 'http2_max_concurrent_streams 2');
($frame) = grep { $_->{type} eq "RST_STREAM" && $_->{sid} == $sid2 } @$frames;
is($frame->{sid}, $sid2, 'http2_max_concurrent_streams RST_STREAM sid');
is($frame->{length}, 4, 'http2_max_concurrent_streams RST_STREAM length');
is($frame->{flags}, 0, 'http2_max_concurrent_streams RST_STREAM flags');
is($frame->{code}, 7, 'http2_max_concurrent_streams RST_STREAM code');
h2_window($sess, 2**16, $sid);
h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
$sid = new_stream($sess, { path => '/t2.html' });
$frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
($frame) = grep { $_->{type} eq "HEADERS" && $_->{sid} == $sid } @$frames;
is($frame->{headers}->{':status'}, 200, 'http2_max_concurrent_streams 3');
# some invalid cases below
# 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 => [{ sid => $sid, fin => 1 }]);
# 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');
# GOAWAY on SYN_STREAM with even StreamID
$sess = new_session();
new_stream($sess, { path => '/' }, 2);
$frames = h2_read($sess, all => [{ type => 'GOAWAY' }]);
($frame) = grep { $_->{type} eq "GOAWAY" } @$frames;
ok($frame, 'even stream - GOAWAY frame');
is($frame->{code}, 1, 'even stream - error code');
is($frame->{last_sid}, 0, 'even stream - last stream');
# GOAWAY on SYN_STREAM with backward StreamID
# 5.1.1. Stream Identifiers
# The first use of a new stream identifier implicitly closes all
# streams in the "idle" state <..> with a lower-valued stream identifier.
$sess = new_session();
$sid = new_stream($sess, { path => '/' }, 3);
h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
$sid2 = new_stream($sess, { path => '/' }, 1);
$frames = h2_read($sess, all => [{ type => 'GOAWAY' }]);
($frame) = grep { $_->{type} eq "GOAWAY" } @$frames;
ok($frame, 'backward stream - GOAWAY frame');
is($frame->{code}, 1, 'backward stream - error code');
is($frame->{last_sid}, $sid, 'backward stream - last stream');
# GOAWAY on the second SYN_STREAM with same StreamID
$sess = new_session();
$sid = new_stream($sess, { path => '/' });
h2_read($sess, all => [{ sid => $sid, fin => 1 }]);
$sid2 = new_stream($sess, { path => '/' }, $sid);
$frames = h2_read($sess, all => [{ type => 'GOAWAY' }]);
($frame) = grep { $_->{type} eq "GOAWAY" } @$frames;
ok($frame, 'dup stream - GOAWAY frame');
is($frame->{code}, 1, 'dup stream - error code');
is($frame->{last_sid}, $sid, 'dup stream - last stream');
# 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');
# unknown frame type
$sess = new_session();
h2_unknown($sess, 'payload');
h2_ping($sess, 'SEE-THIS');
$frames = h2_read($sess, all => [{ type => 'PING' }]);
($frame) = grep { $_->{type} eq "PING" } @$frames;
is($frame->{value}, 'SEE-THIS', 'unknown frame type');
# GOAWAY - force closing a connection by server
$sid = new_stream($sess, { path => 't1.html' });
h2_read($sess, all => [{ sid => $sid, length => 2**16 - 1 }]);
$t->stop();
$frames = h2_read($sess, all => [{ type => 'GOAWAY' }]);
($frame) = grep { $_->{type} eq "GOAWAY" } @$frames;
ok($frame, 'GOAWAY on connection close');
###############################################################################
sub h2_ping {
my ($sess, $payload) = @_;
raw_write($sess->{socket}, pack("x2C2x5a8", 8, 0x6, $payload));
}
sub h2_rst {
my ($sess, $stream, $error) = @_;
raw_write($sess->{socket}, pack("x2C2xNN", 4, 0x3, $stream, $error));
}
sub h2_priority {
my ($sess, $w, $stream, $dep) = @_;
$stream = 0 unless defined $stream;
$dep = 0 unless defined $dep;
raw_write($sess->{socket}, pack("x2C2xNNC", 5, 0x2, $stream, $dep, $w));
}
sub h2_window {
my ($sess, $win, $stream) = @_;
$stream = 0 unless defined $stream;
raw_write($sess->{socket}, pack("x2C2xNN", 4, 0x8, $stream, $win));
}
sub h2_settings {
my ($sess, $ack, %extra) = @_;
my $len = 6 * keys %extra;
my $buf = pack_length($len) . pack "CCx4", 0x4, $ack ? 0x1 : 0x0;
$buf .= join '', map { pack "nN", $_, $extra{$_} } keys %extra;
raw_write($sess->{socket}, $buf);
}
sub h2_unknown {
my ($sess, $payload) = @_;
my $buf = pack_length(length($payload)) . pack("Cx5a*", 0xa, $payload);
raw_write($sess->{socket}, $buf);
}
sub h2_continue {
my ($ctx, $stream, $uri) = @_;
$uri->{h2_continue} = 1;
return new_stream($ctx, $uri, $stream);
}
sub new_stream {
my ($ctx, $uri, $stream) = @_;
my ($input, $buf);
my ($d, $status);
my $host = $uri->{host} || '127.0.0.1:8080';
my $method = $uri->{method} || 'GET';
my $scheme = $uri->{scheme} || 'http';
my $path = $uri->{path} || '/';
my $headers = $uri->{headers};
my $body = $uri->{body};
my $prio = $uri->{prio};
my $dep = $uri->{dep};
my $split = ref $uri->{continuation} && $uri->{continuation} || [];
my $pad = defined $uri->{padding} ? $uri->{padding} : 0;
my $padlen = defined $uri->{padding} ? 1 : 0;
my $bpad = defined $uri->{body_padding} ? $uri->{body_padding} : 0;
my $bpadlen = defined $uri->{body_padding} ? 1 : 0;
my $type = defined $uri->{h2_continue} ? 0x9 : 0x1;
my $flags = defined $uri->{continuation} ? 0x0 : 0x4;
$flags |= 0x1 unless defined $body;
$flags |= 0x8 if $padlen;
$flags |= 0x20 if defined $dep || defined $prio;
if ($stream) {
$ctx->{last_stream} = $stream;
} else {
$ctx->{last_stream} += 2;
}
$buf = pack("xxx"); # Length stub
$buf .= pack("CC", $type, $flags); # END_HEADERS
$buf .= pack("N", $ctx->{last_stream}); # Stream-ID
$dep = 0 if defined $prio and not defined $dep;
$prio = 16 if defined $dep and not defined $prio;
unless ($headers) {
$input = hpack($ctx, ":method", $method);
$input .= hpack($ctx, ":scheme", $scheme);
$input .= hpack($ctx, ":path", $path);
$input .= hpack($ctx, ":authority", $host);
$input .= hpack($ctx, "content-length", length($body)) if $body;
} else {
$input = join '', map {
hpack($ctx, $_->{name}, $_->{value},
mode => $_->{mode}, huff => $_->{huff})
} @$headers if $headers;
}
$input = pack("B*", '001' . ipack(5, $uri->{table_size})) . $input
if defined $uri->{table_size};
my @input = map { substr $input, 0, $_, "" } @$split;
push @input, $input;
# set length, attach headers, padding, priority
my $hlen = length($input[0]) + $pad + $padlen;
$hlen += 5 if $flags & 0x20;
$buf |= pack_length($hlen);
$buf .= pack 'C', $pad if $padlen; # Pad Length?
$buf .= pack 'NC', $dep, $prio if $flags & 0x20;
$buf .= $input[0];
$buf .= (pack 'C', 0) x $pad if $padlen; # Padding
shift @input;
while (@input) {
$input = shift @input;
$flags = @input ? 0x0 : 0x4;
$buf .= pack_length(length($input));
$buf .= pack("CC", 0x9, $flags);
$buf .= pack("N", $ctx->{last_stream});
$buf .= $input;
}
if (defined $body) {
$buf .= pack_length(length($body) + $bpad + $bpadlen);
my $flags = $bpadlen ? 0x8 : 0x0;
$buf .= pack 'CC', 0x0, 0x1 | $flags; # DATA, END_STREAM
$buf .= pack 'N', $ctx->{last_stream};
$buf .= pack 'C', $bpad if $bpadlen; # DATA Pad Length?
$buf .= $body;
$buf .= (pack 'C', 0) x $bpad if $bpadlen; # DATA Padding
}
raw_write($ctx->{socket}, $buf);
return $ctx->{last_stream};
}
sub h2_read {
my ($sess, %extra) = @_;
my (@got);
my $s = $sess->{socket};
my $buf = '';
while (1) {
$buf = raw_read($s, $buf, 9);
last unless length $buf;
my $length = unpack_length($buf);
my $type = unpack('x3C', $buf);
my $flags = unpack('x4C', $buf);
my $stream = unpack "x5 B32", $buf;
substr($stream, 0, 1) = 0;
$stream = unpack("N", pack("B32", $stream));
$buf = raw_read($s, $buf, $length + 9);
last unless length $buf;
$buf = substr($buf, 9);
my $frame = $cframe{$type}{value}($sess, $buf, $length);
$frame->{length} = $length;
$frame->{type} = $cframe{$type}{name};
$frame->{flags} = $flags;
$frame->{sid} = $stream;
push @got, $frame;
$buf = substr($buf, $length);
last unless test_fin($got[-1], $extra{all});
};
return \@got;
}
sub test_fin {
my ($frame, $all) = @_;
my @test = @{$all};
# wait for the specified DATA length
for (@test) {
if ($_->{length} && $frame->{type} eq 'DATA') {
# check also for StreamID if needed
if (!$_->{sid} || $_->{sid} == $frame->{sid}) {
$_->{length} -= $frame->{length};
}
}
}
@test = grep { !(defined $_->{length} && $_->{length} == 0) } @test;
# wait for the fin flag
@test = grep { !(defined $_->{fin}
&& $_->{sid} == $frame->{sid} && $_->{fin} & $frame->{flags})
} @test if defined $frame->{flags};
# wait for the specified frame
@test = grep { !($_->{type} && $_->{type} eq $frame->{type}) } @test;
@{$all} = @test;
}
sub headers {
my ($ctx, $buf, $len) = @_;
return { headers => hunpack($ctx, $buf, $len) };
}
sub data {
my ($ctx, $buf, $len) = @_;
return { data => substr($buf, 0, $len) };
}
sub settings {
my ($ctx, $buf, $len) = @_;
my %payload;
my $skip = 0;
for (1 .. $len / 6) {
my $id = hex unpack "\@$skip n", $buf; $skip += 2;
$payload{$id} = unpack "\@$skip N", $buf; $skip += 4;
}
return \%payload;
}
sub ping {
my ($ctx, $buf, $len) = @_;
return { value => unpack "A$len", $buf };
}
sub rst_stream {
my ($ctx, $buf, $len) = @_;
return { code => unpack "N", $buf };
}
sub goaway {
my ($ctx, $buf, $len) = @_;
my %payload;
my $stream = unpack "B32", $buf;
substr($stream, 0, 1) = 0;
$stream = unpack("N", pack("B32", $stream));
$payload{last_sid} = $stream;
$len -= 4;
$payload{code} = unpack "x4 N", $buf;
$payload{debug} = unpack "x8 A$len", $buf;
return \%payload;
}
sub window_update {
my ($ctx, $buf, $len) = @_;
my $value = unpack "B32", $buf;
substr($value, 0, 1) = 0;
return { wdelta => unpack("N", pack("B32", $value)) };
}
sub pack_length {
pack 'c3', unpack 'xc3', pack 'N', $_[0];
}
sub unpack_length {
unpack 'N', pack 'xc3', unpack 'c3', $_[0];
}
sub raw_read {
my ($s, $buf, $len) = @_;
my $got = '';
while (length($buf) < $len && IO::Select->new($s)->can_read(1)) {
$s->sysread($got, 16384) or last;
log_in($got);
$buf .= $got;
}
return $buf;
}
sub raw_write {
my ($s, $message) = @_;
local $SIG{PIPE} = 'IGNORE';
while (IO::Select->new($s)->can_write(0.4)) {
log_out($message);
my $n = $s->syswrite($message);
last unless $n;
$message = substr($message, $n);
last unless length $message;
}
}
sub new_session {
my ($port, %extra) = @_;
my ($s);
$s = new_socket($port, %extra);
if ($extra{proxy}) {
raw_write($s, $extra{proxy});
}
# preface
raw_write($s, 'PRI * HTTP/2.0' . CRLF . CRLF . 'SM' . CRLF . CRLF);
return { socket => $s, last_stream => -1,
dynamic_encode => [ static_table() ],
dynamic_decode => [ static_table() ],
static_table_size => scalar @{[static_table()]} };
}
sub new_socket {
my ($port, %extra) = @_;
my $npn = $extra{'npn'};
my $alpn = $extra{'alpn'};
my $s;
$port = 8080 unless defined $port;
eval {
local $SIG{ALRM} = sub { die "timeout\n" };
local $SIG{PIPE} = sub { die "sigpipe\n" };
alarm(2);
$s = IO::Socket::INET->new(
Proto => 'tcp',
PeerAddr => "127.0.0.1:$port",
);
IO::Socket::SSL->start_SSL($s,
SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(),
SSL_npn_protocols => $npn ? [ $npn ] : undef,
SSL_alpn_protocols => $alpn ? [ $alpn ] : undef,
SSL_error_trap => sub { die $_[1] }
) if $extra{'SSL'};
alarm(0);
};
alarm(0);
if ($@) {
log_in("died: $@");
return undef;
}
return $s;
}
sub static_table {
[ '', '' ], # unused
[ ':authority', '' ],
[ ':method', 'GET' ],
[ ':method', 'POST' ],
[ ':path', '/' ],
[ ':path', '/index.html' ],
[ ':scheme', 'http' ],
[ ':scheme', 'https' ],
[ ':status', '200' ],
[ ':status', '204' ],
[ ':status', '206' ],
[ ':status', '304' ],
[ ':status', '400' ],
[ ':status', '404' ],
[ ':status', '500' ],
[ 'accept-charset', '' ],
[ 'accept-encoding', 'gzip, deflate' ],
[ 'accept-language', '' ],
[ 'accept-ranges', '' ],
[ 'accept', '' ],
[ 'access-control-allow-origin',
'' ],
[ 'age', '' ],
[ 'allow', '' ],
[ 'authorization', '' ],
[ 'cache-control', '' ],
[ 'content-disposition',
'' ],
[ 'content-encoding', '' ],
[ 'content-language', '' ],
[ 'content-length', '' ],
[ 'content-location', '' ],
[ 'content-range', '' ],
[ 'content-type', '' ],
[ 'cookie', '' ],
[ 'date', '' ],
[ 'etag', '' ],
[ 'expect', '' ],
[ 'expires', '' ],
[ 'from', '' ],
[ 'host', '' ],
[ 'if-match', '' ],
[ 'if-modified-since', '' ],
[ 'if-none-match', '' ],
[ 'if-range', '' ],
[ 'if-unmodified-since',
'' ],
[ 'last-modified', '' ],
[ 'link', '' ],
[ 'location', '' ],
[ 'max-forwards', '' ],
[ 'proxy-authenticate', '' ],
[ 'proxy-authorization',
'' ],
[ 'range', '' ],
[ 'referer', '' ],
[ 'refresh', '' ],
[ 'retry-after', '' ],
[ 'server', '' ],
[ 'set-cookie', '' ],
[ 'strict-transport-security',
'' ],
[ 'transfer-encoding', '' ],
[ 'user-agent', '' ],
[ 'vary', '' ],
[ 'via', '' ],
[ 'www-authenticate', '' ],
}
# RFC 7541, 5.1. Integer Representation
sub ipack {
my ($base, $d) = @_;
return sprintf("%.*b", $base, $d) if $d < 2**$base - 1;
my $o = sprintf("%${base}b", 2**$base - 1);
$d -= 2**$base - 1;
while ($d >= 128) {
$o .= sprintf("%8b", $d % 128 + 128);
$d /= 128;
}
$o .= sprintf("%08b", $d);
return $o;
}
sub iunpack {
my ($base, $b, $s) = @_;
my $len = unpack("\@$s B8", $b); $s++;
my $prefix = substr($len, 0, 8 - $base);
$len = '0' x (8 - $base) . substr($len, 8 - $base);
$len = unpack("C", pack("B8", $len));
return ($len, $s, $prefix) if $len < 2**$base - 1;
my $m = 0;
my $d;
do {
$d = unpack("\@$s C", $b); $s++;
$len += ($d & 127) * 2**$m;
$m += $base;
} while (($d & 128) == 128);
return ($len, $s, $prefix);
}
sub hpack {
my ($ctx, $name, $value, %extra) = @_;
my $table = $ctx->{dynamic_encode};
my $mode = defined $extra{mode} ? $extra{mode} : 1;
my $huff = $extra{huff};
my ($index, $buf) = 0;
# 6.1. Indexed Header Field Representation
if ($mode == 0) {
++$index until $index > $#$table
or $table->[$index][0] eq $name
and $table->[$index][1] eq $value;
$buf = pack('B*', '1' . ipack(7, $index));
}
# 6.2.1. Literal Header Field with Incremental Indexing
if ($mode == 1) {
splice @$table, $ctx->{static_table_size}, 0, [ $name, $value ];
++$index until $index > $#$table
or $table->[$index][0] eq $name;
my $value = $huff ? huff($value) : $value;
$buf = pack('B*', '01' . ipack(6, $index)
. ($huff ? '1' : '0') . ipack(7, length($value)));
$buf .= $value;
}
# 6.2.1. Literal Header Field with Incremental Indexing -- New Name
if ($mode == 2) {
splice @$table, $ctx->{static_table_size}, 0, [ $name, $value ];
my $name = $huff ? huff($name) : $name;
my $value = $huff ? huff($value) : $value;
my $hbit = ($huff ? '1' : '0');
$buf = pack('B*', '01000000');
$buf .= pack('B*', $hbit . ipack(7, length($name)));
$buf .= $name;
$buf .= pack('B*', $hbit . ipack(7, length($value)));
$buf .= $value;
}
# 6.2.2. Literal Header Field without Indexing
if ($mode == 3) {
++$index until $index > $#$table
or $table->[$index][0] eq $name;
my $value = $huff ? huff($value) : $value;
$buf = pack('B*', '0000' . ipack(4, $index)
. ($huff ? '1' : '0') . ipack(7, length($value)));
$buf .= $value;
}
# 6.2.2. Literal Header Field without Indexing -- New Name
if ($mode == 4) {
my $name = $huff ? huff($name) : $name;
my $value = $huff ? huff($value) : $value;
my $hbit = ($huff ? '1' : '0');
$buf = pack('B*', '00000000');
$buf .= pack('B*', $hbit . ipack(7, length($name)));
$buf .= $name;
$buf .= pack('B*', $hbit . ipack(7, length($value)));
$buf .= $value;
}
# 6.2.3. Literal Header Field Never Indexed
if ($mode == 5) {
++$index until $index > $#$table
or $table->[$index][0] eq $name;
my $value = $huff ? huff($value) : $value;
$buf = pack('B*', '0001' . ipack(4, $index)
. ($huff ? '1' : '0') . ipack(7, length($value)));
$buf .= $value;
}
# 6.2.3. Literal Header Field Never Indexed -- New Name
if ($mode == 6) {
my $name = $huff ? huff($name) : $name;
my $value = $huff ? huff($value) : $value;
my $hbit = ($huff ? '1' : '0');
$buf = pack('B*', '00010000');
$buf .= pack('B*', $hbit . ipack(7, length($name)));
$buf .= $name;
$buf .= pack('B*', $hbit . ipack(7, length($value)));
$buf .= $value;
}
return $buf;
}
sub hunpack {
my ($ctx, $data, $length) = @_;
my $table = $ctx->{dynamic_decode};
my %headers;
my $skip = 0;
my ($index, $name, $value);
sub field {
my ($b) = @_;
my ($len, $s, $huff) = iunpack(7, @_);
my $field = substr($b, $s, $len);
$field = $huff ? dehuff($field) : $field;
$s += $len;
return ($field, $s);
}
sub add {
my ($h, $n, $v) = @_;
return $h->{$n} = $v unless exists $h->{$n};
$h->{$n} = [ $h->{$n} ];
push @{$h->{$n}}, $v;
}
while ($skip < $length) {
my $ib = unpack("\@$skip B8", $data);
if (substr($ib, 0, 1) eq '1') {
($index, $skip) = iunpack(7, $data, $skip);
add(\%headers,
$table->[$index][0], $table->[$index][1]);
next;
}
if (substr($ib, 0, 2) eq '01') {
($index, $skip) = iunpack(6, $data, $skip);
$name = $table->[$index][0];
($name, $skip) = field($data, $skip) unless $name;
($value, $skip) = field($data, $skip);
splice @$table,
$ctx->{static_table_size}, 0, [ $name, $value ];
add(\%headers, $name, $value);
next;
}
if (substr($ib, 0, 4) eq '0000') {
($index, $skip) = iunpack(4, $data, $skip);
$name = $table->[$index][0];
($name, $skip) = field($data, $skip) unless $name;
($value, $skip) = field($data, $skip);
add(\%headers, $name, $value);
next;
}
}
return \%headers;
}
sub huff_code { scalar {
pack('C', 0) => '1111111111000',
pack('C', 1) => '11111111111111111011000',
pack('C', 2) => '1111111111111111111111100010',
pack('C', 3) => '1111111111111111111111100011',
pack('C', 4) => '1111111111111111111111100100',
pack('C', 5) => '1111111111111111111111100101',
pack('C', 6) => '1111111111111111111111100110',
pack('C', 7) => '1111111111111111111111100111',
pack('C', 8) => '1111111111111111111111101000',
pack('C', 9) => '111111111111111111101010',
pack('C', 10) => '111111111111111111111111111100',
pack('C', 11) => '1111111111111111111111101001',
pack('C', 12) => '1111111111111111111111101010',
pack('C', 13) => '111111111111111111111111111101',
pack('C', 14) => '1111111111111111111111101011',
pack('C', 15) => '1111111111111111111111101100',
pack('C', 16) => '1111111111111111111111101101',
pack('C', 17) => '1111111111111111111111101110',
pack('C', 18) => '1111111111111111111111101111',
pack('C', 19) => '1111111111111111111111110000',
pack('C', 20) => '1111111111111111111111110001',
pack('C', 21) => '1111111111111111111111110010',
pack('C', 22) => '111111111111111111111111111110',
pack('C', 23) => '1111111111111111111111110011',
pack('C', 24) => '1111111111111111111111110100',
pack('C', 25) => '1111111111111111111111110101',
pack('C', 26) => '1111111111111111111111110110',
pack('C', 27) => '1111111111111111111111110111',
pack('C', 28) => '1111111111111111111111111000',
pack('C', 29) => '1111111111111111111111111001',
pack('C', 30) => '1111111111111111111111111010',
pack('C', 31) => '1111111111111111111111111011',
pack('C', 32) => '010100',
pack('C', 33) => '1111111000',
pack('C', 34) => '1111111001',
pack('C', 35) => '111111111010',
pack('C', 36) => '1111111111001',
pack('C', 37) => '010101',
pack('C', 38) => '11111000',
pack('C', 39) => '11111111010',
pack('C', 40) => '1111111010',
pack('C', 41) => '1111111011',
pack('C', 42) => '11111001',
pack('C', 43) => '11111111011',
pack('C', 44) => '11111010',
pack('C', 45) => '010110',
pack('C', 46) => '010111',
pack('C', 47) => '011000',
pack('C', 48) => '00000',
pack('C', 49) => '00001',
pack('C', 50) => '00010',
pack('C', 51) => '011001',
pack('C', 52) => '011010',
pack('C', 53) => '011011',
pack('C', 54) => '011100',
pack('C', 55) => '011101',
pack('C', 56) => '011110',
pack('C', 57) => '011111',
pack('C', 58) => '1011100',
pack('C', 59) => '11111011',
pack('C', 60) => '111111111111100',
pack('C', 61) => '100000',
pack('C', 62) => '111111111011',
pack('C', 63) => '1111111100',
pack('C', 64) => '1111111111010',
pack('C', 65) => '100001',
pack('C', 66) => '1011101',
pack('C', 67) => '1011110',
pack('C', 68) => '1011111',
pack('C', 69) => '1100000',
pack('C', 70) => '1100001',
pack('C', 71) => '1100010',
pack('C', 72) => '1100011',
pack('C', 73) => '1100100',
pack('C', 74) => '1100101',
pack('C', 75) => '1100110',
pack('C', 76) => '1100111',
pack('C', 77) => '1101000',
pack('C', 78) => '1101001',
pack('C', 79) => '1101010',
pack('C', 80) => '1101011',
pack('C', 81) => '1101100',
pack('C', 82) => '1101101',
pack('C', 83) => '1101110',
pack('C', 84) => '1101111',
pack('C', 85) => '1110000',
pack('C', 86) => '1110001',
pack('C', 87) => '1110010',
pack('C', 88) => '11111100',
pack('C', 89) => '1110011',
pack('C', 90) => '11111101',
pack('C', 91) => '1111111111011',
pack('C', 92) => '1111111111111110000',
pack('C', 93) => '1111111111100',
pack('C', 94) => '11111111111100',
pack('C', 95) => '100010',
pack('C', 96) => '111111111111101',
pack('C', 97) => '00011',
pack('C', 98) => '100011',
pack('C', 99) => '00100',
pack('C', 100) => '100100',
pack('C', 101) => '00101',
pack('C', 102) => '100101',
pack('C', 103) => '100110',
pack('C', 104) => '100111',
pack('C', 105) => '00110',
pack('C', 106) => '1110100',
pack('C', 107) => '1110101',
pack('C', 108) => '101000',
pack('C', 109) => '101001',
pack('C', 110) => '101010',
pack('C', 111) => '00111',
pack('C', 112) => '101011',
pack('C', 113) => '1110110',
pack('C', 114) => '101100',
pack('C', 115) => '01000',
pack('C', 116) => '01001',
pack('C', 117) => '101101',
pack('C', 118) => '1110111',
pack('C', 119) => '1111000',
pack('C', 120) => '1111001',
pack('C', 121) => '1111010',
pack('C', 122) => '1111011',
pack('C', 123) => '111111111111110',
pack('C', 124) => '11111111100',
pack('C', 125) => '11111111111101',
pack('C', 126) => '1111111111101',
pack('C', 127) => '1111111111111111111111111100',
pack('C', 128) => '11111111111111100110',
pack('C', 129) => '1111111111111111010010',
pack('C', 130) => '11111111111111100111',
pack('C', 131) => '11111111111111101000',
pack('C', 132) => '1111111111111111010011',
pack('C', 133) => '1111111111111111010100',
pack('C', 134) => '1111111111111111010101',
pack('C', 135) => '11111111111111111011001',
pack('C', 136) => '1111111111111111010110',
pack('C', 137) => '11111111111111111011010',
pack('C', 138) => '11111111111111111011011',
pack('C', 139) => '11111111111111111011100',
pack('C', 140) => '11111111111111111011101',
pack('C', 141) => '11111111111111111011110',
pack('C', 142) => '111111111111111111101011',
pack('C', 143) => '11111111111111111011111',
pack('C', 144) => '111111111111111111101100',
pack('C', 145) => '111111111111111111101101',
pack('C', 146) => '1111111111111111010111',
pack('C', 147) => '11111111111111111100000',
pack('C', 148) => '111111111111111111101110',
pack('C', 149) => '11111111111111111100001',
pack('C', 150) => '11111111111111111100010',
pack('C', 151) => '11111111111111111100011',
pack('C', 152) => '11111111111111111100100',
pack('C', 153) => '111111111111111011100',
pack('C', 154) => '1111111111111111011000',
pack('C', 155) => '11111111111111111100101',
pack('C', 156) => '1111111111111111011001',
pack('C', 157) => '11111111111111111100110',
pack('C', 158) => '11111111111111111100111',
pack('C', 159) => '111111111111111111101111',
pack('C', 160) => '1111111111111111011010',
pack('C', 161) => '111111111111111011101',
pack('C', 162) => '11111111111111101001',
pack('C', 163) => '1111111111111111011011',
pack('C', 164) => '1111111111111111011100',
pack('C', 165) => '11111111111111111101000',
pack('C', 166) => '11111111111111111101001',
pack('C', 167) => '111111111111111011110',
pack('C', 168) => '11111111111111111101010',
pack('C', 169) => '1111111111111111011101',
pack('C', 170) => '1111111111111111011110',
pack('C', 171) => '111111111111111111110000',
pack('C', 172) => '111111111111111011111',
pack('C', 173) => '1111111111111111011111',
pack('C', 174) => '11111111111111111101011',
pack('C', 175) => '11111111111111111101100',
pack('C', 176) => '111111111111111100000',
pack('C', 177) => '111111111111111100001',
pack('C', 178) => '1111111111111111100000',
pack('C', 179) => '111111111111111100010',
pack('C', 180) => '11111111111111111101101',
pack('C', 181) => '1111111111111111100001',
pack('C', 182) => '11111111111111111101110',
pack('C', 183) => '11111111111111111101111',
pack('C', 184) => '11111111111111101010',
pack('C', 185) => '1111111111111111100010',
pack('C', 186) => '1111111111111111100011',
pack('C', 187) => '1111111111111111100100',
pack('C', 188) => '11111111111111111110000',
pack('C', 189) => '1111111111111111100101',
pack('C', 190) => '1111111111111111100110',
pack('C', 191) => '11111111111111111110001',
pack('C', 192) => '11111111111111111111100000',
pack('C', 193) => '11111111111111111111100001',
pack('C', 194) => '11111111111111101011',
pack('C', 195) => '1111111111111110001',
pack('C', 196) => '1111111111111111100111',
pack('C', 197) => '11111111111111111110010',
pack('C', 198) => '1111111111111111101000',
pack('C', 199) => '1111111111111111111101100',
pack('C', 200) => '11111111111111111111100010',
pack('C', 201) => '11111111111111111111100011',
pack('C', 202) => '11111111111111111111100100',
pack('C', 203) => '111111111111111111111011110',
pack('C', 204) => '111111111111111111111011111',
pack('C', 205) => '11111111111111111111100101',
pack('C', 206) => '111111111111111111110001',
pack('C', 207) => '1111111111111111111101101',
pack('C', 208) => '1111111111111110010',
pack('C', 209) => '111111111111111100011',
pack('C', 210) => '11111111111111111111100110',
pack('C', 211) => '111111111111111111111100000',
pack('C', 212) => '111111111111111111111100001',
pack('C', 213) => '11111111111111111111100111',
pack('C', 214) => '111111111111111111111100010',
pack('C', 215) => '111111111111111111110010',
pack('C', 216) => '111111111111111100100',
pack('C', 217) => '111111111111111100101',
pack('C', 218) => '11111111111111111111101000',
pack('C', 219) => '11111111111111111111101001',
pack('C', 220) => '1111111111111111111111111101',
pack('C', 221) => '111111111111111111111100011',
pack('C', 222) => '111111111111111111111100100',
pack('C', 223) => '111111111111111111111100101',
pack('C', 224) => '11111111111111101100',
pack('C', 225) => '111111111111111111110011',
pack('C', 226) => '11111111111111101101',
pack('C', 227) => '111111111111111100110',
pack('C', 228) => '1111111111111111101001',
pack('C', 229) => '111111111111111100111',
pack('C', 230) => '111111111111111101000',
pack('C', 231) => '11111111111111111110011',
pack('C', 232) => '1111111111111111101010',
pack('C', 233) => '1111111111111111101011',
pack('C', 234) => '1111111111111111111101110',
pack('C', 235) => '1111111111111111111101111',
pack('C', 236) => '111111111111111111110100',
pack('C', 237) => '111111111111111111110101',
pack('C', 238) => '11111111111111111111101010',
pack('C', 239) => '11111111111111111110100',
pack('C', 240) => '11111111111111111111101011',
pack('C', 241) => '111111111111111111111100110',
pack('C', 242) => '11111111111111111111101100',
pack('C', 243) => '11111111111111111111101101',
pack('C', 244) => '111111111111111111111100111',
pack('C', 245) => '111111111111111111111101000',
pack('C', 246) => '111111111111111111111101001',
pack('C', 247) => '111111111111111111111101010',
pack('C', 248) => '111111111111111111111101011',
pack('C', 249) => '1111111111111111111111111110',
pack('C', 250) => '111111111111111111111101100',
pack('C', 251) => '111111111111111111111101101',
pack('C', 252) => '111111111111111111111101110',
pack('C', 253) => '111111111111111111111101111',
pack('C', 254) => '111111111111111111111110000',
pack('C', 255) => '11111111111111111111101110',
'_eos' => '111111111111111111111111111111',
}};
sub huff {
my ($string) = @_;
my $code = &huff_code;
my $ret = join '', map { $code->{$_} } (split //, $string);
my $len = length($ret) + (8 - length($ret) % 8);
$ret .= $code->{_eos};
return pack("B$len", $ret);
}
sub dehuff {
my ($string) = @_;
my $code = &huff_code;
my %decode = reverse %$code;
my $ret = ''; my $c = '';
for (split //, unpack('B*', $string)) {
$c .= $_;
next unless exists $decode{$c};
last if $decode{$c} eq '_eos';
$ret .= $decode{$c};
$c = '';
}
return $ret;
}
###############################################################################
sub gunzip_like {
my ($in, $re, $name) = @_;
SKIP: {
eval { require IO::Uncompress::Gunzip; };
Test::More::skip(
"IO::Uncompress::Gunzip not installed", 1) if $@;
my $out;
IO::Uncompress::Gunzip::gunzip(\$in => \$out);
like($out, $re, $name);
}
}
###############################################################################
# for tests with multiple header fields
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;
}
}
###############################################################################