From 7373822b8645f34c03a7bf522ea170c62817d8e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Wed, 25 Apr 2018 21:32:58 +0200 Subject: Add the max_concurrent_streams h2 option --- doc/src/manual/cowboy_http2.asciidoc | 8 ++- src/cowboy_http2.erl | 23 ++++++- test/h2spec_SUITE.erl | 3 +- test/rfc7540_SUITE.erl | 116 ++++++++++++++++++++++------------- 4 files changed, 104 insertions(+), 46 deletions(-) diff --git a/doc/src/manual/cowboy_http2.asciidoc b/doc/src/manual/cowboy_http2.asciidoc index 5ce809a..4c47fea 100644 --- a/doc/src/manual/cowboy_http2.asciidoc +++ b/doc/src/manual/cowboy_http2.asciidoc @@ -21,6 +21,7 @@ opts() :: #{ 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()], @@ -55,6 +56,9 @@ env (#{}):: inactivity_timeout (300000):: Time in ms with nothing received at all before Cowboy closes the connection. +max_concurrent_streams (infinity):: + Maximum number of concurrent streams allowed on 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 @@ -79,7 +83,9 @@ stream_handlers ([cowboy_stream_h]):: == Changelog -* *2.4*: Add the options `max_decode_table_size` and `max_encode_table_size`. +* *2.4*: Add the options `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`. * *2.0*: Protocol introduced. diff --git a/src/cowboy_http2.erl b/src/cowboy_http2.erl index c8522ce..3bcd402 100644 --- a/src/cowboy_http2.erl +++ b/src/cowboy_http2.erl @@ -27,6 +27,7 @@ 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()], @@ -206,11 +207,12 @@ init(Parent, Ref, Socket, Transport, Opts, Peer, Sock, Cert, Buffer, _Settings, settings_init(State, Opts) -> S0 = setting_from_opt(#{}, Opts, max_decode_table_size, header_table_size, 4096), - %% @todo max_concurrent_streams + enforce it + S1 = setting_from_opt(S0, Opts, max_concurrent_streams, + max_concurrent_streams, infinity), %% @todo initial_window_size %% @todo max_frame_size %% @todo max_header_list_size - Settings = setting_from_opt(S0, Opts, enable_connect_protocol, + Settings = setting_from_opt(S1, Opts, enable_connect_protocol, enable_connect_protocol, false), State#state{next_settings=Settings}. @@ -868,13 +870,24 @@ terminate_all_streams([#stream{id=StreamID, state=StreamState}|Tail], Reason) -> stream_decode_init(State=#state{decode_state=DecodeState0}, StreamID, IsFin, HeaderBlock) -> try cow_hpack:decode(HeaderBlock, DecodeState0) of {Headers, DecodeState} -> - stream_pseudo_headers_init(State#state{decode_state=DecodeState}, + stream_enforce_concurrency_limit(State#state{decode_state=DecodeState}, StreamID, IsFin, Headers) catch _:_ -> terminate(State, {connection_error, compression_error, 'Error while trying to decode HPACK-encoded header block. (RFC7540 4.3)'}) end. +stream_enforce_concurrency_limit(State=#state{opts=Opts, streams=Streams}, + StreamID, IsFin, Headers) -> + MaxConcurrentStreams = maps:get(max_concurrent_streams, Opts, infinity), + case length(Streams) < MaxConcurrentStreams of + true -> + stream_pseudo_headers_init(State, StreamID, IsFin, Headers); + false -> + stream_refused(State, StreamID, + 'Maximum number of concurrent streams has been reached. (RFC7540 5.1.2)') + end. + stream_pseudo_headers_init(State=#state{local_settings=LocalSettings}, StreamID, IsFin, Headers0) -> IsExtendedConnectEnabled = maps:get(enable_connect_protocol, LocalSettings, false), @@ -1045,6 +1058,10 @@ stream_malformed(State=#state{socket=Socket, transport=Transport}, StreamID, _) Transport:send(Socket, cow_http2:rst_stream(StreamID, protocol_error)), State. +stream_refused(State=#state{socket=Socket, transport=Transport}, StreamID, _) -> + Transport:send(Socket, cow_http2:rst_stream(StreamID, refused_stream)), + State. + stream_early_error(State0=#state{ref=Ref, opts=Opts, peer=Peer, local_settings=#{initial_window_size := RemoteWindow}, remote_settings=#{initial_window_size := LocalWindow}, diff --git a/test/h2spec_SUITE.erl b/test/h2spec_SUITE.erl index 3429bb3..1eb18ee 100644 --- a/test/h2spec_SUITE.erl +++ b/test/h2spec_SUITE.erl @@ -30,7 +30,8 @@ init_per_suite(Config) -> skip; _ -> cowboy_test:init_http2(h2spec, #{ - env => #{dispatch => init_dispatch()} + env => #{dispatch => init_dispatch()}, + max_concurrent_streams => 100 }, Config) end. diff --git a/test/rfc7540_SUITE.erl b/test/rfc7540_SUITE.erl index c125f17..14e38fd 100644 --- a/test/rfc7540_SUITE.erl +++ b/test/rfc7540_SUITE.erl @@ -49,6 +49,7 @@ init_routes(_) -> [ {"localhost", [ {"/", hello_h, []}, {"/echo/:key", echo_h, []}, + {"/long_polling", long_polling_h, []}, {"/resp/:key[/:arg]", resp_h, []} ]} ]. @@ -2263,34 +2264,6 @@ reject_streamid_lower(Config) -> % frame so that the client is forced to open a new connection for new % streams. -%% @todo We need this option too. (RFC7540 5.1.2) -% A peer can limit the number of concurrently active streams using the -% SETTINGS_MAX_CONCURRENT_STREAMS parameter (see Section 6.5.2) within -% a SETTINGS frame. The maximum concurrent streams setting is specific -% to each endpoint and applies only to the peer that receives the -% setting. That is, clients specify the maximum number of concurrent -% streams the server can initiate, and servers specify the maximum -% number of concurrent streams the client can initiate. -% -% Streams that are in the "open" state or in either of the "half- -% closed" states count toward the maximum number of streams that an -% endpoint is permitted to open. Streams in any of these three states -% count toward the limit advertised in the -% SETTINGS_MAX_CONCURRENT_STREAMS setting. Streams in either of the -% "reserved" states do not count toward the stream limit. -% -% Endpoints MUST NOT exceed the limit set by their peer. An endpoint -% that receives a HEADERS frame that causes its advertised concurrent -% stream limit to be exceeded MUST treat this as a stream error -% (Section 5.4.2) of type PROTOCOL_ERROR or REFUSED_STREAM. The choice -% of error code determines whether the endpoint wishes to enable -% automatic retry (see Section 8.1.4) for details). -% -% An endpoint that wishes to reduce the value of -% SETTINGS_MAX_CONCURRENT_STREAMS to a value that is below the current -% number of open streams can either close streams that exceed the new -% value or allow streams to complete. - %% (RFC7540 5.2.1) % 3. Flow control is directional with overall control provided by the % receiver. A receiver MAY choose to set any window size that it @@ -2539,21 +2512,72 @@ settings_header_table_size_server(Config0) -> % 0. An endpoint that has both set this parameter to 0 and had it % acknowledged MUST treat the receipt of a PUSH_PROMISE frame as a % connection error (Section 5.4.1) of type PROTOCOL_ERROR. +%% @todo settings_disable_push + +settings_max_concurrent_streams(Config0) -> + doc("The SETTINGS_MAX_CONCURRENT_STREAMS setting can be used to " + "restrict the number of concurrent streams. (RFC7540 5.1.2, 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))}, + max_concurrent_streams => 1 + }, Config0), + {ok, Socket} = do_handshake(Config), + %% Send two HEADERS frames as two separate streams. + Headers = [ + {<<":method">>, <<"GET">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. + {<<":path">>, <<"/long_polling">>} + ], + {ReqHeadersBlock1, EncodeState} = cow_hpack:encode(Headers), + {ReqHeadersBlock2, _} = cow_hpack:encode(Headers, EncodeState), + ok = gen_tcp:send(Socket, [ + cow_http2:headers(1, fin, ReqHeadersBlock1), + cow_http2:headers(3, fin, ReqHeadersBlock2) + ]), + %% Receive a REFUSED_STREAM stream error. + {ok, << _:24, 3:8, _:8, 3:32, 7:32 >>} = gen_tcp:recv(Socket, 13, 6000), + ok. + +settings_max_concurrent_streams_0(Config0) -> + doc("The SETTINGS_MAX_CONCURRENT_STREAMS setting can be set to " + "0 to refuse all incoming streams. (RFC7540 5.1.2, 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))}, + max_concurrent_streams => 0 + }, Config0), + {ok, Socket} = do_handshake(Config), + %% Send a HEADERS frame. + {HeadersBlock, _} = cow_hpack:encode([ + {<<":method">>, <<"GET">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. + {<<":path">>, <<"/long_polling">>} + ]), + ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), + %% Receive a REFUSED_STREAM stream error. + {ok, << _:24, 3:8, _:8, 1:32, 7:32 >>} = gen_tcp:recv(Socket, 13, 6000), + ok. + +%% @todo The client can limit the number of concurrent streams too. (RFC7540 5.1.2) % -% SETTINGS_MAX_CONCURRENT_STREAMS (0x3): Indicates the maximum number -% of concurrent streams that the sender will allow. This limit is -% directional: it applies to the number of streams that the sender -% permits the receiver to create. Initially, there is no limit to -% this value. It is recommended that this value be no smaller than -% 100, so as to not unnecessarily limit parallelism. -% -% A value of 0 for SETTINGS_MAX_CONCURRENT_STREAMS SHOULD NOT be -% treated as special by endpoints. A zero value does prevent the -% creation of new streams; however, this can also happen for any -% limit that is exhausted with active streams. Servers SHOULD only -% set a zero value for short durations; if a server does not wish to -% accept requests, closing the connection is more appropriate. +% A peer can limit the number of concurrently active streams using the +% SETTINGS_MAX_CONCURRENT_STREAMS parameter (see Section 6.5.2) within +% a SETTINGS frame. The maximum concurrent streams setting is specific +% to each endpoint and applies only to the peer that receives the +% setting. That is, clients specify the maximum number of concurrent +% streams the server can initiate, and servers specify the maximum +% number of concurrent streams the client can initiate. % +% Endpoints MUST NOT exceed the limit set by their peer. An endpoint +% that receives a HEADERS frame that causes its advertised concurrent +% stream limit to be exceeded MUST treat this as a stream error +% (Section 5.4.2) of type PROTOCOL_ERROR or REFUSED_STREAM. The choice +% 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 @@ -2566,6 +2590,16 @@ settings_header_table_size_server(Config0) -> % Values outside this range MUST be treated as a connection error % (Section 5.4.1) of type PROTOCOL_ERROR. % +% 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 +% uncompressed size of header fields, including the length of the +% name and value in octets plus an overhead of 32 octets for each +% header field. +% +% For any given request, a lower limit than what is advertised MAY +% be enforced. The initial value of this setting is unlimited. +% % An endpoint that receives a SETTINGS frame with any unknown or % unsupported identifier MUST ignore that setting. (6.5.2 and 6.5.3) -- cgit v1.2.3