From d38d86c4a93340b1dd2633e1649c257e3f160d63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Thu, 26 Apr 2018 22:08:05 +0200 Subject: Add options controlling initial control flow windows --- doc/src/manual/cowboy_http2.asciidoc | 40 +++++-- src/cowboy_http2.erl | 48 ++++++-- test/http2_SUITE.erl | 38 ++++-- test/rfc7540_SUITE.erl | 222 +++++++++++++++++++++++++++++++++-- 4 files changed, 309 insertions(+), 39 deletions(-) diff --git a/doc/src/manual/cowboy_http2.asciidoc b/doc/src/manual/cowboy_http2.asciidoc index 4c47fea..7151eb0 100644 --- a/doc/src/manual/cowboy_http2.asciidoc +++ b/doc/src/manual/cowboy_http2.asciidoc @@ -17,17 +17,19 @@ as a Ranch protocol. [source,erlang] ---- opts() :: #{ - connection_type => worker | supervisor, - enable_connect_protocol => boolean(), - env => cowboy_middleware:env(), - inactivity_timeout => timeout(), - max_concurrent_streams => non_neg_integer() | infinity, - max_decode_table_size => non_neg_integer(), - max_encode_table_size => non_neg_integer(), - middlewares => [module()], - preface_timeout => timeout(), - shutdown_timeout => timeout(), - stream_handlers => [module()] + connection_type => worker | supervisor, + enable_connect_protocol => boolean(), + env => cowboy_middleware:env(), + inactivity_timeout => timeout(), + initial_connection_window_size => 65535..16#7fffffff, + initial_stream_window_size => 0..16#7fffffff, + max_concurrent_streams => non_neg_integer() | infinity, + max_decode_table_size => non_neg_integer(), + max_encode_table_size => non_neg_integer(), + middlewares => [module()], + preface_timeout => timeout(), + shutdown_timeout => timeout(), + stream_handlers => [module()] } ---- @@ -56,6 +58,19 @@ env (#{}):: inactivity_timeout (300000):: Time in ms with nothing received at all before Cowboy closes the connection. +initial_connection_window_size (65535):: + Initial window size for the connection. This is the total amount + of data (from request bodies for example) that may be buffered + by the connection across all streams before the user code + explicitly requests it. ++ + Note that this value cannot be lower than the default. + +initial_stream_window_size (65535):: + Initial window size for new streams. This is the total amount + of data (from request bodies for example) that may be buffered + by a single stream before the user code explicitly requests it. + max_concurrent_streams (infinity):: Maximum number of concurrent streams allowed on the connection. @@ -83,7 +98,8 @@ stream_handlers ([cowboy_stream_h]):: == Changelog -* *2.4*: Add the options `max_concurrent_streams`, +* *2.4*: Add the options `initial_connection_window_size`, + `initial_stream_window_size`, `max_concurrent_streams`, `max_decode_table_size` and `max_encode_table_size` to configure HTTP/2 SETTINGS. * *2.4*: Add the experimental option `enable_connect_protocol`. diff --git a/src/cowboy_http2.erl b/src/cowboy_http2.erl index 3bcd402..45a60c2 100644 --- a/src/cowboy_http2.erl +++ b/src/cowboy_http2.erl @@ -27,6 +27,8 @@ enable_connect_protocol => boolean(), env => cowboy_middleware:env(), inactivity_timeout => timeout(), + initial_connection_window_size => 65535..16#7fffffff, + initial_stream_window_size => 0..16#7fffffff, max_concurrent_streams => non_neg_integer() | infinity, max_decode_table_size => non_neg_integer(), max_encode_table_size => non_neg_integer(), @@ -171,6 +173,7 @@ init(Parent, Ref, Socket, Transport, Opts) -> init(Parent, Ref, Socket, Transport, Opts, Peer, Sock, Cert, Buffer) -> State0 = #state{parent=Parent, ref=Ref, socket=Socket, transport=Transport, opts=Opts, peer=Peer, sock=Sock, cert=Cert, + remote_window=maps:get(initial_connection_window_size, Opts, 65535), parse_state={preface, sequence, preface_timeout(Opts)}}, State = settings_init(State0, Opts), preface(State), @@ -186,6 +189,7 @@ init(Parent, Ref, Socket, Transport, Opts, Peer, Sock, Cert, Buffer) -> init(Parent, Ref, Socket, Transport, Opts, Peer, Sock, Cert, Buffer, _Settings, Req) -> State0 = #state{parent=Parent, ref=Ref, socket=Socket, transport=Transport, opts=Opts, peer=Peer, sock=Sock, cert=Cert, + remote_window=maps:get(initial_connection_window_size, Opts, 65535), parse_state={preface, sequence, preface_timeout(Opts)}}, %% @todo Apply settings. %% StreamID from HTTP/1.1 Upgrade requests is always 1. @@ -209,10 +213,11 @@ settings_init(State, Opts) -> header_table_size, 4096), S1 = setting_from_opt(S0, Opts, max_concurrent_streams, max_concurrent_streams, infinity), - %% @todo initial_window_size + S2 = setting_from_opt(S1, Opts, initial_stream_window_size, + initial_window_size, 65535), %% @todo max_frame_size %% @todo max_header_list_size - Settings = setting_from_opt(S1, Opts, enable_connect_protocol, + Settings = setting_from_opt(S2, Opts, enable_connect_protocol, enable_connect_protocol, false), State#state{next_settings=Settings}. @@ -222,9 +227,16 @@ setting_from_opt(Settings, Opts, OptName, SettingName, Default) -> Value -> Settings#{SettingName => Value} end. -preface(#state{socket=Socket, transport=Transport, next_settings=Settings}) -> - %% We send next_settings and use defaults until we get a ack. - Transport:send(Socket, cow_http2:settings(Settings)). +%% We send next_settings and use defaults until we get an ack. +%% +%% We also send a WINDOW_UPDATE frame for the connection when +%% the user specified an initial_connection_window_size. +preface(#state{socket=Socket, transport=Transport, opts=Opts, next_settings=Settings}) -> + MaybeWindowUpdate = case maps:get(initial_connection_window_size, Opts, 65535) of + 65535 -> <<>>; + Size -> cow_http2:window_update(Size - 65535) + end, + Transport:send(Socket, [cow_http2:settings(Settings), MaybeWindowUpdate]). preface_timeout(Opts) -> case maps:get(preface_timeout, Opts, 5000) of @@ -348,11 +360,18 @@ frame(State=#state{client_streamid=LastStreamID}, {data, StreamID, _, _}) when StreamID > LastStreamID -> terminate(State, {connection_error, protocol_error, 'DATA frame received on a stream in idle state. (RFC7540 5.1)'}); +frame(State=#state{remote_window=ConnWindow}, {data, _, _, Data}) + when byte_size(Data) > ConnWindow -> + terminate(State, {connection_error, flow_control_error, + 'DATA frame overflowed the connection flow control window. (RFC7540 6.9, RFC7540 6.9.1)'}); frame(State0=#state{remote_window=ConnWindow, streams=Streams, lingering_streams=Lingering}, {data, StreamID, IsFin, Data}) -> DataLen = byte_size(Data), State = State0#state{remote_window=ConnWindow - DataLen}, case lists:keyfind(StreamID, #stream.id, Streams) of + #stream{remote_window=StreamWindow} when StreamWindow < DataLen -> + stream_reset(State, StreamID, {stream_error, flow_control_error, + 'DATA frame overflowed the stream flow control window. (RFC7540 6.9, RFC7540 6.9.1)'}); Stream = #stream{state=flush, remote=nofin, remote_window=StreamWindow} -> after_commands(State, Stream#stream{remote=IsFin, remote_window=StreamWindow - DataLen}); Stream = #stream{state=StreamState0, remote=nofin, remote_window=StreamWindow} -> @@ -436,7 +455,7 @@ frame(State0=#state{socket=Socket, transport=Transport, opts=Opts, State#state{encode_state=EncodeState}; (initial_window_size, NewWindowSize, State) -> OldWindowSize = maps:get(initial_window_size, Settings0, 65535), - update_stream_windows(State, NewWindowSize - OldWindowSize); + update_streams_local_window(State, NewWindowSize - OldWindowSize); (_, _, State) -> State end, State1, Settings); @@ -448,6 +467,9 @@ frame(State0=#state{local_settings=Local0, next_settings=NextSettings}, settings (header_table_size, MaxSize, State=#state{decode_state=DecodeState0}) -> DecodeState = cow_hpack:set_max_size(MaxSize, DecodeState0), State#state{decode_state=DecodeState}; + (initial_window_size, NewWindowSize, State) -> + OldWindowSize = maps:get(initial_window_size, Local0, 65535), + update_streams_remote_window(State, NewWindowSize - OldWindowSize); (_, _, State) -> State end, State1, NextSettings); @@ -718,14 +740,22 @@ send_data(State=#state{streams=Streams}) -> resume_streams(State, Streams, []). %% When SETTINGS_INITIAL_WINDOW_SIZE changes we need to update -%% the stream windows for all active streams and perhaps resume -%% sending data. -update_stream_windows(State=#state{streams=Streams0}, Increment) -> +%% the local stream windows for all active streams and perhaps +%% resume sending data. +update_streams_local_window(State=#state{streams=Streams0}, Increment) -> Streams = [ S#stream{local_window=StreamWindow + Increment} || S=#stream{local_window=StreamWindow} <- Streams0], resume_streams(State, Streams, []). +%% When we receive an ack to a SETTINGS frame we sent we need to update +%% the remote stream windows for all active streams. +update_streams_remote_window(State=#state{streams=Streams0}, Increment) -> + Streams = [ + S#stream{remote_window=StreamWindow + Increment} + || S=#stream{remote_window=StreamWindow} <- Streams0], + State#state{streams=Streams}. + resume_streams(State, [], Acc) -> State#state{streams=lists:reverse(Acc)}; %% While technically we should never get < 0 here, let's be on the safe side. diff --git a/test/http2_SUITE.erl b/test/http2_SUITE.erl index 933a2b2..5b9ca95 100644 --- a/test/http2_SUITE.erl +++ b/test/http2_SUITE.erl @@ -18,6 +18,7 @@ -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). +-import(ct_helper, [name/0]). -import(cowboy_test, [gun_open/1]). all() -> [{group, clear}]. @@ -46,28 +47,49 @@ do_handshake(Config) -> {ok, Socket}. inactivity_timeout(Config) -> - doc("Terminate when the inactivity timeout is reached"), + doc("Terminate when the inactivity timeout is reached."), ProtoOpts = #{ env => #{dispatch => cowboy_router:compile(init_routes(Config))}, inactivity_timeout => 1000 }, - {ok, _} = cowboy:start_clear(inactivity_timeout, [{port, 0}], ProtoOpts), - Port = ranch:get_port(inactivity_timeout), + {ok, _} = cowboy:start_clear(name(), [{port, 0}], ProtoOpts), + Port = ranch:get_port(name()), {ok, Socket} = do_handshake([{port, Port}|Config]), receive after 1000 -> ok end, %% Receive a GOAWAY frame back with an INTERNAL_ERROR. {ok, << _:24, 7:8, _:72, 2:32 >>} = gen_tcp:recv(Socket, 17, 1000), ok. +initial_connection_window_size(Config) -> + doc("Confirm a WINDOW_UPDATE frame is sent when the configured " + "connection window is larger than the default."), + ConfiguredSize = 100000, + ProtoOpts = #{ + env => #{dispatch => cowboy_router:compile(init_routes(Config))}, + initial_connection_window_size => ConfiguredSize + }, + {ok, _} = cowboy:start_clear(name(), [{port, 0}], ProtoOpts), + Port = ranch:get_port(name()), + {ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {active, false}]), + %% Send a valid preface. + ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), + %% Receive the server preface. + {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), + {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), + %% Receive a WINDOW_UPDATE frame incrementing the connection window to 100000. + {ok, <<4:24, 8:8, 0:41, Size:31>>} = gen_tcp:recv(Socket, 13, 1000), + ConfiguredSize = Size + 65535, + ok. + preface_timeout_infinity(Config) -> - doc("Ensure infinity for preface_timeout is accepted"), + doc("Ensure infinity for preface_timeout is accepted."), ProtoOpts = #{ env => #{dispatch => cowboy_router:compile(init_routes(Config))}, preface_timeout => infinity }, - {ok, Pid} = cowboy:start_clear(preface_timeout_infinity, [{port, 0}], ProtoOpts), + {ok, Pid} = cowboy:start_clear(name(), [{port, 0}], ProtoOpts), Ref = erlang:monitor(process, Pid), - Port = ranch:get_port(preface_timeout_infinity), + Port = ranch:get_port(name()), {ok, _} = do_handshake([{port, Port}|Config]), receive {'DOWN', Ref, process, Pid, Reason} -> @@ -83,8 +105,8 @@ resp_iolist_body(Config) -> ProtoOpts = #{ env => #{dispatch => cowboy_router:compile(init_routes(Config))} }, - {ok, _} = cowboy:start_clear(resp_iolist_body, [{port, 0}], ProtoOpts), - Port = ranch:get_port(resp_iolist_body), + {ok, _} = cowboy:start_clear(name(), [{port, 0}], ProtoOpts), + Port = ranch:get_port(name()), ConnPid = gun_open([{type, tcp}, {protocol, http2}, {port, Port}|Config]), Ref = gun:get(ConnPid, "/resp_iolist_body"), {response, nofin, 200, RespHeaders} = gun:await(ConnPid, Ref), diff --git a/test/rfc7540_SUITE.erl b/test/rfc7540_SUITE.erl index 14e38fd..d4d5f30 100644 --- a/test/rfc7540_SUITE.erl +++ b/test/rfc7540_SUITE.erl @@ -2578,11 +2578,139 @@ settings_max_concurrent_streams_0(Config0) -> % of error code determines whether the endpoint wishes to enable % automatic retry (see Section 8.1.4) for details). -% SETTINGS_INITIAL_WINDOW_SIZE (0x4): -% Values above the maximum flow-control window size of 2^31-1 MUST -% be treated as a connection error (Section 5.4.1) of type -% FLOW_CONTROL_ERROR. -% +settings_initial_window_size(Config0) -> + doc("The SETTINGS_INITIAL_WINDOW_SIZE setting can be used to " + "change the initial window size of streams. (RFC7540 6.5.2)"), + %% Create a new listener that allows only a single concurrent stream. + Config = cowboy_test:init_http(name(), #{ + env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, + initial_connection_window_size => 100000, + initial_stream_window_size => 100000 + }, Config0), + %% We need to do the handshake manually because a WINDOW_UPDATE + %% frame will be sent to update the connection window. + {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), + %% Send a valid preface. + ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), + %% Receive the server preface. + {ok, << Len1:24 >>} = gen_tcp:recv(Socket, 3, 1000), + {ok, << 4:8, 0:40, _:Len1/binary >>} = gen_tcp:recv(Socket, 6 + Len1, 1000), + %% Send the SETTINGS ack. + ok = gen_tcp:send(Socket, cow_http2:settings_ack()), + %% Receive the WINDOW_UPDATE for the connection. + {ok, << 4:24, 8:8, 0:40, _:32 >>} = gen_tcp:recv(Socket, 13, 1000), + %% Receive the SETTINGS ack. + {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), + %% Send a HEADERS frame initiating a stream followed by + %% DATA frames totaling 90000 bytes of body. + Headers = [ + {<<":method">>, <<"POST">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. + {<<":path">>, <<"/long_polling">>} + ], + {HeadersBlock, _} = cow_hpack:encode(Headers), + ok = gen_tcp:send(Socket, [ + cow_http2:headers(1, nofin, HeadersBlock), + cow_http2:data(1, nofin, <<0:15000/unit:8>>), + cow_http2:data(1, nofin, <<0:15000/unit:8>>), + cow_http2:data(1, nofin, <<0:15000/unit:8>>), + cow_http2:data(1, nofin, <<0:15000/unit:8>>), + cow_http2:data(1, nofin, <<0:15000/unit:8>>), + cow_http2:data(1, fin, <<0:15000/unit:8>>) + ]), + %% Receive a proper response. + {ok, << Len2:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), + {ok, _} = gen_tcp:recv(Socket, Len2, 6000), + %% No errors follow due to our sending of more than 65535 bytes of data. + {error, timeout} = gen_tcp:recv(Socket, 0, 1000), + ok. + +settings_initial_window_size_after_ack(Config0) -> + doc("The SETTINGS_INITIAL_WINDOW_SIZE setting can be used to " + "change the initial window size of streams. It is applied " + "to all existing streams upon receipt of the SETTINGS ack. (RFC7540 6.5.2)"), + %% Create a new listener that allows only a single concurrent stream. + Config = cowboy_test:init_http(name(), #{ + env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, + initial_stream_window_size => 0 + }, Config0), + %% We need to do the handshake manually because we don't + %% want to send the SETTINGS ack immediately. + {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), + %% Send a valid preface. + ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), + %% Receive the server preface. + {ok, << Len1:24 >>} = gen_tcp:recv(Socket, 3, 1000), + {ok, << 4:8, 0:40, _:Len1/binary >>} = gen_tcp:recv(Socket, 6 + Len1, 1000), + %% + %% Don't send the SETTINGS ack yet! We want to create a stream first. + %% + %% Receive the SETTINGS ack. + {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), + %% Send a HEADERS frame initiating a stream, a SETTINGS ack + %% and a small DATA frame despite no window available in the stream. + Headers = [ + {<<":method">>, <<"POST">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. + {<<":path">>, <<"/long_polling">>} + ], + {HeadersBlock, _} = cow_hpack:encode(Headers), + ok = gen_tcp:send(Socket, [ + cow_http2:headers(1, nofin, HeadersBlock), + cow_http2:settings_ack(), + cow_http2:data(1, fin, <<0:32/unit:8>>) + ]), + %% Receive a FLOW_CONTROL_ERROR stream error. + {ok, << _:24, 3:8, _:8, 1:32, 3:32 >>} = gen_tcp:recv(Socket, 13, 6000), + ok. + +settings_initial_window_size_before_ack(Config0) -> + doc("The SETTINGS_INITIAL_WINDOW_SIZE setting can be used to " + "change the initial window size of streams. It is only " + "applied upon receipt of the SETTINGS ack. (RFC7540 6.5.2)"), + %% Create a new listener that allows only a single concurrent stream. + Config = cowboy_test:init_http(name(), #{ + env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, + initial_stream_window_size => 0 + }, Config0), + %% We need to do the handshake manually because we don't + %% want to send the SETTINGS ack. + {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), + %% Send a valid preface. + ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), + %% Receive the server preface. + {ok, << Len1:24 >>} = gen_tcp:recv(Socket, 3, 1000), + {ok, << 4:8, 0:40, _:Len1/binary >>} = gen_tcp:recv(Socket, 6 + Len1, 1000), + %% + %% Don't send the SETTINGS ack! We want the server to keep the original settings. + %% + %% Receive the SETTINGS ack. + {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), + %% Send a HEADERS frame initiating a stream followed by + %% DATA frames totaling 60000 bytes of body. + Headers = [ + {<<":method">>, <<"POST">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. + {<<":path">>, <<"/long_polling">>} + ], + {HeadersBlock, _} = cow_hpack:encode(Headers), + ok = gen_tcp:send(Socket, [ + cow_http2:headers(1, nofin, HeadersBlock), + cow_http2:data(1, nofin, <<0:15000/unit:8>>), + cow_http2:data(1, nofin, <<0:15000/unit:8>>), + cow_http2:data(1, nofin, <<0:15000/unit:8>>), + cow_http2:data(1, fin, <<0:15000/unit:8>>) + ]), + %% Receive a proper response. + {ok, << Len2:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), + {ok, _} = gen_tcp:recv(Socket, Len2, 6000), + %% No errors follow due to our sending of more than 0 bytes of data. + {error, timeout} = gen_tcp:recv(Socket, 0, 1000), + ok. + % SETTINGS_MAX_FRAME_SIZE (0x5): % The initial value is 2^14 (16,384) octets. The value advertised % by an endpoint MUST be between this initial value and the maximum @@ -2737,11 +2865,85 @@ window_update_reject_0_stream(Config) -> % the receiver does not, the flow-control window at the sender and % receiver can become different. +data_reject_overflow(Config0) -> + doc("DATA frames that cause the connection flow control window " + "to overflow must be rejected with a FLOW_CONTROL_ERROR " + "connection error. (RFC7540 6.9.1)"), + %% Create a new listener that allows only a single concurrent stream. + Config = cowboy_test:init_http(name(), #{ + env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, + initial_stream_window_size => 100000 + }, Config0), + {ok, Socket} = do_handshake(Config), + %% Send a HEADERS frame initiating a stream followed by + %% DATA frames totaling 90000 bytes of body. + Headers = [ + {<<":method">>, <<"POST">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. + {<<":path">>, <<"/long_polling">>} + ], + {HeadersBlock, _} = cow_hpack:encode(Headers), + ok = gen_tcp:send(Socket, [ + cow_http2:headers(1, nofin, HeadersBlock), + cow_http2:data(1, nofin, <<0:15000/unit:8>>), + cow_http2:data(1, nofin, <<0:15000/unit:8>>), + cow_http2:data(1, nofin, <<0:15000/unit:8>>), + cow_http2:data(1, nofin, <<0:15000/unit:8>>), + cow_http2:data(1, nofin, <<0:15000/unit:8>>), + cow_http2:data(1, fin, <<0:15000/unit:8>>) + ]), + %% Receive a FLOW_CONTROL_ERROR connection error. + {ok, << _:24, 7:8, _:72, 3:32 >>} = gen_tcp:recv(Socket, 17, 6000), + ok. + +data_reject_overflow_stream(Config0) -> + doc("DATA frames that cause the stream flow control window " + "to overflow must be rejected with a FLOW_CONTROL_ERROR " + "stream error. (RFC7540 6.9.1)"), + %% Create a new listener that allows only a single concurrent stream. + Config = cowboy_test:init_http(name(), #{ + env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, + initial_connection_window_size => 100000 + }, Config0), + %% We need to do the handshake manually because a WINDOW_UPDATE + %% frame will be sent to update the connection window. + {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), + %% Send a valid preface. + ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), + %% Receive the server preface. + {ok, << Len1:24 >>} = gen_tcp:recv(Socket, 3, 1000), + {ok, << 4:8, 0:40, _:Len1/binary >>} = gen_tcp:recv(Socket, 6 + Len1, 1000), + %% Send the SETTINGS ack. + ok = gen_tcp:send(Socket, cow_http2:settings_ack()), + %% Receive the WINDOW_UPDATE for the connection. + {ok, << 4:24, 8:8, 0:40, _:32 >>} = gen_tcp:recv(Socket, 13, 1000), + %% Receive the SETTINGS ack. + {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), + %% Send a HEADERS frame initiating a stream followed by + %% DATA frames totaling 90000 bytes of body. + Headers = [ + {<<":method">>, <<"POST">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. + {<<":path">>, <<"/long_polling">>} + ], + {HeadersBlock, _} = cow_hpack:encode(Headers), + ok = gen_tcp:send(Socket, [ + cow_http2:headers(1, nofin, HeadersBlock), + cow_http2:data(1, nofin, <<0:15000/unit:8>>), + cow_http2:data(1, nofin, <<0:15000/unit:8>>), + cow_http2:data(1, nofin, <<0:15000/unit:8>>), + cow_http2:data(1, nofin, <<0:15000/unit:8>>), + cow_http2:data(1, nofin, <<0:15000/unit:8>>), + cow_http2:data(1, fin, <<0:15000/unit:8>>) + ]), + %% Receive a FLOW_CONTROL_ERROR stream error. + {ok, << _:24, 3:8, _:8, 1:32, 3:32 >>} = gen_tcp:recv(Socket, 13, 6000), + ok. + %% (RFC7540 6.9.1) -% The sender MUST NOT -% send a flow-controlled frame with a length that exceeds the space -% available in either of the flow-control windows advertised by the -% receiver. Frames with zero length with the END_STREAM flag set (that +% Frames with zero length with the END_STREAM flag set (that % is, an empty DATA frame) MAY be sent if there is no available space % in either flow-control window. @@ -2852,7 +3054,7 @@ settings_initial_window_size_changes_negative(Config) -> settings_initial_window_size_reject_overflow(Config) -> doc("A SETTINGS_INITIAL_WINDOW_SIZE that causes a flow control window " "to exceed 2^31-1 must be rejected with a FLOW_CONTROL_ERROR " - "connection error. (RFC7540 6.9.2)"), + "connection error. (RFC7540 6.5.2, RFC7540 6.9.2)"), {ok, Socket} = do_handshake(Config), %% Set SETTINGS_INITIAL_WINDOW_SIZE to 2^31. ok = gen_tcp:send(Socket, cow_http2:settings(#{initial_window_size => 16#80000000})), -- cgit v1.2.3