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:
parent
b2f16d462a
commit
d38d86c4a9
4 changed files with 309 additions and 39 deletions
|
@ -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`.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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})),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue