aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/cowboy_http2.erl12
-rw-r--r--test/rfc7540_SUITE.erl399
2 files changed, 410 insertions, 1 deletions
diff --git a/src/cowboy_http2.erl b/src/cowboy_http2.erl
index cbcaf79..a221692 100644
--- a/src/cowboy_http2.erl
+++ b/src/cowboy_http2.erl
@@ -295,6 +295,9 @@ parse(State=#state{local_settings=#{max_frame_size := MaxFrameSize},
{continuation, _, _, _} ->
parse(continuation_frame(State, Frame), Rest)
end;
+ {ignore, _} when element(1, ParseState) =:= continuation ->
+ terminate(State, {connection_error, protocol_error,
+ 'An invalid frame was received in the middle of a header block. (RFC7540 6.2)'});
{ignore, Rest} ->
parse(State, Rest);
{stream_error, StreamID, Reason, Human, Rest} ->
@@ -422,6 +425,10 @@ frame(State, {ping_ack, _Opaque}) ->
frame(State, Frame={goaway, _, _, _}) ->
terminate(State, {stop, Frame, 'Client is going away.'});
%% Connection-wide WINDOW_UPDATE frame.
+frame(State=#state{local_window=ConnWindow}, {window_update, Increment})
+ when ConnWindow + Increment > 2147483647 ->
+ terminate(State, {connection_error, flow_control_error,
+ 'The flow control window must not be greater than 2^31-1. (RFC7540 6.9.1)'});
frame(State=#state{local_window=ConnWindow}, {window_update, Increment}) ->
send_data(State#state{local_window=ConnWindow + Increment});
%% Stream-specific WINDOW_UPDATE frame.
@@ -431,6 +438,9 @@ frame(State=#state{client_streamid=LastStreamID}, {window_update, StreamID, _})
'WINDOW_UPDATE frame received on a stream in idle state. (RFC7540 5.1)'});
frame(State0=#state{streams=Streams0}, {window_update, StreamID, Increment}) ->
case lists:keyfind(StreamID, #stream.id, Streams0) of
+ #stream{local_window=StreamWindow} when StreamWindow + Increment > 2147483647 ->
+ stream_reset(State0, StreamID, {stream_error, flow_control_error,
+ 'The flow control window must not be greater than 2^31-1. (RFC7540 6.9.1)'});
Stream0 = #stream{local_window=StreamWindow} ->
{State, Stream} = send_data(State0,
Stream0#stream{local_window=StreamWindow + Increment}),
@@ -457,7 +467,7 @@ continuation_frame(State=#state{parse_state={continuation, StreamID, IsFin, Head
<< HeaderBlockFragment0/binary, HeaderBlockFragment1/binary >>}};
continuation_frame(State, _) ->
terminate(State, {connection_error, protocol_error,
- 'An invalid frame was received while expecting a CONTINUATION frame. (RFC7540 6.2)'}).
+ 'An invalid frame was received in the middle of a header block. (RFC7540 6.2)'}).
down(State=#state{children=Children0}, Pid, Msg) ->
case cowboy_children:down(Children0, Pid) of
diff --git a/test/rfc7540_SUITE.erl b/test/rfc7540_SUITE.erl
index 605db4a..747d625 100644
--- a/test/rfc7540_SUITE.erl
+++ b/test/rfc7540_SUITE.erl
@@ -2251,3 +2251,402 @@ reject_streamid_lower(Config) ->
% that is unable to establish a new stream identifier can send a GOAWAY
% 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
+% desires for each stream and for the entire connection. A sender
+% MUST respect flow-control limits imposed by a receiver. Clients,
+% servers, and intermediaries all independently advertise their
+% flow-control window as a receiver and abide by the flow-control
+% limits set by their peer when sending.
+%
+% 4. The initial value for the flow-control window is 65,535 octets
+% for both new streams and the overall connection.
+%
+% 5. The frame type determines whether flow control applies to a
+% frame. Of the frames specified in this document, only DATA
+% frames are subject to flow control; all other frame types do not
+% consume space in the advertised flow-control window. This
+% ensures that important control frames are not blocked by flow
+% control.
+%
+% 6. Flow control cannot be disabled.
+
+%% (RFC7540 5.2.2)
+% Even with full awareness of the current bandwidth-delay product,
+% implementation of flow control can be difficult. When using flow
+% control, the receiver MUST read from the TCP receive buffer in a
+% timely fashion. Failure to do so could lead to a deadlock when
+% critical frames, such as WINDOW_UPDATE, are not read and acted upon.
+
+%% @todo Stream priorities. (RFC7540 5.3 5.3.x)
+
+%% (RFC7540 5.4.1)
+% An endpoint that encounters a connection error SHOULD first send a
+% GOAWAY frame (Section 6.8) with the stream identifier of the last
+% stream that it successfully received from its peer. The GOAWAY frame
+% includes an error code that indicates why the connection is
+% terminating. After sending the GOAWAY frame for an error condition,
+% the endpoint MUST close the TCP connection.
+%
+% An endpoint can end a connection at any time. In particular, an
+% endpoint MAY choose to treat a stream error as a connection error.
+% Endpoints SHOULD send a GOAWAY frame when ending a connection,
+% providing that circumstances permit it.
+
+%% (RFC7540 5.4.2)
+% A RST_STREAM is the last frame that an endpoint can send on a stream.
+% The peer that sends the RST_STREAM frame MUST be prepared to receive
+% any frames that were sent or enqueued for sending by the remote peer.
+% These frames can be ignored, except where they modify connection
+% state (such as the state maintained for header compression
+% (Section 4.3) or flow control).
+%
+% Normally, an endpoint SHOULD NOT send more than one RST_STREAM frame
+% for any stream. However, an endpoint MAY send additional RST_STREAM
+% frames if it receives frames on a closed stream after more than a
+% round-trip time. This behavior is permitted to deal with misbehaving
+% implementations.
+%
+% To avoid looping, an endpoint MUST NOT send a RST_STREAM in response
+% to a RST_STREAM frame.
+
+%% (RFC7540 5.5)
+% Extensions are permitted to use new frame types (Section 4.1), new
+% settings (Section 6.5.2), or new error codes (Section 7). Registries
+% are established for managing these extension points: frame types
+% (Section 11.2), settings (Section 11.3), and error codes
+% (Section 11.4).
+%
+% Implementations MUST ignore unknown or unsupported values in all
+% extensible protocol elements. Implementations MUST discard frames
+% that have unknown or unsupported types. This means that any of these
+% extension points can be safely used by extensions without prior
+% arrangement or negotiation. However, extension frames that appear in
+% the middle of a header block (Section 4.3) are not permitted; these
+% MUST be treated as a connection error (Section 5.4.1) of type
+% PROTOCOL_ERROR.
+
+continuation_with_extension_frame_interleaved_error(Config) ->
+ doc("Extension frames interleaved in a header block must be rejected "
+ "with a PROTOCOL_ERROR connection error. "
+ "(RFC7540 4.3, RFC7540 5.5, RFC7540 6.2, RFC7540 6.10)"),
+ {ok, Socket} = do_handshake(Config),
+ %% Send an unterminated HEADERS frame followed by an extension frame.
+ ok = gen_tcp:send(Socket, [
+ << 0:24, 1:8, 0:7, 1:1, 0:1, 1:31 >>,
+ << 0:24, 128:8, 0:8, 0:32 >>
+ ]),
+ %% Receive a PROTOCOL_ERROR connection error.
+ {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000),
+ ok.
+
+%% (RFC7540 6.1) DATA
+% Padding: Padding octets that contain no application semantic value.
+% Padding octets MUST be set to zero when sending. A receiver is
+% not obligated to verify padding but MAY treat non-zero padding as
+% a connection error (Section 5.4.1) of type PROTOCOL_ERROR.
+%
+% DATA frames MUST be associated with a stream. If a DATA frame is
+% received whose stream identifier field is 0x0, the recipient MUST
+% respond with a connection error (Section 5.4.1) of type
+% PROTOCOL_ERROR.
+
+%% (RFC7540 6.2) HEADERS
+% Padding: Padding octets that contain no application semantic value.
+% Padding octets MUST be set to zero when sending. A receiver is
+% not obligated to verify padding but MAY treat non-zero padding as
+% a connection error (Section 5.4.1) of type PROTOCOL_ERROR.
+%
+% A HEADERS frame carries the END_STREAM flag that signals the end
+% of a stream. However, a HEADERS frame with the END_STREAM flag
+% set can be followed by CONTINUATION frames on the same stream.
+% Logically, the CONTINUATION frames are part of the HEADERS frame.
+%
+%% @todo We probably need a test for the server sending HEADERS too large.
+% The payload of a HEADERS frame contains a header block fragment
+% (Section 4.3). A header block that does not fit within a HEADERS
+% frame is continued in a CONTINUATION frame (Section 6.10).
+%
+% HEADERS frames MUST be associated with a stream. If a HEADERS frame
+% is received whose stream identifier field is 0x0, the recipient MUST
+% respond with a connection error (Section 5.4.1) of type
+% PROTOCOL_ERROR.
+
+%% (RFC7540 6.3) PRIORITY
+% The PRIORITY frame always identifies a stream. If a PRIORITY frame
+% is received with a stream identifier of 0x0, the recipient MUST
+% respond with a connection error (Section 5.4.1) of type
+% PROTOCOL_ERROR.
+
+%% (RFC7540 6.4) RST_STREAM
+% The RST_STREAM frame fully terminates the referenced stream and
+% causes it to enter the "closed" state. After receiving a RST_STREAM
+% on a stream, the receiver MUST NOT send additional frames for that
+% stream, with the exception of PRIORITY. However, after sending the
+% RST_STREAM, the sending endpoint MUST be prepared to receive and
+% process additional frames sent on the stream that might have been
+% sent by the peer prior to the arrival of the RST_STREAM.
+%
+% RST_STREAM frames MUST be associated with a stream. If a RST_STREAM
+% frame is received with a stream identifier of 0x0, the recipient MUST
+% treat this as a connection error (Section 5.4.1) of type
+% PROTOCOL_ERROR.
+
+%% (RFC7540 6.5) SETTINGS
+% A SETTINGS frame MUST be sent by both endpoints at the start of a
+% connection and MAY be sent at any other time by either endpoint over
+% the lifetime of the connection. Implementations MUST support all of
+% the parameters defined by this specification.
+%
+% SETTINGS frames always apply to a connection, never a single stream.
+% The stream identifier for a SETTINGS frame MUST be zero (0x0). If an
+% endpoint receives a SETTINGS frame whose stream identifier field is
+% anything other than 0x0, the endpoint MUST respond with a connection
+% error (Section 5.4.1) of type PROTOCOL_ERROR.
+%
+% The SETTINGS frame affects connection state. A badly formed or
+% incomplete SETTINGS frame MUST be treated as a connection error
+% (Section 5.4.1) of type PROTOCOL_ERROR.
+
+%% (RFC7540 6.5.2)
+% SETTINGS_ENABLE_PUSH (0x2): This setting can be used to disable
+% server push (Section 8.2). An endpoint MUST NOT send a
+% PUSH_PROMISE frame if it receives this parameter set to a value of
+% 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.
+%
+% 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.
+%
+% 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
+% FLOW_CONTROL_ERROR.
+%
+% 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.
+%
+% An endpoint that receives a SETTINGS frame with any unknown or
+% unsupported identifier MUST ignore that setting. (6.5.2 and 6.5.3)
+
+%% (RFC7540 6.5.3)
+% Upon receiving a SETTINGS frame with the ACK flag set, the
+% sender of the altered parameters can rely on the setting having been
+% applied.
+%
+% If the sender of a SETTINGS frame does not receive an acknowledgement
+% within a reasonable amount of time, it MAY issue a connection error
+% (Section 5.4.1) of type SETTINGS_TIMEOUT.
+
+%% (RFC7540 6.6) PUSH_PROMISE
+% @todo PUSH_PROMISE frames have a reserved bit in the payload that must be ignored.
+%
+% Padding: Padding octets that contain no application semantic value.
+% Padding octets MUST be set to zero when sending. A receiver is
+% not obligated to verify padding but MAY treat non-zero padding as
+% a connection error (Section 5.4.1) of type PROTOCOL_ERROR.
+%
+% PUSH_PROMISE frames MUST only be sent on a peer-initiated stream that
+% is in either the "open" or "half-closed (remote)" state. The stream
+% identifier of a PUSH_PROMISE frame indicates the stream it is
+% associated with. If the stream identifier field specifies the value
+% 0x0, a recipient MUST respond with a connection error (Section 5.4.1)
+% of type PROTOCOL_ERROR.
+%
+% PUSH_PROMISE MUST NOT be sent if the SETTINGS_ENABLE_PUSH setting of
+% the peer endpoint is set to 0. An endpoint that has set this setting
+% and has received acknowledgement MUST treat the receipt of a
+% PUSH_PROMISE frame as a connection error (Section 5.4.1) of type
+% PROTOCOL_ERROR.
+%
+% Since PUSH_PROMISE reserves a stream, ignoring a PUSH_PROMISE frame
+% causes the stream state to become indeterminate. A receiver MUST
+% treat the receipt of a PUSH_PROMISE on a stream that is neither
+% "open" nor "half-closed (local)" as a connection error
+% (Section 5.4.1) of type PROTOCOL_ERROR.
+%
+% A receiver MUST treat the receipt of a PUSH_PROMISE that promises an
+% illegal stream identifier (Section 5.1.1) as a connection error
+% (Section 5.4.1) of type PROTOCOL_ERROR. Note that an illegal stream
+% identifier is an identifier for a stream that is not currently in the
+% "idle" state.
+
+%% (RFC7540 6.7) PING
+% PING frames are not associated with any individual stream. If a PING
+% frame is received with a stream identifier field value other than
+% 0x0, the recipient MUST respond with a connection error
+% (Section 5.4.1) of type PROTOCOL_ERROR.
+
+%% (RFC7540 6.8) GOAWAY
+% @todo GOAWAY frames have a reserved bit in the payload that must be ignored.
+%
+%% @todo We should eventually implement the mechanism for gracefully
+%% shutting down of the connection. (Send the GOAWAY, finish processing
+%% the current set of streams, give up after a certain timeout.)
+%
+%% @todo If we graceful shutdown and receive a GOAWAY, we give up too.
+% A GOAWAY frame might not immediately precede closing of the
+% connection; a receiver of a GOAWAY that has no more use for the
+% connection SHOULD still send a GOAWAY frame before terminating the
+% connection.
+%
+%% @todo And it gets more complex when you think about h1 to h2 proxies.
+% A server that is attempting to gracefully shut down a
+% connection SHOULD send an initial GOAWAY frame with the last stream
+% identifier set to 2^31-1 and a NO_ERROR code. This signals to the
+% client that a shutdown is imminent and that initiating further
+% requests is prohibited. After allowing time for any in-flight stream
+% creation (at least one round-trip time), the server can send another
+% GOAWAY frame with an updated last stream identifier. This ensures
+% that a connection can be cleanly shut down without losing requests.
+%
+%% @todo And of course even if we shutdown we need to be careful about
+%% the connection state.
+% After sending a GOAWAY frame, the sender can discard frames for
+% streams initiated by the receiver with identifiers higher than the
+% identified last stream. However, any frames that alter connection
+% state cannot be completely ignored. For instance, HEADERS,
+% PUSH_PROMISE, and CONTINUATION frames MUST be minimally processed to
+% ensure the state maintained for header compression is consistent (see
+% Section 4.3); similarly, DATA frames MUST be counted toward the
+% connection flow-control window. Failure to process these frames can
+% cause flow control or header compression state to become
+% unsynchronized.
+%
+% The GOAWAY frame applies to the connection, not a specific stream.
+% An endpoint MUST treat a GOAWAY frame with a stream identifier other
+% than 0x0 as a connection error (Section 5.4.1) of type
+% PROTOCOL_ERROR.
+
+%% (RFC7540 6.9) WINDOW_UPDATE
+% @todo WINDOW_UPDATE frames have a reserved bit in the payload that must be ignored.
+
+window_update_reject_0(Config) ->
+ doc("WINDOW_UPDATE frames with an increment of 0 for the connection "
+ "flow control window must be rejected with a "
+ "PROTOCOL_ERROR connection error. (RFC7540 6.9.1)"),
+ {ok, Socket} = do_handshake(Config),
+ %% Send connection-wide WINDOW_UPDATE frame with a value of 0.
+ ok = gen_tcp:send(Socket, [
+ cow_http2:window_update(0)
+ ]),
+ %% Receive a PROTOCOL_ERROR connection error.
+ {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000),
+ ok.
+
+window_update_reject_0_stream(Config) ->
+ doc("WINDOW_UPDATE frames with an increment of 0 for a stream "
+ "flow control window must be rejected with a "
+ "PROTOCOL_ERROR stream error. (RFC7540 6.9.1)"),
+ {ok, Socket} = do_handshake(Config),
+ %% Send a HEADERS frame immediately followed by
+ %% a WINDOW_UPDATE frame with a value of 0.
+ {HeadersBlock, _} = cow_hpack:encode([
+ {<<":method">>, <<"GET">>},
+ {<<":scheme">>, <<"http">>},
+ {<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+ {<<":path">>, <<"/">>}
+ ]),
+ ok = gen_tcp:send(Socket, [
+ cow_http2:headers(1, fin, HeadersBlock),
+ cow_http2:window_update(1, 0)
+ ]),
+ %% Receive a PROTOCOL_ERROR stream error.
+ {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000),
+ ok.
+
+% A receiver that receives a flow-controlled frame MUST always account
+% for its contribution against the connection flow-control window,
+% unless the receiver treats this as a connection error
+% (Section 5.4.1). This is necessary even if the frame is in error.
+% The sender counts the frame toward the flow-control window, but if
+% the receiver does not, the flow-control window at the sender and
+% receiver can become different.
+
+%% (RFC7540 6.9.1)
+% The sender MUST NOT
+% 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
+% in either flow-control window.
+
+window_update_reject_overflow(Config) ->
+ doc("WINDOW_UPDATE frames that cause the connection flow control "
+ "window to exceed 2^31-1 must be rejected with a "
+ "FLOW_CONTROL_ERROR connection error. (RFC7540 6.9.1)"),
+ {ok, Socket} = do_handshake(Config),
+ %% Send connection-wide WINDOW_UPDATE frame that causes the window to overflow.
+ ok = gen_tcp:send(Socket, [
+ cow_http2:window_update(2147483647)
+ ]),
+ %% Receive a FLOW_CONTROL_ERROR connection error.
+ {ok, << _:24, 7:8, _:72, 3:32 >>} = gen_tcp:recv(Socket, 17, 6000),
+ ok.
+
+window_update_reject_overflow_stream(Config) ->
+ doc("WINDOW_UPDATE frames that cause a stream flow control "
+ "window to exceed 2^31-1 must be rejected with a "
+ "FLOW_CONTROL_ERROR stream error. (RFC7540 6.9.1)"),
+ {ok, Socket} = do_handshake(Config),
+ %% Send a HEADERS frame immediately followed by a WINDOW_UPDATE
+ %% frame that causes the stream window to overflow.
+ {HeadersBlock, _} = cow_hpack:encode([
+ {<<":method">>, <<"GET">>},
+ {<<":scheme">>, <<"http">>},
+ {<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+ {<<":path">>, <<"/">>}
+ ]),
+ ok = gen_tcp:send(Socket, [
+ cow_http2:headers(1, fin, HeadersBlock),
+ cow_http2:window_update(1, 2147483647)
+ ]),
+ %% Receive a FLOW_CONTROL_ERROR stream error.
+ {ok, << _:24, 3:8, _:8, 1:32, 3:32 >>} = gen_tcp:recv(Socket, 13, 6000),
+ ok.