Просмотр исходного кода

Add the max_concurrent_streams h2 option

Loïc Hoguin 7 лет назад
Родитель
Сommit
7373822b86
4 измененных файлов с 104 добавлено и 46 удалено
  1. 7 1
      doc/src/manual/cowboy_http2.asciidoc
  2. 20 3
      src/cowboy_http2.erl
  3. 2 1
      test/h2spec_SUITE.erl
  4. 75 41
      test/rfc7540_SUITE.erl

+ 7 - 1
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.
 

+ 20 - 3
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},

+ 2 - 1
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.
 

+ 75 - 41
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)