0
Fork 0
mirror of https://github.com/ninenines/cowboy.git synced 2025-07-14 12:20:24 +00:00

Add options controlling initial control flow windows

This commit is contained in:
Loïc Hoguin 2018-04-26 22:08:05 +02:00
parent b2f16d462a
commit d38d86c4a9
No known key found for this signature in database
GPG key ID: 8A9DF795F6FED764
4 changed files with 309 additions and 39 deletions

View file

@ -17,17 +17,19 @@ as a Ranch protocol.
[source,erlang] [source,erlang]
---- ----
opts() :: #{ opts() :: #{
connection_type => worker | supervisor, connection_type => worker | supervisor,
enable_connect_protocol => boolean(), enable_connect_protocol => boolean(),
env => cowboy_middleware:env(), env => cowboy_middleware:env(),
inactivity_timeout => timeout(), inactivity_timeout => timeout(),
max_concurrent_streams => non_neg_integer() | infinity, initial_connection_window_size => 65535..16#7fffffff,
max_decode_table_size => non_neg_integer(), initial_stream_window_size => 0..16#7fffffff,
max_encode_table_size => non_neg_integer(), max_concurrent_streams => non_neg_integer() | infinity,
middlewares => [module()], max_decode_table_size => non_neg_integer(),
preface_timeout => timeout(), max_encode_table_size => non_neg_integer(),
shutdown_timeout => timeout(), middlewares => [module()],
stream_handlers => [module()] preface_timeout => timeout(),
shutdown_timeout => timeout(),
stream_handlers => [module()]
} }
---- ----
@ -56,6 +58,19 @@ env (#{})::
inactivity_timeout (300000):: inactivity_timeout (300000)::
Time in ms with nothing received at all before Cowboy closes the connection. 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):: max_concurrent_streams (infinity)::
Maximum number of concurrent streams allowed on the connection. Maximum number of concurrent streams allowed on the connection.
@ -83,7 +98,8 @@ stream_handlers ([cowboy_stream_h])::
== Changelog == 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` `max_decode_table_size` and `max_encode_table_size`
to configure HTTP/2 SETTINGS. to configure HTTP/2 SETTINGS.
* *2.4*: Add the experimental option `enable_connect_protocol`. * *2.4*: Add the experimental option `enable_connect_protocol`.

View file

@ -27,6 +27,8 @@
enable_connect_protocol => boolean(), enable_connect_protocol => boolean(),
env => cowboy_middleware:env(), env => cowboy_middleware:env(),
inactivity_timeout => timeout(), 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_concurrent_streams => non_neg_integer() | infinity,
max_decode_table_size => non_neg_integer(), max_decode_table_size => non_neg_integer(),
max_encode_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) -> init(Parent, Ref, Socket, Transport, Opts, Peer, Sock, Cert, Buffer) ->
State0 = #state{parent=Parent, ref=Ref, socket=Socket, State0 = #state{parent=Parent, ref=Ref, socket=Socket,
transport=Transport, opts=Opts, peer=Peer, sock=Sock, cert=Cert, 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)}}, parse_state={preface, sequence, preface_timeout(Opts)}},
State = settings_init(State0, Opts), State = settings_init(State0, Opts),
preface(State), 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) -> init(Parent, Ref, Socket, Transport, Opts, Peer, Sock, Cert, Buffer, _Settings, Req) ->
State0 = #state{parent=Parent, ref=Ref, socket=Socket, State0 = #state{parent=Parent, ref=Ref, socket=Socket,
transport=Transport, opts=Opts, peer=Peer, sock=Sock, cert=Cert, 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)}}, parse_state={preface, sequence, preface_timeout(Opts)}},
%% @todo Apply settings. %% @todo Apply settings.
%% StreamID from HTTP/1.1 Upgrade requests is always 1. %% StreamID from HTTP/1.1 Upgrade requests is always 1.
@ -209,10 +213,11 @@ settings_init(State, Opts) ->
header_table_size, 4096), header_table_size, 4096),
S1 = setting_from_opt(S0, Opts, max_concurrent_streams, S1 = setting_from_opt(S0, Opts, max_concurrent_streams,
max_concurrent_streams, infinity), 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_frame_size
%% @todo max_header_list_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), enable_connect_protocol, false),
State#state{next_settings=Settings}. State#state{next_settings=Settings}.
@ -222,9 +227,16 @@ setting_from_opt(Settings, Opts, OptName, SettingName, Default) ->
Value -> Settings#{SettingName => Value} Value -> Settings#{SettingName => Value}
end. end.
preface(#state{socket=Socket, transport=Transport, next_settings=Settings}) -> %% We send next_settings and use defaults until we get an ack.
%% We send next_settings and use defaults until we get a ack. %%
Transport:send(Socket, cow_http2:settings(Settings)). %% 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) -> preface_timeout(Opts) ->
case maps:get(preface_timeout, Opts, 5000) of case maps:get(preface_timeout, Opts, 5000) of
@ -348,11 +360,18 @@ frame(State=#state{client_streamid=LastStreamID}, {data, StreamID, _, _})
when StreamID > LastStreamID -> when StreamID > LastStreamID ->
terminate(State, {connection_error, protocol_error, terminate(State, {connection_error, protocol_error,
'DATA frame received on a stream in idle state. (RFC7540 5.1)'}); '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}, frame(State0=#state{remote_window=ConnWindow, streams=Streams, lingering_streams=Lingering},
{data, StreamID, IsFin, Data}) -> {data, StreamID, IsFin, Data}) ->
DataLen = byte_size(Data), DataLen = byte_size(Data),
State = State0#state{remote_window=ConnWindow - DataLen}, State = State0#state{remote_window=ConnWindow - DataLen},
case lists:keyfind(StreamID, #stream.id, Streams) of 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} -> Stream = #stream{state=flush, remote=nofin, remote_window=StreamWindow} ->
after_commands(State, Stream#stream{remote=IsFin, remote_window=StreamWindow - DataLen}); after_commands(State, Stream#stream{remote=IsFin, remote_window=StreamWindow - DataLen});
Stream = #stream{state=StreamState0, remote=nofin, remote_window=StreamWindow} -> 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}; State#state{encode_state=EncodeState};
(initial_window_size, NewWindowSize, State) -> (initial_window_size, NewWindowSize, State) ->
OldWindowSize = maps:get(initial_window_size, Settings0, 65535), OldWindowSize = maps:get(initial_window_size, Settings0, 65535),
update_stream_windows(State, NewWindowSize - OldWindowSize); update_streams_local_window(State, NewWindowSize - OldWindowSize);
(_, _, State) -> (_, _, State) ->
State State
end, State1, Settings); 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}) -> (header_table_size, MaxSize, State=#state{decode_state=DecodeState0}) ->
DecodeState = cow_hpack:set_max_size(MaxSize, DecodeState0), DecodeState = cow_hpack:set_max_size(MaxSize, DecodeState0),
State#state{decode_state=DecodeState}; 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) ->
State State
end, State1, NextSettings); end, State1, NextSettings);
@ -718,14 +740,22 @@ send_data(State=#state{streams=Streams}) ->
resume_streams(State, Streams, []). resume_streams(State, Streams, []).
%% When SETTINGS_INITIAL_WINDOW_SIZE changes we need to update %% When SETTINGS_INITIAL_WINDOW_SIZE changes we need to update
%% the stream windows for all active streams and perhaps resume %% the local stream windows for all active streams and perhaps
%% sending data. %% resume sending data.
update_stream_windows(State=#state{streams=Streams0}, Increment) -> update_streams_local_window(State=#state{streams=Streams0}, Increment) ->
Streams = [ Streams = [
S#stream{local_window=StreamWindow + Increment} S#stream{local_window=StreamWindow + Increment}
|| S=#stream{local_window=StreamWindow} <- Streams0], || S=#stream{local_window=StreamWindow} <- Streams0],
resume_streams(State, Streams, []). 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) -> resume_streams(State, [], Acc) ->
State#state{streams=lists:reverse(Acc)}; State#state{streams=lists:reverse(Acc)};
%% While technically we should never get < 0 here, let's be on the safe side. %% While technically we should never get < 0 here, let's be on the safe side.

View file

@ -18,6 +18,7 @@
-import(ct_helper, [config/2]). -import(ct_helper, [config/2]).
-import(ct_helper, [doc/1]). -import(ct_helper, [doc/1]).
-import(ct_helper, [name/0]).
-import(cowboy_test, [gun_open/1]). -import(cowboy_test, [gun_open/1]).
all() -> [{group, clear}]. all() -> [{group, clear}].
@ -46,28 +47,49 @@ do_handshake(Config) ->
{ok, Socket}. {ok, Socket}.
inactivity_timeout(Config) -> inactivity_timeout(Config) ->
doc("Terminate when the inactivity timeout is reached"), doc("Terminate when the inactivity timeout is reached."),
ProtoOpts = #{ ProtoOpts = #{
env => #{dispatch => cowboy_router:compile(init_routes(Config))}, env => #{dispatch => cowboy_router:compile(init_routes(Config))},
inactivity_timeout => 1000 inactivity_timeout => 1000
}, },
{ok, _} = cowboy:start_clear(inactivity_timeout, [{port, 0}], ProtoOpts), {ok, _} = cowboy:start_clear(name(), [{port, 0}], ProtoOpts),
Port = ranch:get_port(inactivity_timeout), Port = ranch:get_port(name()),
{ok, Socket} = do_handshake([{port, Port}|Config]), {ok, Socket} = do_handshake([{port, Port}|Config]),
receive after 1000 -> ok end, receive after 1000 -> ok end,
%% Receive a GOAWAY frame back with an INTERNAL_ERROR. %% Receive a GOAWAY frame back with an INTERNAL_ERROR.
{ok, << _:24, 7:8, _:72, 2:32 >>} = gen_tcp:recv(Socket, 17, 1000), {ok, << _:24, 7:8, _:72, 2:32 >>} = gen_tcp:recv(Socket, 17, 1000),
ok. 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) -> preface_timeout_infinity(Config) ->
doc("Ensure infinity for preface_timeout is accepted"), doc("Ensure infinity for preface_timeout is accepted."),
ProtoOpts = #{ ProtoOpts = #{
env => #{dispatch => cowboy_router:compile(init_routes(Config))}, env => #{dispatch => cowboy_router:compile(init_routes(Config))},
preface_timeout => infinity 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), Ref = erlang:monitor(process, Pid),
Port = ranch:get_port(preface_timeout_infinity), Port = ranch:get_port(name()),
{ok, _} = do_handshake([{port, Port}|Config]), {ok, _} = do_handshake([{port, Port}|Config]),
receive receive
{'DOWN', Ref, process, Pid, Reason} -> {'DOWN', Ref, process, Pid, Reason} ->
@ -83,8 +105,8 @@ resp_iolist_body(Config) ->
ProtoOpts = #{ ProtoOpts = #{
env => #{dispatch => cowboy_router:compile(init_routes(Config))} env => #{dispatch => cowboy_router:compile(init_routes(Config))}
}, },
{ok, _} = cowboy:start_clear(resp_iolist_body, [{port, 0}], ProtoOpts), {ok, _} = cowboy:start_clear(name(), [{port, 0}], ProtoOpts),
Port = ranch:get_port(resp_iolist_body), Port = ranch:get_port(name()),
ConnPid = gun_open([{type, tcp}, {protocol, http2}, {port, Port}|Config]), ConnPid = gun_open([{type, tcp}, {protocol, http2}, {port, Port}|Config]),
Ref = gun:get(ConnPid, "/resp_iolist_body"), Ref = gun:get(ConnPid, "/resp_iolist_body"),
{response, nofin, 200, RespHeaders} = gun:await(ConnPid, Ref), {response, nofin, 200, RespHeaders} = gun:await(ConnPid, Ref),

View file

@ -2578,11 +2578,139 @@ settings_max_concurrent_streams_0(Config0) ->
% of error code determines whether the endpoint wishes to enable % of error code determines whether the endpoint wishes to enable
% automatic retry (see Section 8.1.4) for details). % automatic retry (see Section 8.1.4) for details).
% SETTINGS_INITIAL_WINDOW_SIZE (0x4): settings_initial_window_size(Config0) ->
% Values above the maximum flow-control window size of 2^31-1 MUST doc("The SETTINGS_INITIAL_WINDOW_SIZE setting can be used to "
% be treated as a connection error (Section 5.4.1) of type "change the initial window size of streams. (RFC7540 6.5.2)"),
% FLOW_CONTROL_ERROR. %% 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): % SETTINGS_MAX_FRAME_SIZE (0x5):
% The initial value is 2^14 (16,384) octets. The value advertised % The initial value is 2^14 (16,384) octets. The value advertised
% by an endpoint MUST be between this initial value and the maximum % 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 % the receiver does not, the flow-control window at the sender and
% receiver can become different. % 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) %% (RFC7540 6.9.1)
% The sender MUST NOT % Frames with zero length with the END_STREAM flag set (that
% 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
% is, an empty DATA frame) MAY be sent if there is no available space % is, an empty DATA frame) MAY be sent if there is no available space
% in either flow-control window. % in either flow-control window.
@ -2852,7 +3054,7 @@ settings_initial_window_size_changes_negative(Config) ->
settings_initial_window_size_reject_overflow(Config) -> settings_initial_window_size_reject_overflow(Config) ->
doc("A SETTINGS_INITIAL_WINDOW_SIZE that causes a flow control window " 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 " "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), {ok, Socket} = do_handshake(Config),
%% Set SETTINGS_INITIAL_WINDOW_SIZE to 2^31. %% Set SETTINGS_INITIAL_WINDOW_SIZE to 2^31.
ok = gen_tcp:send(Socket, cow_http2:settings(#{initial_window_size => 16#80000000})), ok = gen_tcp:send(Socket, cow_http2:settings(#{initial_window_size => 16#80000000})),