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

Add options to control h2's SETTINGS_HEADER_TABLE_SIZE

This commit is contained in:
Loïc Hoguin 2018-04-25 16:55:52 +02:00
parent bc79529b4d
commit 8f4adf437c
No known key found for this signature in database
GPG key ID: 8A9DF795F6FED764
4 changed files with 147 additions and 16 deletions

View file

@ -21,6 +21,8 @@ opts() :: #{
enable_connect_protocol => boolean(), enable_connect_protocol => boolean(),
env => cowboy_middleware:env(), env => cowboy_middleware:env(),
inactivity_timeout => timeout(), inactivity_timeout => timeout(),
max_decode_table_size => non_neg_integer(),
max_encode_table_size => non_neg_integer(),
middlewares => [module()], middlewares => [module()],
preface_timeout => timeout(), preface_timeout => timeout(),
shutdown_timeout => timeout(), shutdown_timeout => timeout(),
@ -45,6 +47,7 @@ connection_type (supervisor)::
enable_connect_protocol (false):: enable_connect_protocol (false)::
Whether to enable the extended CONNECT method to allow Whether to enable the extended CONNECT method to allow
protocols like Websocket to be used over an HTTP/2 stream. protocols like Websocket to be used over an HTTP/2 stream.
This option is experimental and disabled by default.
env (#{}):: env (#{})::
Middleware environment. Middleware environment.
@ -52,6 +55,16 @@ 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.
max_decode_table_size (4096)::
Maximum header table size used by the decoder. This is the value advertised
to the client. The client can then choose a header table size equal or lower
to the advertised value.
max_encode_table_size (4096)::
Maximum header table size used by the encoder. The server will compare this
value to what the client advertises and choose the smallest one as the
encoder's header table size.
middlewares ([cowboy_router, cowboy_handler]):: middlewares ([cowboy_router, cowboy_handler])::
Middlewares to run for every request. Middlewares to run for every request.
@ -66,6 +79,8 @@ stream_handlers ([cowboy_stream_h])::
== Changelog == Changelog
* *2.4*: Add the options `max_decode_table_size` and `max_encode_table_size`.
* *2.4*: Add the experimental option `enable_connect_protocol`.
* *2.0*: Protocol introduced. * *2.0*: Protocol introduced.
== See also == See also

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(),
max_decode_table_size => non_neg_integer(),
max_encode_table_size => non_neg_integer(),
middlewares => [module()], middlewares => [module()],
preface_timeout => timeout(), preface_timeout => timeout(),
shutdown_timeout => timeout(), shutdown_timeout => timeout(),
@ -93,7 +95,7 @@
%% @todo We need a TimerRef to do SETTINGS_TIMEOUT errors. %% @todo We need a TimerRef to do SETTINGS_TIMEOUT errors.
%% We need to be careful there. It's well possible that we send %% We need to be careful there. It's well possible that we send
%% two SETTINGS frames before we receive a SETTINGS ack. %% two SETTINGS frames before we receive a SETTINGS ack.
next_settings = #{} :: undefined | map(), %% @todo perhaps set to undefined by default next_settings = undefined :: undefined | map(),
remote_settings = #{ remote_settings = #{
initial_window_size => 65535 initial_window_size => 65535
} :: map(), } :: map(),
@ -201,9 +203,22 @@ init(Parent, Ref, Socket, Transport, Opts, Peer, Sock, Cert, Buffer, _Settings,
_ -> parse(State, Buffer) _ -> parse(State, Buffer)
end. end.
settings_init(State=#state{next_settings=Settings}, Opts) -> settings_init(State, Opts) ->
EnableConnectProtocol = maps:get(enable_connect_protocol, Opts, false), S0 = setting_from_opt(#{}, Opts, max_decode_table_size,
State#state{next_settings=Settings#{enable_connect_protocol => EnableConnectProtocol}}. header_table_size, 4096),
%% @todo max_concurrent_streams + enforce it
%% @todo initial_window_size
%% @todo max_frame_size
%% @todo max_header_list_size
Settings = setting_from_opt(S0, Opts, enable_connect_protocol,
enable_connect_protocol, false),
State#state{next_settings=Settings}.
setting_from_opt(Settings, Opts, OptName, SettingName, Default) ->
case maps:get(OptName, Opts, Default) of
Default -> Settings;
Value -> Settings#{SettingName => Value}
end.
preface(#state{socket=Socket, transport=Transport, next_settings=Settings}) -> preface(#state{socket=Socket, transport=Transport, next_settings=Settings}) ->
%% We send next_settings and use defaults until we get a ack. %% We send next_settings and use defaults until we get a ack.
@ -408,21 +423,32 @@ frame(State=#state{client_streamid=LastStreamID}, {rst_stream, StreamID, _})
frame(State, {rst_stream, StreamID, Reason}) -> frame(State, {rst_stream, StreamID, Reason}) ->
stream_terminate(State, StreamID, {stream_error, Reason, 'Stream reset requested by client.'}); stream_terminate(State, StreamID, {stream_error, Reason, 'Stream reset requested by client.'});
%% SETTINGS frame. %% SETTINGS frame.
frame(State0=#state{socket=Socket, transport=Transport, remote_settings=Settings0}, frame(State0=#state{socket=Socket, transport=Transport, opts=Opts,
{settings, Settings}) -> remote_settings=Settings0}, {settings, Settings}) ->
Transport:send(Socket, cow_http2:settings_ack()), Transport:send(Socket, cow_http2:settings_ack()),
State = State0#state{remote_settings=maps:merge(Settings0, Settings)}, State1 = State0#state{remote_settings=maps:merge(Settings0, Settings)},
case Settings of maps:fold(fun
#{initial_window_size := NewWindowSize} -> (header_table_size, NewSize, State=#state{encode_state=EncodeState0}) ->
MaxSize = maps:get(max_encode_table_size, Opts, 4096),
EncodeState = cow_hpack:set_max_size(min(NewSize, MaxSize), EncodeState0),
State#state{encode_state=EncodeState};
(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_stream_windows(State, NewWindowSize - OldWindowSize);
_ -> (_, _, State) ->
State State
end; end, State1, Settings);
%% Ack for a previously sent SETTINGS frame. %% Ack for a previously sent SETTINGS frame.
frame(State=#state{local_settings=Local0, next_settings=Next}, settings_ack) -> frame(State0=#state{local_settings=Local0, next_settings=NextSettings}, settings_ack) ->
Local = maps:merge(Local0, Next), Local = maps:merge(Local0, NextSettings),
State#state{local_settings=Local, next_settings=#{}}; State1 = State0#state{local_settings=Local, next_settings=#{}},
maps:fold(fun
(header_table_size, MaxSize, State=#state{decode_state=DecodeState0}) ->
DecodeState = cow_hpack:set_max_size(MaxSize, DecodeState0),
State#state{decode_state=DecodeState};
(_, _, State) ->
State
end, State1, NextSettings);
%% Unexpected PUSH_PROMISE frame. %% Unexpected PUSH_PROMISE frame.
frame(State, {push_promise, _, _, _, _}) -> frame(State, {push_promise, _, _, _, _}) ->
terminate(State, {connection_error, protocol_error, terminate(State, {connection_error, protocol_error,

View file

@ -78,7 +78,10 @@ reject_handshake_when_disabled(Config0) ->
}, Config0), }, Config0),
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 0. %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 0.
{ok, Socket, Settings} = do_handshake(Config), {ok, Socket, Settings} = do_handshake(Config),
#{enable_connect_protocol := false} = Settings, case Settings of
#{enable_connect_protocol := false} -> ok;
_ when map_size(Settings) =:= 0 -> ok
end,
%% Send a CONNECT :protocol request to upgrade the stream to Websocket. %% Send a CONNECT :protocol request to upgrade the stream to Websocket.
{ReqHeadersBlock, _} = cow_hpack:encode([ {ReqHeadersBlock, _} = cow_hpack:encode([
{<<":method">>, <<"CONNECT">>}, {<<":method">>, <<"CONNECT">>},
@ -102,7 +105,10 @@ reject_handshake_disabled_by_default(Config0) ->
}, Config0), }, Config0),
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 0. %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 0.
{ok, Socket, Settings} = do_handshake(Config), {ok, Socket, Settings} = do_handshake(Config),
#{enable_connect_protocol := false} = Settings, case Settings of
#{enable_connect_protocol := false} -> ok;
_ when map_size(Settings) =:= 0 -> ok
end,
%% Send a CONNECT :protocol request to upgrade the stream to Websocket. %% Send a CONNECT :protocol request to upgrade the stream to Websocket.
{ReqHeadersBlock, _} = cow_hpack:encode([ {ReqHeadersBlock, _} = cow_hpack:encode([
{<<":method">>, <<"CONNECT">>}, {<<":method">>, <<"CONNECT">>},

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]).
-import(cowboy_test, [raw_open/1]). -import(cowboy_test, [raw_open/1]).
-import(cowboy_test, [raw_send/2]). -import(cowboy_test, [raw_send/2]).
@ -2449,6 +2450,89 @@ continuation_with_extension_frame_interleaved_error(Config) ->
% (Section 5.4.1) of type PROTOCOL_ERROR. % (Section 5.4.1) of type PROTOCOL_ERROR.
%% (RFC7540 6.5.2) %% (RFC7540 6.5.2)
settings_header_table_size_client(Config) ->
doc("The SETTINGS_HEADER_TABLE_SIZE setting can be used to "
"inform the server of the maximum header table size "
"used by the client to decode header blocks. (RFC7540 6.5.2)"),
HeaderTableSize = 128,
%% Do the handhsake.
{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(#{header_table_size => HeaderTableSize})]),
%% Receive the server preface.
{ok, << Len0:24 >>} = gen_tcp:recv(Socket, 3, 1000),
{ok, << 4:8, 0:40, _:Len0/binary >>} = gen_tcp:recv(Socket, 6 + Len0, 1000),
%% Send the SETTINGS ack.
ok = gen_tcp:send(Socket, cow_http2:settings_ack()),
%% Receive the SETTINGS ack.
{ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000),
%% Initialize decoding/encoding states.
DecodeState = cow_hpack:set_max_size(HeaderTableSize, cow_hpack:init()),
EncodeState = cow_hpack:init(),
%% Send a HEADERS frame as a request.
{ReqHeadersBlock1, _} = cow_hpack:encode([
{<<":method">>, <<"GET">>},
{<<":scheme">>, <<"http">>},
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
{<<":path">>, <<"/">>}
], EncodeState),
ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, ReqHeadersBlock1)),
%% Receive a HEADERS frame as a response.
{ok, << Len1:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000),
{ok, RespHeadersBlock1} = gen_tcp:recv(Socket, Len1, 6000),
{RespHeaders, _} = cow_hpack:decode(RespHeadersBlock1, DecodeState),
{_, <<"200">>} = lists:keyfind(<<":status">>, 1, RespHeaders),
%% The decoding succeeded, confirming that the table size is
%% lower than or equal to HeaderTableSize.
ok.
settings_header_table_size_server(Config0) ->
doc("The SETTINGS_HEADER_TABLE_SIZE setting can be used to "
"inform the client of the maximum header table size "
"used by the server to decode header blocks. (RFC7540 6.5.2)"),
HeaderTableSize = 128,
%% Create a new listener that allows larger header table sizes.
Config = cowboy_test:init_http(name(), #{
env => #{dispatch => cowboy_router:compile(init_routes(Config0))},
max_decode_table_size => HeaderTableSize
}, Config0),
%% Do the handhsake.
{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(#{header_table_size => HeaderTableSize})]),
%% Receive the server preface.
{ok, << Len0:24 >>} = gen_tcp:recv(Socket, 3, 1000),
{ok, Data = <<_:48, _:Len0/binary>>} = gen_tcp:recv(Socket, 6 + Len0, 1000),
%% Confirm the server's SETTINGS_HEADERS_TABLE_SIZE uses HeaderTableSize.
{ok, {settings, #{header_table_size := HeaderTableSize}}, <<>>}
= cow_http2:parse(<<Len0:24, Data/binary>>),
%% Send the SETTINGS ack.
ok = gen_tcp:send(Socket, cow_http2:settings_ack()),
%% Receive the SETTINGS ack.
{ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000),
%% Initialize decoding/encoding states.
DecodeState = cow_hpack:init(),
EncodeState = cow_hpack:set_max_size(HeaderTableSize, cow_hpack:init()),
%% Send a HEADERS frame as a request.
{ReqHeadersBlock1, _} = cow_hpack:encode([
{<<":method">>, <<"GET">>},
{<<":scheme">>, <<"http">>},
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
{<<":path">>, <<"/">>}
], EncodeState),
ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, ReqHeadersBlock1)),
%% Receive a HEADERS frame as a response.
{ok, << Len1:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000),
{ok, RespHeadersBlock1} = gen_tcp:recv(Socket, Len1, 6000),
{RespHeaders, _} = cow_hpack:decode(RespHeadersBlock1, DecodeState),
{_, <<"200">>} = lists:keyfind(<<":status">>, 1, RespHeaders),
%% The decoding succeeded on the server, confirming that
%% the table size was updated to HeaderTableSize.
ok.
% SETTINGS_ENABLE_PUSH (0x2): This setting can be used to disable % SETTINGS_ENABLE_PUSH (0x2): This setting can be used to disable
% server push (Section 8.2). An endpoint MUST NOT send a % server push (Section 8.2). An endpoint MUST NOT send a
% PUSH_PROMISE frame if it receives this parameter set to a value of % PUSH_PROMISE frame if it receives this parameter set to a value of