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

Add options controlling maximum h2 frame sizes

This commit is contained in:
Loïc Hoguin 2018-04-27 17:58:11 +02:00
parent d38d86c4a9
commit 9a29aea148
No known key found for this signature in database
GPG key ID: 8A9DF795F6FED764
4 changed files with 174 additions and 73 deletions

View file

@ -26,6 +26,8 @@ opts() :: #{
max_concurrent_streams => non_neg_integer() | infinity,
max_decode_table_size => non_neg_integer(),
max_encode_table_size => non_neg_integer(),
max_frame_size_received => 16384..16777215,
max_frame_size_sent => 16384..16777215 | infinity,
middlewares => [module()],
preface_timeout => timeout(),
shutdown_timeout => timeout(),
@ -84,6 +86,19 @@ max_encode_table_size (4096)::
value to what the client advertises and choose the smallest one as the
encoder's header table size.
max_frame_size_received (16384)::
Maximum size of the frames received by the server. This value is
advertised to the remote endpoint which can then decide to use
any value lower or equal for its frame sizes.
max_frame_size_sent (infinity)::
Maximum size of the frames sent by the server. This option allows
setting an upper limit to the frame sizes instead of blindly
following the client's advertised maximum.
+
Note that actual frame sizes may be lower than the limit when
there is not enough space left in the flow control window.
middlewares ([cowboy_router, cowboy_handler])::
Middlewares to run for every request.
@ -100,7 +115,8 @@ stream_handlers ([cowboy_stream_h])::
* *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`, `max_encode_table_size`,
`max_frame_size_received` and `max_frame_size_sent`
to configure HTTP/2 SETTINGS.
* *2.4*: Add the experimental option `enable_connect_protocol`.
* *2.0*: Protocol introduced.

View file

@ -32,6 +32,8 @@
max_concurrent_streams => non_neg_integer() | infinity,
max_decode_table_size => non_neg_integer(),
max_encode_table_size => non_neg_integer(),
max_frame_size_received => 16384..16777215,
max_frame_size_sent => 16384..16777215 | infinity,
middlewares => [module()],
preface_timeout => timeout(),
shutdown_timeout => timeout(),
@ -89,7 +91,7 @@
%% the final settings handling will be very different.
local_settings = #{
% header_table_size => 4096,
% enable_push => false, %% We are the server. Push is never enabled.
% enable_push => false, %% We are the server. Push is never enabled for clients.
% max_concurrent_streams => infinity,
initial_window_size => 65535,
max_frame_size => 16384
@ -215,9 +217,10 @@ settings_init(State, Opts) ->
max_concurrent_streams, infinity),
S2 = setting_from_opt(S1, Opts, initial_stream_window_size,
initial_window_size, 65535),
%% @todo max_frame_size
S3 = setting_from_opt(S2, Opts, max_frame_size_received,
max_frame_size, 16384),
%% @todo max_header_list_size
Settings = setting_from_opt(S2, Opts, enable_connect_protocol,
Settings = setting_from_opt(S3, Opts, enable_connect_protocol,
enable_connect_protocol, false),
State#state{next_settings=Settings}.
@ -810,10 +813,15 @@ send_data(State=#state{local_window=ConnWindow},
Stream=#stream{local_window=StreamWindow}, IsFin, Data, In)
when ConnWindow =< 0; StreamWindow =< 0 ->
{State, queue_data(Stream, IsFin, Data, In)};
send_data(State=#state{socket=Socket, transport=Transport, local_window=ConnWindow},
send_data(State=#state{socket=Socket, transport=Transport, opts=Opts,
remote_settings=RemoteSettings, local_window=ConnWindow},
Stream=#stream{id=StreamID, local_window=StreamWindow}, IsFin, Data, In) ->
MaxFrameSize = 16384, %% @todo Use the real SETTINGS_MAX_FRAME_SIZE set by the client.
MaxSendSize = min(min(ConnWindow, StreamWindow), MaxFrameSize),
RemoteMaxFrameSize = maps:get(max_frame_size, RemoteSettings, 16384),
ConfiguredMaxFrameSize = maps:get(max_frame_size_sent, Opts, infinity),
MaxSendSize = min(
min(ConnWindow, StreamWindow),
min(RemoteMaxFrameSize, ConfiguredMaxFrameSize)
),
case Data of
{sendfile, Offset, Bytes, Path} when Bytes =< MaxSendSize ->
Transport:send(Socket, cow_http2:data_header(StreamID, IsFin, Bytes)),

View file

@ -28,15 +28,19 @@ groups() -> [{clear, [parallel], ct_helper:all(?MODULE)}].
init_routes(_) -> [
{"localhost", [
{"/", hello_h, []},
{"/echo/:key", echo_h, []},
{"/resp_iolist_body", resp_iolist_body_h, []}
]}
].
%% Do a prior knowledge handshake (function copied from rfc7540_SUITE).
%% Do a prior knowledge handshake (function originally copied from rfc7540_SUITE).
do_handshake(Config) ->
do_handshake(#{}, Config).
do_handshake(Settings, Config) ->
{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(#{})]),
ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(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),
@ -81,6 +85,38 @@ initial_connection_window_size(Config) ->
ConfiguredSize = Size + 65535,
ok.
max_frame_size_sent(Config) ->
doc("Confirm that frames sent by Cowboy are limited in size "
"by the max_frame_size_sent configuration value."),
MaxFrameSize = 20000,
ProtoOpts = #{
env => #{dispatch => cowboy_router:compile(init_routes(Config))},
max_frame_size_sent => MaxFrameSize
},
{ok, _} = cowboy:start_clear(name(), [{port, 0}], ProtoOpts),
Port = ranch:get_port(name()),
{ok, Socket} = do_handshake(#{max_frame_size => MaxFrameSize + 10000}, [{port, Port}|Config]),
%% Send a request with a 30000 bytes body.
{HeadersBlock, _} = cow_hpack:encode([
{<<":method">>, <<"POST">>},
{<<":scheme">>, <<"http">>},
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
{<<":path">>, <<"/echo/read_body">>}
]),
ok = gen_tcp:send(Socket, [
cow_http2:headers(1, nofin, HeadersBlock),
cow_http2:data(1, nofin, <<0:16384/unit:8>>),
cow_http2:data(1, fin, <<0:13616/unit:8>>)
]),
%% Receive a HEADERS frame as a response.
{ok, <<Len:24, 1:8, _:40>>} = gen_tcp:recv(Socket, 9, 6000),
{ok, _} = gen_tcp:recv(Socket, Len, 6000),
%% The DATA frames following must have lengths of 20000
%% and then 10000 due to the limit.
{ok, <<20000:24, 0:8, _:40, _:20000/unit:8>>} = gen_tcp:recv(Socket, 20009, 6000),
{ok, <<10000:24, 0:8, _:40, _:10000/unit:8>>} = gen_tcp:recv(Socket, 10009, 6000),
ok.
preface_timeout_infinity(Config) ->
doc("Ensure infinity for preface_timeout is accepted."),
ProtoOpts = #{

View file

@ -1277,65 +1277,63 @@ max_frame_size_reject_larger_than_default(Config) ->
{ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
%% @todo We need configurable SETTINGS in Cowboy for these tests.
%% max_frame_size_config_reject_too_small(Config) ->
%% doc("SETTINGS_MAX_FRAME_SIZE configuration values smaller than "
%% "16384 must be rejected. (RFC7540 6.5.2)"),
%% %% @todo This requires us to have a configurable SETTINGS in Cowboy.
%% todo.
%%
%% max_frame_size_config_reject_too_large(Config) ->
%% doc("SETTINGS_MAX_FRAME_SIZE configuration values larger than "
%% "16777215 must be rejected. (RFC7540 6.5.2)"),
%% %% @todo This requires us to have a configurable SETTINGS in Cowboy.
%% todo.
%%
%% max_frame_size_allow_exactly_custom(Config) ->
%% doc("An endpoint that sets SETTINGS_MAX_FRAME_SIZE must allow frames "
%% "of up to that size. (RFC7540 4.2, RFC7540 6.5.2)"),
%% %% @todo This requires us to have a configurable SETTINGS in Cowboy.
%% todo.
%%
%% max_frame_size_reject_larger_than_custom(Config) ->
%% doc("An endpoint that sets SETTINGS_MAX_FRAME_SIZE must reject frames "
%% "of up to that size with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.5.2)"),
%% %% @todo This requires us to have a configurable SETTINGS in Cowboy.
%% todo.
%% @todo How do I test this?
%%
%% max_frame_size_client_default_respect_limits(Config) ->
%% doc("The server must not send frame sizes of more "
%% "than 16384 by default. (RFC7540 4.1, RFC7540 4.2)"),
%% This is about the client sending a SETTINGS frame.
max_frame_size_client_override_reject_too_small(Config) ->
doc("A SETTINGS_MAX_FRAME_SIZE smaller than 16384 must be rejected "
"with a PROTOCOL_ERROR connection error. (RFC7540 6.5.2)"),
max_frame_size_allow_exactly_custom(Config0) ->
doc("An endpoint that sets SETTINGS_MAX_FRAME_SIZE must allow frames "
"of up to that size. (RFC7540 4.2, RFC7540 6.5.2)"),
%% Create a new listener that sets the maximum frame size to 30000.
Config = cowboy_test:init_http(name(), #{
env => #{dispatch => cowboy_router:compile(init_routes(Config0))},
max_frame_size_received => 30000
}, Config0),
%% Do the handshake.
{ok, Socket} = do_handshake(Config),
%% Send a SETTINGS frame with a SETTINGS_MAX_FRAME_SIZE lower than 16384.
ok = gen_tcp:send(Socket, << 6:24, 4:8, 0:40, 5:16, 16383:32 >>),
%% Receive a PROTOCOL_ERROR connection error.
{ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000),
%% Send a HEADERS frame initiating a stream followed by
%% a single 30000 bytes DATA frame.
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, fin, <<0:30000/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 a 25000 bytes frame.
{error, timeout} = gen_tcp:recv(Socket, 0, 1000),
ok.
%% This is about the client sending a SETTINGS frame.
max_frame_size_client_override_reject_too_large(Config) ->
doc("A SETTINGS_MAX_FRAME_SIZE larger than 16777215 must be rejected "
"with a PROTOCOL_ERROR connection error. (RFC7540 6.5.2)"),
max_frame_size_reject_larger_than_custom(Config0) ->
doc("An endpoint that sets SETTINGS_MAX_FRAME_SIZE must reject frames "
"of up to that size with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.5.2)"),
%% Create a new listener that sets the maximum frame size to 30000.
Config = cowboy_test:init_http(name(), #{
env => #{dispatch => cowboy_router:compile(init_routes(Config0))},
max_frame_size_received => 30000
}, Config0),
%% Do the handshake.
{ok, Socket} = do_handshake(Config),
%% Send a SETTINGS frame with a SETTINGS_MAX_FRAME_SIZE larger than 16777215.
ok = gen_tcp:send(Socket, << 6:24, 4:8, 0:40, 5:16, 16777216:32 >>),
%% Receive a PROTOCOL_ERROR connection error.
{ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000),
%% Send a HEADERS frame initiating a stream followed by
%% a single DATA frame larger than 30000 bytes.
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, fin, <<0:30001/unit:8>>)
]),
%% Receive a FRAME_SIZE_ERROR connection error.
{ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
%% @todo How do I test this?
%%
%% max_frame_size_client_custom_respect_limits(Config) ->
%% doc("The server must not send frame sizes of more than "
%% "client's advertised limits. (RFC7540 4.1, RFC7540 4.2)"),
%% I am using FRAME_SIZE_ERROR here because the information in the
%% frame header tells us this frame is at least 1 byte long, while
%% the given length is smaller; i.e. it is too small to contain
@ -2422,7 +2420,7 @@ continuation_with_extension_frame_interleaved_error(Config) ->
% incomplete SETTINGS frame MUST be treated as a connection error
% (Section 5.4.1) of type PROTOCOL_ERROR.
%% (RFC7540 6.5.2)
%% Settings.
settings_header_table_size_client(Config) ->
doc("The SETTINGS_HEADER_TABLE_SIZE setting can be used to "
@ -2581,7 +2579,7 @@ settings_max_concurrent_streams_0(Config0) ->
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.
%% Create a new listener that sets initial window sizes to 100000.
Config = cowboy_test:init_http(name(), #{
env => #{dispatch => cowboy_router:compile(init_routes(Config0))},
initial_connection_window_size => 100000,
@ -2630,7 +2628,7 @@ 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.
%% Create a new listener that sets the initial stream window sizes to 0.
Config = cowboy_test:init_http(name(), #{
env => #{dispatch => cowboy_router:compile(init_routes(Config0))},
initial_stream_window_size => 0
@ -2670,7 +2668,7 @@ 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.
%% Create a new listener that sets the initial stream window sizes to 0.
Config = cowboy_test:init_http(name(), #{
env => #{dispatch => cowboy_router:compile(init_routes(Config0))},
initial_stream_window_size => 0
@ -2711,13 +2709,56 @@ settings_initial_window_size_before_ack(Config0) ->
{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
% allowed frame size (2^24-1 or 16,777,215 octets), inclusive.
% Values outside this range MUST be treated as a connection error
% (Section 5.4.1) of type PROTOCOL_ERROR.
%
settings_max_frame_size(Config0) ->
doc("The SETTINGS_MAX_FRAME_SIZE setting can be used to "
"change the maximum frame size allowed. (RFC7540 6.5.2)"),
%% Create a new listener that sets the maximum frame size to 30000.
Config = cowboy_test:init_http(name(), #{
env => #{dispatch => cowboy_router:compile(init_routes(Config0))},
max_frame_size_received => 30000
}, Config0),
%% Do the handshake.
{ok, Socket} = do_handshake(Config),
%% Send a HEADERS frame initiating a stream followed by
%% a single 25000 bytes DATA frame.
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, fin, <<0:25000/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 a 25000 bytes frame.
{error, timeout} = gen_tcp:recv(Socket, 0, 1000),
ok.
settings_max_frame_size_reject_too_small(Config) ->
doc("A SETTINGS_MAX_FRAME_SIZE smaller than 16384 must be rejected "
"with a PROTOCOL_ERROR connection error. (RFC7540 6.5.2)"),
{ok, Socket} = do_handshake(Config),
%% Send a SETTINGS frame with a SETTINGS_MAX_FRAME_SIZE lower than 16384.
ok = gen_tcp:send(Socket, << 6:24, 4:8, 0:40, 5:16, 16383:32 >>),
%% Receive a PROTOCOL_ERROR connection error.
{ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
settings_max_frame_size_reject_too_large(Config) ->
doc("A SETTINGS_MAX_FRAME_SIZE larger than 16777215 must be rejected "
"with a PROTOCOL_ERROR connection error. (RFC7540 6.5.2)"),
{ok, Socket} = do_handshake(Config),
%% Send a SETTINGS frame with a SETTINGS_MAX_FRAME_SIZE larger than 16777215.
ok = gen_tcp:send(Socket, << 6:24, 4:8, 0:40, 5:16, 16777216:32 >>),
%% Receive a PROTOCOL_ERROR connection error.
{ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
% SETTINGS_MAX_HEADER_LIST_SIZE (0x6): This advisory setting informs a
% peer of the maximum size of header list that the sender is
% prepared to accept, in octets. The value is based on the