| #!/usr/bin/perl |
| |
| # (C) Sergey Kandaurov |
| # (C) Nginx, Inc. |
| |
| # Tests for HTTP/2 protocol [RFC7540]. |
| |
| ############################################################################### |
| |
| use warnings; |
| use strict; |
| |
| use Test::More; |
| |
| use Socket qw/ CRLF /; |
| |
| BEGIN { use FindBin; chdir($FindBin::Bin); } |
| |
| use lib 'lib'; |
| use Test::Nginx; |
| use Test::Nginx::HTTP2; |
| |
| ############################################################################### |
| |
| select STDERR; $| = 1; |
| select STDOUT; $| = 1; |
| |
| my $t = Test::Nginx->new()->has(qw/http http_v2 proxy rewrite charset gzip/) |
| ->plan(150); |
| |
| $t->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; |
| server_name localhost; |
| |
| 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 /gzip.html { |
| gzip on; |
| gzip_min_length 0; |
| gzip_vary on; |
| alias %%TESTDIR%%/t2.html; |
| } |
| location /frame_size { |
| http2_chunk_size 64k; |
| alias %%TESTDIR%%; |
| output_buffers 2 1m; |
| } |
| 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 /charset { |
| charset utf-8; |
| return 200; |
| } |
| } |
| |
| server { |
| listen 127.0.0.1:8082 http2; |
| server_name localhost; |
| return 200 first; |
| } |
| |
| server { |
| listen 127.0.0.1:8082 http2; |
| server_name localhost2; |
| return 200 second; |
| } |
| |
| server { |
| listen 127.0.0.1:8083 http2; |
| server_name localhost; |
| |
| http2_max_concurrent_streams 1; |
| } |
| |
| server { |
| listen 127.0.0.1:8084 http2; |
| server_name localhost; |
| |
| http2_recv_timeout 1s; |
| client_header_timeout 1s; |
| send_timeout 1s; |
| } |
| |
| server { |
| listen 127.0.0.1:8085 http2; |
| server_name localhost; |
| |
| http2_idle_timeout 1s; |
| client_body_timeout 1s; |
| |
| location /proxy2/ { |
| add_header X-Body $request_body; |
| proxy_pass http://127.0.0.1:8081/; |
| } |
| } |
| |
| server { |
| listen 127.0.0.1:8086 http2; |
| server_name localhost; |
| |
| send_timeout 1s; |
| } |
| |
| server { |
| listen 127.0.0.1:8087 http2; |
| server_name localhost; |
| |
| client_header_timeout 1s; |
| client_body_timeout 1s; |
| |
| location /proxy/ { |
| proxy_pass http://127.0.0.1:8081/; |
| } |
| } |
| } |
| |
| EOF |
| |
| $t->run(); |
| |
| # 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 .. 500000))); |
| |
| $t->write_file('t2.html', 'SEE-THIS'); |
| |
| ############################################################################### |
| |
| # Upgrade mechanism |
| |
| my $r = http(<<EOF); |
| GET / HTTP/1.1 |
| Host: localhost |
| Connection: Upgrade, HTTP2-Settings |
| Upgrade: h2c |
| HTTP2-Settings: AAMAAABkAAQAAP__ |
| |
| EOF |
| |
| SKIP: { |
| skip 'no Upgrade-based negotiation', 2 if $r !~ m!HTTP/1.1 101!; |
| |
| like($r, qr!Connection: Upgrade!, 'upgrade - connection'); |
| like($r, qr!Upgrade: h2c!, 'upgrade - token'); |
| |
| } |
| |
| # SETTINGS |
| |
| my $s = Test::Nginx::HTTP2->new(port(8080), pure => 1); |
| my $frames = $s->read(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'); |
| |
| $s->h2_settings(1); |
| $s->h2_settings(0); |
| |
| $frames = $s->read(all => [{ type => 'SETTINGS' }]); |
| |
| ($frame) = grep { $_->{type} eq 'SETTINGS' } @$frames; |
| ok($frame, 'SETTINGS frame ack'); |
| is($frame->{flags}, 1, 'SETTINGS flags ack'); |
| |
| # SETTINGS - no ack on PROTOCOL_ERROR |
| |
| $s = Test::Nginx::HTTP2->new(port(8080), pure => 1); |
| $frames = $s->read(all => [ |
| { type => 'WINDOW_UPDATE' }, |
| { type => 'SETTINGS'} |
| ]); |
| |
| $s->h2_settings(1); |
| $s->h2_settings(0, 0x5 => 42); |
| |
| $frames = $s->read(all => [ |
| { type => 'SETTINGS'}, |
| { type => 'GOAWAY' } |
| ]); |
| |
| ($frame) = grep { $_->{type} eq 'SETTINGS' } @$frames; |
| is($frame, undef, 'SETTINGS PROTOCOL_ERROR - no ack'); |
| |
| ($frame) = grep { $_->{type} eq 'GOAWAY' } @$frames; |
| ok($frame, 'SETTINGS PROTOCOL_ERROR - GOAWAY'); |
| |
| # PING |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $s->h2_ping('SEE-THIS'); |
| $frames = $s->read(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'); |
| |
| # timeouts |
| |
| SKIP: { |
| skip 'long tests', 6 unless $ENV{TEST_NGINX_UNSAFE}; |
| |
| push my @s, Test::Nginx::HTTP2->new(port(8084), pure => 1); |
| push @s, Test::Nginx::HTTP2->new(port(8084), pure => 1); |
| $s[-1]->h2_ping('SEE-THIS'); |
| push @s, Test::Nginx::HTTP2->new(port(8085), pure => 1); |
| push @s, Test::Nginx::HTTP2->new(port(8085), pure => 1); |
| $s[-1]->h2_ping('SEE-THIS'); |
| |
| select undef, undef, undef, 2.1; |
| |
| $frames = (shift @s)->read(all => [{ type => "GOAWAY" }]); |
| ($frame) = grep { $_->{type} eq "GOAWAY" } @$frames; |
| ok($frame, 'recv timeout - new connection GOAWAY'); |
| is($frame->{code}, 1, 'recv timeout - new connection code'); |
| |
| $frames = (shift @s)->read(all => [{ type => "GOAWAY" }]); |
| ($frame) = grep { $_->{type} eq "GOAWAY" } @$frames; |
| is($frame, undef, 'recv timeout - idle connection GOAWAY'); |
| |
| $frames = (shift @s)->read(all => [{ type => "GOAWAY" }]); |
| ($frame) = grep { $_->{type} eq "GOAWAY" } @$frames; |
| is($frame, undef, 'idle timeout - new connection GOAWAY'); |
| |
| $frames = (shift @s)->read(all => [{ type => "GOAWAY" }]); |
| ($frame) = grep { $_->{type} eq "GOAWAY" } @$frames; |
| ok($frame, 'idle timeout - idle connection GOAWAY'); |
| is($frame->{code}, 0, 'idle timeout - idle connection code'); |
| |
| } |
| |
| # GOAWAY |
| |
| Test::Nginx::HTTP2->new()->h2_goaway(0, 0, 5); |
| Test::Nginx::HTTP2->new()->h2_goaway(0, 0, 5, 'foobar'); |
| Test::Nginx::HTTP2->new()->h2_goaway(0, 0, 5, 'foobar', split => [ 8, 8, 4 ]); |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $s->h2_goaway(0, 0, 5); |
| $s->h2_goaway(0, 0, 5); |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $s->h2_goaway(0, 0, 5, 'foobar', len => 0); |
| $frames = $s->read(all => [{ type => "GOAWAY" }]); |
| |
| ($frame) = grep { $_->{type} eq "GOAWAY" } @$frames; |
| ok($frame, 'GOAWAY invalid length - GOAWAY frame'); |
| is($frame->{code}, 6, 'GOAWAY invalid length - GOAWAY FRAME_SIZE_ERROR'); |
| |
| # 6.8. GOAWAY |
| # An endpoint MUST treat a GOAWAY frame with a stream identifier other |
| # than 0x0 as a connection error (Section 5.4.1) of type PROTOCOL_ERROR. |
| |
| TODO: { |
| local $TODO = 'not yet'; |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $s->h2_goaway(1, 0, 5, 'foobar'); |
| $frames = $s->read(all => [{ type => "GOAWAY" }], wait => 0.5); |
| |
| ($frame) = grep { $_->{type} eq "GOAWAY" } @$frames; |
| ok($frame, 'GOAWAY invalid stream - GOAWAY frame'); |
| is($frame->{code}, 1, 'GOAWAY invalid stream - GOAWAY PROTOCOL_ERROR'); |
| |
| } |
| |
| # client-initiated PUSH_PROMISE, just to ensure nothing went wrong |
| # N.B. other implementation returns zero code, which is not anyhow regulated |
| |
| $s = Test::Nginx::HTTP2->new(); |
| { |
| local $SIG{PIPE} = 'IGNORE'; |
| syswrite($s->{socket}, pack("x2C2xN", 4, 0x5, 1)); |
| } |
| $frames = $s->read(all => [{ type => "GOAWAY" }]); |
| |
| ($frame) = grep { $_->{type} eq "GOAWAY" } @$frames; |
| ok($frame, 'client-initiated PUSH_PROMISE - GOAWAY frame'); |
| is($frame->{code}, 1, 'client-initiated PUSH_PROMISE - GOAWAY PROTOCOL_ERROR'); |
| |
| # GET |
| |
| $s = Test::Nginx::HTTP2->new(); |
| my $sid = $s->new_stream(); |
| $frames = $s->read(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 = $s->new_stream(); |
| $frames = $s->read(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'); |
| |
| # HEAD |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $sid = $s->new_stream({ method => 'HEAD' }); |
| $frames = $s->read(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'); |
| |
| # range filter |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $sid = $s->new_stream({ headers => [ |
| { name => ':method', value => 'GET', mode => 0 }, |
| { name => ':scheme', value => 'http', mode => 0 }, |
| { name => ':path', value => '/t1.html', mode => 1 }, |
| { name => ':authority', value => 'localhost', mode => 1 }, |
| { name => 'range', value => 'bytes=10-19', mode => 1 }]}); |
| $frames = $s->read(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_chunk_size=1 |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $sid = $s->new_stream({ path => '/chunk_size' }); |
| $frames = $s->read(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 |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $sid = $s->new_stream({ 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 }]}); |
| $s->h2_continue($sid, { continuation => 1, headers => [ |
| { name => 'x-foo', value => 'X-Bar', mode => 2 }]}); |
| $s->h2_continue($sid, { headers => [ |
| { name => 'referer', value => 'foo', mode => 2 }]}); |
| $frames = $s->read(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 |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $sid = $s->new_stream({ 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 = $s->read(all => [{ sid => $sid, fin => 1 }]); |
| |
| ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; |
| is($frame->{headers}->{':status'}, 200, 'CONTINUATION - in header field'); |
| |
| # CONTINUATION on a closed stream |
| |
| $s->h2_continue(1, { headers => [ |
| { name => 'x-foo', value => 'X-Bar', mode => 2 }]}); |
| $frames = $s->read(all => [{ sid => 1, fin => 1 }]); |
| |
| ($frame) = grep { $_->{type} eq "GOAWAY" } @$frames; |
| is($frame->{type}, 'GOAWAY', 'GOAWAY - CONTINUATION closed stream'); |
| is($frame->{code}, 1, 'GOAWAY - CONTINUATION closed stream - PROTOCOL_ERROR'); |
| |
| # frame padding |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $sid = $s->new_stream({ 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 = $s->read(all => [{ sid => $sid, fin => 1 }]); |
| |
| ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; |
| is($frame->{headers}->{':status'}, 200, 'padding - HEADERS status'); |
| |
| $sid = $s->new_stream({ headers => [ |
| { name => ':method', value => 'GET', mode => 0 }, |
| { name => ':scheme', value => 'http', mode => 0 }, |
| { name => ':path', value => '/', mode => 0 }, |
| { name => ':authority', value => 'localhost', mode => 1 }]}); |
| $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); |
| |
| ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; |
| is($frame->{headers}->{':status'}, 200, 'padding - next stream'); |
| |
| # padding followed by CONTINUATION |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $sid = $s->new_stream({ padding => 42, continuation => [ 2, 4, 1, 5 ], |
| headers => [ |
| { name => ':method', value => 'GET', mode => 1 }, |
| { name => ':scheme', value => 'http', mode => 0 }, |
| { name => ':path', value => '/', mode => 0 }, |
| { name => ':authority', value => 'localhost', mode => 1 }]}); |
| $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); |
| |
| ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; |
| is($frame->{headers}->{':status'}, 200, 'padding - CONTINUATION'); |
| |
| # internal redirect |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $sid = $s->new_stream({ path => '/redirect' }); |
| $frames = $s->read(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 |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $sid = $s->new_stream({ path => '/return301_absolute' }); |
| $frames = $s->read(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 |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $sid = $s->new_stream({ path => '/return301_relative' }); |
| $frames = $s->read(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://localhost:' . port(8080) . '/', |
| 'return 301 relative - location'); |
| |
| # return 301 with relative URI and ':authority' request header field |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $sid = $s->new_stream({ headers => [ |
| { name => ':method', value => 'GET', mode => 0 }, |
| { name => ':scheme', value => 'http', mode => 0 }, |
| { name => ':path', value => '/return301_relative', mode => 2 }, |
| { name => ':authority', value => 'localhost', mode => 2 }]}); |
| $frames = $s->read(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:' . port(8080) . '/', |
| 'return 301 relative - authority - location'); |
| |
| # return 301 with relative URI and 'host' request header field |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $sid = $s->new_stream({ headers => [ |
| { name => ':method', value => 'GET', mode => 0 }, |
| { name => ':scheme', value => 'http', mode => 0 }, |
| { name => ':path', value => '/return301_relative', mode => 2 }, |
| { name => 'host', value => 'localhost', mode => 2 }]}); |
| $frames = $s->read(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:' . port(8080) . '/', |
| 'return 301 relative - host - location'); |
| |
| # virtual host |
| |
| $s = Test::Nginx::HTTP2->new(port(8082)); |
| $sid = $s->new_stream({ 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 = $s->read(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 = $s->new_stream({ headers => [ |
| { name => ':method', value => 'GET', mode => 0 }, |
| { name => ':scheme', value => 'http', mode => 0 }, |
| { name => ':path', value => '/', mode => 0 }, |
| { name => ':authority', value => 'localhost', mode => 2 }]}); |
| $frames = $s->read(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 = $s->new_stream({ 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 = $s->read(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 = $s->new_stream({ 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 = $s->read(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 |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $sid = $s->new_stream({ headers => [ |
| { name => ':method', value => 'GET', mode => 0 }, |
| { name => ':scheme', value => 'http', mode => 0 }, |
| { name => ':path', value => '/gzip.html' }, |
| { name => ':authority', value => 'localhost', mode => 1 }, |
| { name => 'accept-encoding', value => 'gzip' }]}); |
| $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); |
| |
| ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; |
| is($frame->{headers}->{'content-encoding'}, 'gzip', 'gzip - encoding'); |
| is($frame->{headers}->{'vary'}, 'Accept-Encoding', 'gzip - vary'); |
| |
| ($frame) = grep { $_->{type} eq "DATA" } @$frames; |
| gunzip_like($frame->{data}, qr/^SEE-THIS\Z/, 'gzip - DATA'); |
| |
| # charset |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $sid = $s->new_stream({ path => '/charset' }); |
| $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); |
| |
| ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; |
| is($frame->{headers}->{'content-type'}, 'text/plain; charset=utf-8', 'charset'); |
| |
| # partial request header frame received (field split), |
| # the rest of frame is received after client header timeout |
| |
| $s = Test::Nginx::HTTP2->new(port(8087)); |
| $sid = $s->new_stream({ path => '/t2.html', split => [35], |
| split_delay => 2.1 }); |
| $frames = $s->read(all => [{ type => 'RST_STREAM' }]); |
| |
| ($frame) = grep { $_->{type} eq "RST_STREAM" } @$frames; |
| ok($frame, 'client header timeout'); |
| is($frame->{code}, 1, 'client header timeout - protocol error'); |
| |
| $s->h2_ping('SEE-THIS'); |
| $frames = $s->read(all => [{ type => 'PING' }]); |
| |
| ($frame) = grep { $_->{type} eq "PING" && $_->{flags} & 0x1 } @$frames; |
| ok($frame, 'client header timeout - PING'); |
| |
| # partial request header frame received (no field split), |
| # the rest of frame is received after client header timeout |
| |
| $s = Test::Nginx::HTTP2->new(port(8087)); |
| $sid = $s->new_stream({ path => '/t2.html', split => [20], split_delay => 2.1 }); |
| $frames = $s->read(all => [{ type => 'RST_STREAM' }]); |
| |
| TODO: { |
| local $TODO = 'not yet' unless $t->has_version('1.17.9'); |
| |
| ($frame) = grep { $_->{type} eq "RST_STREAM" } @$frames; |
| ok($frame, 'client header timeout 2'); |
| is($frame->{code}, 1, 'client header timeout 2 - protocol error'); |
| |
| } |
| |
| $s->h2_ping('SEE-THIS'); |
| $frames = $s->read(all => [{ type => 'PING' }]); |
| |
| ($frame) = grep { $_->{type} eq "PING" && $_->{flags} & 0x1 } @$frames; |
| ok($frame, 'client header timeout 2 - PING'); |
| |
| # partial request body data frame received, the rest is after body timeout |
| |
| $s = Test::Nginx::HTTP2->new(port(8087)); |
| $sid = $s->new_stream({ path => '/proxy/t2.html', body_more => 1 }); |
| $s->h2_body('TEST', { split => [10], split_delay => 2.1 }); |
| $frames = $s->read(all => [{ type => 'RST_STREAM' }]); |
| |
| ($frame) = grep { $_->{type} eq "RST_STREAM" } @$frames; |
| ok($frame, 'client body timeout'); |
| is($frame->{code}, 1, 'client body timeout - protocol error'); |
| |
| $s->h2_ping('SEE-THIS'); |
| $frames = $s->read(all => [{ type => 'PING' }]); |
| |
| ($frame) = grep { $_->{type} eq "PING" && $_->{flags} & 0x1 } @$frames; |
| ok($frame, 'client body timeout - PING'); |
| |
| # proxied request with logging pristine request header field (e.g., referer) |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $sid = $s->new_stream({ headers => [ |
| { name => ':method', value => 'GET' }, |
| { name => ':scheme', value => 'http' }, |
| { name => ':path', value => '/proxy2/' }, |
| { name => ':authority', value => 'localhost' }, |
| { name => 'referer', value => 'foo' }]}); |
| $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); |
| |
| ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; |
| is($frame->{headers}->{':status'}, 200, 'proxy with logging request headers'); |
| |
| $sid = $s->new_stream(); |
| $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); |
| |
| ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; |
| ok($frame->{headers}, 'proxy with logging request headers - 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. |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $sid = $s->new_stream({ path => '/t1.html' }); |
| $frames = $s->read(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'); |
| |
| $s->h2_ping('SEE-THIS'); |
| $frames = $s->read(all => [{ type => 'PING' }]); |
| |
| ($frame) = grep { $_->{type} eq "PING" && $_->{flags} & 0x1 } @$frames; |
| ok($frame, 'iws - PING not blocked'); |
| |
| $s->h2_window(2**16, $sid); |
| $frames = $s->read(wait => 0.2); |
| is(@$frames, 0, 'iws - updated stream window'); |
| |
| $s->h2_window(2**16); |
| $frames = $s->read(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. |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $s->h2_settings(0, 0x4 => 2**17); |
| $s->h2_window(2**17); |
| |
| $sid = $s->new_stream({ path => '/t1.html' }); |
| $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); |
| |
| @data = grep { $_->{type} eq "DATA" } @$frames; |
| $sum = eval join '+', map { $_->{length} } @data; |
| is($sum, 2**16 + 80, 'iws - increased'); |
| |
| # INITIAL_WINDOW_SIZE duplicate settings |
| |
| # 6.5. SETTINGS |
| # Each parameter in a SETTINGS frame replaces any existing value for |
| # that parameter. Parameters are processed in the order in which they |
| # appear, and a receiver of a SETTINGS frame does not need to maintain |
| # any state other than the current value of its parameters. Therefore, |
| # the value of a SETTINGS parameter is the last value that is seen by a |
| # receiver. |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $s->h2_window(2**17); |
| |
| $sid = $s->new_stream({ path => '/t1.html' }); |
| |
| $frames = $s->read(all => [{ sid => $sid, length => 2**16 - 1 }]); |
| @data = grep { $_->{type} eq "DATA" } @$frames; |
| $sum = eval join '+', map { $_->{length} } @data; |
| is($sum, 2**16 - 1, 'iws duplicate - default stream window'); |
| |
| # this should effect in extra stream window octect |
| # $s->h2_settings(0, 0x4 => 42, 0x4 => 2**16); |
| { |
| local $SIG{PIPE} = 'IGNORE'; |
| syswrite($s->{socket}, pack("x2C2x5nNnN", 12, 0x4, 4, 42, 4, 2**16)); |
| } |
| |
| $frames = $s->read(all => [{ sid => $sid, length => 1 }]); |
| @data = grep { $_->{type} eq "DATA" } @$frames; |
| $sum = eval join '+', map { $_->{length} } @data; |
| is($sum, 1, 'iws duplicate - updated stream window'); |
| |
| # yet more octets to finish receiving the response |
| |
| $s->h2_settings(0, 0x4 => 2**16 + 80); |
| |
| $frames = $s->read(all => [{ sid => $sid, length => 80 }]); |
| @data = grep { $_->{type} eq "DATA" } @$frames; |
| $sum = eval join '+', map { $_->{length} } @data; |
| is($sum, 80, 'iws duplicate - updated stream window 2'); |
| |
| # 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. |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $sid = $s->new_stream({ path => '/t1.html' }); |
| $s->read(all => [{ sid => $sid, length => 2**16 - 1 }]); |
| |
| $s->h2_window(1); |
| $s->h2_settings(0, 0x4 => 42); |
| $s->h2_window(1024, $sid); |
| |
| $frames = $s->read(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 |
| |
| $s->h2_window(2**16 - 1 - 42 - 1024, $sid); |
| |
| $frames = $s->read(wait => 0.2); |
| is(@$frames, 0, 'zero window - no data'); |
| |
| $s->h2_window(1, $sid); |
| |
| $frames = $s->read(all => [{ sid => $sid, length => 1 }]); |
| is(@$frames, 1, 'positive window'); |
| |
| SKIP: { |
| skip 'failed connection', 2 unless @$frames; |
| |
| is(@$frames[0]->{type}, 'DATA', 'positive window - data'); |
| is(@$frames[0]->{length}, 1, 'positive window - data length'); |
| |
| } |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $s->h2_window(2**30); |
| $s->h2_settings(0, 0x4 => 2**30); |
| |
| $sid = $s->new_stream({ path => '/frame_size/tbig.html' }); |
| |
| sleep 1; |
| $s->h2_settings(0, 0x5 => 2**15); |
| |
| $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); |
| $lengths = join ' ', map { $_->{length} } @$frames; |
| unlike($lengths, qr/16384 0 16384/, 'SETTINGS ack after queued DATA'); |
| |
| # ask write handler in sending large response |
| |
| SKIP: { |
| skip 'unsafe socket tests', 4 unless $ENV{TEST_NGINX_UNSAFE}; |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $sid = $s->new_stream({ path => '/tbig.html' }); |
| |
| $s->h2_window(2**30, $sid); |
| $s->h2_window(2**30); |
| |
| sleep 1; |
| $frames = $s->read(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, 5000000, 'large response - DATA'); |
| |
| # Make sure http2 write handler doesn't break a connection. |
| |
| $sid = $s->new_stream(); |
| $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); |
| |
| ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; |
| is($frame->{headers}->{':status'}, 200, 'new stream after large response'); |
| |
| # write event send timeout |
| |
| $s = Test::Nginx::HTTP2->new(port(8086)); |
| $sid = $s->new_stream({ path => '/tbig.html' }); |
| $s->h2_window(2**30, $sid); |
| $s->h2_window(2**30); |
| |
| select undef, undef, undef, 2.1; |
| |
| $s->h2_ping('SEE-THIS'); |
| |
| $frames = $s->read(all => [{ type => 'PING' }]); |
| ok(!grep ({ $_->{type} eq "PING" } @$frames), 'large response - send timeout'); |
| |
| } |
| |
| # SETTINGS_MAX_FRAME_SIZE |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $sid = $s->new_stream({ path => '/frame_size/t1.html' }); |
| $s->h2_window(2**18, 1); |
| $s->h2_window(2**18); |
| |
| $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); |
| @data = grep { $_->{type} eq "DATA" } @$frames; |
| is($data[0]->{length}, 2**14, 'max frame size - default'); |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $s->h2_settings(0, 0x5 => 2**15); |
| $sid = $s->new_stream({ path => '/frame_size/t1.html' }); |
| $s->h2_window(2**18, 1); |
| $s->h2_window(2**18); |
| |
| $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); |
| @data = grep { $_->{type} eq "DATA" } @$frames; |
| is($data[0]->{length}, 2**15, 'max frame size - custom'); |
| |
| # SETTINGS_INITIAL_WINDOW_SIZE + SETTINGS_MAX_FRAME_SIZE |
| # Expanding available stream window should not result in emitting |
| # new frames before remaining SETTINGS parameters were applied. |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $s->h2_window(2**17); |
| $s->h2_settings(0, 0x4 => 42); |
| |
| $sid = $s->new_stream({ path => '/frame_size/t1.html' }); |
| $s->read(all => [{ sid => $sid, length => 42 }]); |
| |
| $s->h2_settings(0, 0x4 => 2**17, 0x5 => 2**15); |
| |
| $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); |
| @data = grep { $_->{type} eq "DATA" } @$frames; |
| $lengths = join ' ', map { $_->{length} } @data; |
| is($lengths, '32768 32768 38', 'multiple SETTINGS'); |
| |
| # stream multiplexing + WINDOW_UPDATE |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $sid = $s->new_stream({ path => '/t1.html' }); |
| $frames = $s->read(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 = $s->new_stream({ path => '/t1.html' }); |
| $frames = $s->read(all => [{ sid => $sid2, fin => 0x4 }]); |
| |
| @data = grep { $_->{type} eq "DATA" } @$frames; |
| is(@data, 0, 'multiple - stream2 no data'); |
| |
| $s->h2_window(2**17, $sid); |
| $s->h2_window(2**17, $sid2); |
| $s->h2_window(2**17); |
| |
| $frames = $s->read(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'); |
| |
| # http2_max_concurrent_streams |
| |
| $s = Test::Nginx::HTTP2->new(port(8083), pure => 1); |
| $frames = $s->read(all => [{ type => 'SETTINGS' }]); |
| |
| ($frame) = grep { $_->{type} eq 'SETTINGS' } @$frames; |
| is($frame->{3}, 1, 'http2_max_concurrent_streams SETTINGS'); |
| |
| $s->h2_window(2**18); |
| |
| $sid = $s->new_stream({ path => '/t1.html' }); |
| $frames = $s->read(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 = $s->new_stream({ path => '/t1.html' }); |
| $frames = $s->read(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'); |
| |
| # properly skip header field that's not/never indexed from discarded streams |
| |
| $sid2 = $s->new_stream({ headers => [ |
| { name => ':method', value => 'GET' }, |
| { name => ':scheme', value => 'http' }, |
| { name => ':path', value => '/', mode => 6 }, |
| { name => ':authority', value => 'localhost' }, |
| { name => 'x-foo', value => 'Foo', mode => 2 }]}); |
| $frames = $s->read(all => [{ type => 'RST_STREAM' }]); |
| |
| # also if split across writes |
| |
| $sid2 = $s->new_stream({ split => [ 22 ], headers => [ |
| { name => ':method', value => 'GET' }, |
| { name => ':scheme', value => 'http' }, |
| { name => ':path', value => '/', mode => 6 }, |
| { name => ':authority', value => 'localhost' }, |
| { name => 'x-bar', value => 'Bar', mode => 2 }]}); |
| $frames = $s->read(all => [{ type => 'RST_STREAM' }]); |
| |
| # also if split across frames |
| |
| $sid2 = $s->new_stream({ continuation => [ 17 ], headers => [ |
| { name => ':method', value => 'GET' }, |
| { name => ':scheme', value => 'http' }, |
| { name => ':path', value => '/', mode => 6 }, |
| { name => ':authority', value => 'localhost' }, |
| { name => 'x-baz', value => 'Baz', mode => 2 }]}); |
| $frames = $s->read(all => [{ type => 'RST_STREAM' }]); |
| |
| $s->h2_window(2**16, $sid); |
| $s->read(all => [{ sid => $sid, fin => 1 }]); |
| |
| $sid = $s->new_stream({ headers => [ |
| { name => ':method', value => 'GET' }, |
| { name => ':scheme', value => 'http' }, |
| { name => ':path', value => '/t2.html' }, |
| { name => ':authority', value => 'localhost' }, |
| # make sure that discarded streams updated dynamic table |
| { name => 'x-foo', value => 'Foo', mode => 0 }, |
| { name => 'x-bar', value => 'Bar', mode => 0 }, |
| { name => 'x-baz', value => 'Baz', mode => 0 }]}); |
| $frames = $s->read(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 |
| |
| # invalid connection preface |
| |
| $s = Test::Nginx::HTTP2->new(port(8080), preface => 'x' x 16, pure => 1); |
| $frames = $s->read(all => [{ type => 'GOAWAY' }]); |
| |
| ($frame) = grep { $_->{type} eq "GOAWAY" } @$frames; |
| ok($frame, 'invalid preface - GOAWAY frame'); |
| is($frame->{code}, 1, 'invalid preface - error code'); |
| |
| my $preface = 'PRI * HTTP/2.0' . CRLF . CRLF . 'x' x 8; |
| $s = Test::Nginx::HTTP2->new(port(8080), preface => $preface, pure => 1); |
| $frames = $s->read(all => [{ type => 'GOAWAY' }]); |
| |
| ($frame) = grep { $_->{type} eq "GOAWAY" } @$frames; |
| ok($frame, 'invalid preface 2 - GOAWAY frame'); |
| is($frame->{code}, 1, 'invalid preface 2 - error code'); |
| |
| # GOAWAY on SYN_STREAM with even StreamID |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $s->new_stream({ path => '/' }, 2); |
| $frames = $s->read(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. |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $sid = $s->new_stream({ path => '/' }, 3); |
| $s->read(all => [{ sid => $sid, fin => 1 }]); |
| |
| $sid2 = $s->new_stream({ path => '/' }, 1); |
| $frames = $s->read(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 |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $sid = $s->new_stream({ path => '/' }); |
| $s->read(all => [{ sid => $sid, fin => 1 }]); |
| |
| $sid2 = $s->new_stream({ path => '/' }, $sid); |
| $frames = $s->read(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'); |
| |
| # aborted stream with zero HEADERS payload followed by client connection close |
| |
| Test::Nginx::HTTP2->new()->new_stream({ split => [ 9 ], abort => 1 }); |
| |
| # unknown frame type |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $s->h2_unknown('payload'); |
| $s->h2_ping('SEE-THIS'); |
| $frames = $s->read(all => [{ type => 'PING' }]); |
| |
| ($frame) = grep { $_->{type} eq "PING" } @$frames; |
| is($frame->{value}, 'SEE-THIS', 'unknown frame type'); |
| |
| # graceful shutdown with stream waiting on HEADERS payload |
| |
| my $grace = Test::Nginx::HTTP2->new(port(8084)); |
| $grace->new_stream({ split => [ 9 ], abort => 1 }); |
| |
| # graceful shutdown with stream waiting on WINDOW_UPDATE |
| |
| my $grace2 = Test::Nginx::HTTP2->new(port(8084)); |
| $sid = $grace2->new_stream({ path => '/t1.html' }); |
| $grace2->read(all => [{ sid => $sid, length => 2**16 - 1 }]); |
| |
| # graceful shutdown waiting on incomplete request body DATA frames |
| |
| my $grace3 = Test::Nginx::HTTP2->new(port(8085)); |
| $sid = $grace3->new_stream({ path => '/proxy2/t2.html', body_more => 1 }); |
| $grace3->h2_body('TEST', { body_more => 1 }); |
| |
| # partial request body data frame with connection close after body timeout |
| |
| my $grace4 = Test::Nginx::HTTP2->new(port(8087)); |
| $sid = $grace4->new_stream({ path => '/proxy/t2.html', body_more => 1 }); |
| $grace4->h2_body('TEST', { split => [ 12 ], abort => 1 }); |
| |
| select undef, undef, undef, 1.1; |
| undef $grace4; |
| |
| # GOAWAY without awaiting active streams, further streams ignored |
| |
| $s = Test::Nginx::HTTP2->new(port(8080)); |
| $sid = $s->new_stream({ path => '/t1.html' }); |
| $s->read(all => [{ sid => $sid, length => 2**16 - 1 }]); |
| |
| $t->reload(); |
| |
| $frames = $s->read(all => [{ type => 'GOAWAY' }]); |
| |
| ($frame) = grep { $_->{type} eq "GOAWAY" } @$frames; |
| is($frame->{last_sid}, $sid, 'GOAWAY with active stream - last sid'); |
| |
| $sid2 = $s->new_stream(); |
| $frames = $s->read(all => [{ sid => $sid2, fin => 0x4 }], wait => 0.5); |
| |
| ($frame) = grep { $_->{type} eq 'HEADERS' } @$frames; |
| is($frame, undef, 'GOAWAY with active stream - no new stream'); |
| |
| $s->h2_window(100, $sid); |
| $s->h2_window(100); |
| $frames = $s->read(all => [{ sid => $sid, fin => 0x1 }]); |
| |
| @data = grep { $_->{type} eq "DATA" && $_->{sid} == $sid } @$frames; |
| $sum = eval join '+', map { $_->{length} } @data; |
| is($sum, 81, 'GOAWAY with active stream - active stream DATA after GOAWAY'); |
| |
| # GOAWAY - force closing a connection by server with idle or active streams |
| |
| $s = Test::Nginx::HTTP2->new(); |
| $sid = $s->new_stream(); |
| $s->read(all => [{ sid => $sid, fin => 1 }]); |
| |
| my $active = Test::Nginx::HTTP2->new(port(8086)); |
| $sid = $active->new_stream({ path => '/t1.html' }); |
| $active->read(all => [{ sid => $sid, length => 2**16 - 1 }]); |
| |
| $t->stop(); |
| |
| $frames = $s->read(all => [{ type => 'GOAWAY' }]); |
| ($frame) = grep { $_->{type} eq "GOAWAY" } @$frames; |
| ok($frame, 'GOAWAY on connection close - idle stream'); |
| |
| $frames = $active->read(all => [{ type => 'GOAWAY' }]); |
| ($frame) = grep { $_->{type} eq "GOAWAY" } @$frames; |
| ok($frame, 'GOAWAY on connection close - active stream'); |
| |
| ############################################################################### |
| |
| 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); |
| } |
| } |
| |
| ############################################################################### |