From 9ef4536656fd6c8bd9c80c6731460c057ba2b177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Tue, 23 May 2017 14:09:38 +0200 Subject: Add many tests for RFC7540 5.1 and 5.1.1 and related fixes --- src/cowboy_http2.erl | 38 +++- test/rfc7540_SUITE.erl | 566 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 598 insertions(+), 6 deletions(-) diff --git a/src/cowboy_http2.erl b/src/cowboy_http2.erl index 85fdeef..0661296 100644 --- a/src/cowboy_http2.erl +++ b/src/cowboy_http2.erl @@ -284,12 +284,17 @@ parse_settings_preface(State, _, _, _) -> %% and terminate the stream if this is the end of it. %% DATA frame. -frame(State=#state{remote_window=ConnWindow, streams=Streams}, +frame(State=#state{client_streamid=LastStreamID}, {data, StreamID, _, _}) + when StreamID > LastStreamID -> + terminate(State, {connection_error, protocol_error, + 'DATA frame received on a stream in idle state. (RFC7540 5.1)'}); +frame(State0=#state{remote_window=ConnWindow, streams=Streams}, {data, StreamID, IsFin0, Data}) -> + DataLen = byte_size(Data), + State = State0#state{remote_window=ConnWindow - DataLen}, case lists:keyfind(StreamID, #stream.id, Streams) of Stream = #stream{state=StreamState0, remote=nofin, remote_window=StreamWindow, body_length=Len0} -> - DataLen = byte_size(Data), Len = Len0 + DataLen, IsFin = case IsFin0 of fin -> {fin, Len}; @@ -297,7 +302,7 @@ frame(State=#state{remote_window=ConnWindow, streams=Streams}, end, try cowboy_stream:data(StreamID, IsFin, Data, StreamState0) of {Commands, StreamState} -> - commands(State#state{remote_window=ConnWindow - DataLen}, + commands(State, Stream#stream{state=StreamState, remote_window=StreamWindow - DataLen, body_length=Len}, Commands) catch Class:Reason -> @@ -307,10 +312,23 @@ frame(State=#state{remote_window=ConnWindow, streams=Streams}, stream_reset(State, StreamID, {internal_error, {Class, Reason}, 'Exception occurred in cowboy_stream:data/4.'}) end; - _ -> + #stream{remote=fin} -> stream_reset(State, StreamID, {stream_error, stream_closed, - 'DATA frame received for a closed or non-existent stream. (RFC7540 6.1)'}) + 'DATA frame received for a half-closed (remote) stream. (RFC7540 5.1)'}); + false -> + %% @todo What about RST_STREAM? Sigh. + terminate(State, {connection_error, stream_closed, + 'DATA frame received for a closed stream. (RFC7540 5.1)'}) end; +%% HEADERS frame with invalid even-numbered streamid. +frame(State, {headers, StreamID, _, _, _}) when StreamID rem 2 =:= 0 -> + terminate(State, {connection_error, protocol_error, + 'HEADERS frame received with even-numbered streamid. (RFC7540 5.1.1)'}); +%% HEADERS frame received on (half-)closed stream. +frame(State=#state{client_streamid=LastStreamID}, {headers, StreamID, _, _, _}) + when StreamID =< LastStreamID -> + stream_reset(State, StreamID, {stream_error, stream_closed, + 'HEADERS frame received on a stream in closed or half-closed state. (RFC7540 5.1)'}); %% Single HEADERS frame headers block. frame(State, {headers, StreamID, IsFin, head_fin, HeaderBlock}) -> %% @todo We probably need to validate StreamID here and in 4 next clauses. @@ -334,6 +352,10 @@ frame(State, {priority, _StreamID, _IsExclusive, _DepStreamID, _Weight}) -> %% @todo Handle priority. State; %% RST_STREAM frame. +frame(State=#state{client_streamid=LastStreamID}, {rst_stream, StreamID, _}) + when StreamID > LastStreamID -> + terminate(State, {connection_error, protocol_error, + 'RST_STREAM frame received on a stream in idle state. (RFC7540 5.1)'}); frame(State, {rst_stream, StreamID, Reason}) -> stream_terminate(State, StreamID, {stream_error, Reason, 'Stream reset requested by client.'}); %% SETTINGS frame. @@ -365,6 +387,10 @@ frame(State, Frame={goaway, _, _, _}) -> frame(State=#state{local_window=ConnWindow}, {window_update, Increment}) -> send_data(State#state{local_window=ConnWindow + Increment}); %% Stream-specific WINDOW_UPDATE frame. +frame(State=#state{client_streamid=LastStreamID}, {window_update, StreamID, _}) + when StreamID > LastStreamID -> + terminate(State, {connection_error, protocol_error, + '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 Stream0 = #stream{local_window=StreamWindow} -> @@ -372,8 +398,8 @@ frame(State0=#state{streams=Streams0}, {window_update, StreamID, Increment}) -> Stream0#stream{local_window=StreamWindow + Increment}), Streams = lists:keystore(StreamID, #stream.id, Streams0, Stream), State#state{streams=Streams}; + %% @todo We must reject WINDOW_UPDATE frames on RST_STREAM closed streams. false -> - %% @todo Receiving this frame on a stream in the idle state is an error. %% WINDOW_UPDATE frames may be received for a short period of time %% after a stream is closed. They must be ignored. State0 diff --git a/test/rfc7540_SUITE.erl b/test/rfc7540_SUITE.erl index 6ce5253..727ea0c 100644 --- a/test/rfc7540_SUITE.erl +++ b/test/rfc7540_SUITE.erl @@ -1670,3 +1670,569 @@ continuation_wrong_stream_error(Config) -> %% Receive a PROTOCOL_ERROR connection error. {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. + +%% Stream states. + +idle_stream_reject_data(Config) -> + doc("DATA frames received on an idle stream must be rejected " + "with a PROTOCOL_ERROR connection error. (RFC7540 5.1)"), + {ok, Socket} = do_handshake(Config), + %% Send a DATA frame on an idle stream. + ok = gen_tcp:send(Socket, cow_http2:data(1, fin, <<"Unexpected DATA frame.">>)), + %% Receive a PROTOCOL_ERROR connection error. + {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), + ok. + +idle_stream_accept_headers(Config) -> + doc("HEADERS frames received on an idle stream must be accepted. (RFC7540 5.1)"), + {ok, Socket} = do_handshake(Config), + %% Send a HEADERS frame on an idle stream. + {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)), + %% Receive a HEADERS frame as a response. + {ok, << _:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), + ok. + +idle_stream_accept_priority(Config) -> + doc("PRIORITY frames received on an idle stream must be accepted. (RFC7540 5.1)"), + {ok, Socket} = do_handshake(Config), + %% Send a PRIORITY frame on an idle stream. + ok = gen_tcp:send(Socket, cow_http2:priority(1, shared, 3, 123)), + %% Receive no error. + {error, timeout} = gen_tcp:recv(Socket, 7, 1000), + %% Send a HEADERS frame on the same stream. + {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)), + %% Receive a HEADERS frame as a response. + {ok, << _:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), + ok. + +idle_stream_reject_rst_stream(Config) -> + doc("RST_STREAM frames received on an idle stream must be rejected " + "with a PROTOCOL_ERROR connection error. (RFC7540 5.1)"), + {ok, Socket} = do_handshake(Config), + %% Send an RST_STREAM frame on an idle stream. + ok = gen_tcp:send(Socket, cow_http2:rst_stream(1, no_error)), + %% Receive a PROTOCOL_ERROR connection error. + {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), + ok. + +idle_stream_reject_push_promise(Config) -> + doc("PUSH_PROMISE frames received on an idle stream must be rejected " + "with a PROTOCOL_ERROR connection error. (RFC7540 5.1)"), + {ok, Socket} = do_handshake(Config), + %% Send a PUSH_PROMISE frame on an idle stream. + {HeadersBlock, _} = cow_hpack:encode([ + {<<":method">>, <<"GET">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. + {<<":path">>, <<"/">>} + ]), + ok = gen_tcp:send(Socket, cow_http2:push_promise(1, 3, HeadersBlock)), + %% Receive a PROTOCOL_ERROR connection error. + {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), + ok. + +idle_stream_reject_window_update(Config) -> + doc("WINDOW_UPDATE frames received on an idle stream must be rejected " + "with a PROTOCOL_ERROR connection error. (RFC7540 5.1)"), + {ok, Socket} = do_handshake(Config), + %% Send a WINDOW_UPDATE frame on an idle stream. + ok = gen_tcp:send(Socket, cow_http2:window_update(1, 12345)), + %% Receive a PROTOCOL_ERROR connection error. + {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), + ok. + +%reserved (local) - after sending PUSH_PROMISE: +% An endpoint MUST NOT send any type of frame other than HEADERS, +% RST_STREAM, or PRIORITY in this state. +%%% how to test this? +% +% A PRIORITY or WINDOW_UPDATE frame MAY be received in this state. +% Receiving any type of frame other than RST_STREAM, PRIORITY, or +% WINDOW_UPDATE on a stream in this state MUST be treated as a +% connection error (Section 5.4.1) of type PROTOCOL_ERROR. +%%% we need to use a large enough file for this +% +%reserved_local_reject_data +%reserved_local_reject_headers +%reserved_local_accept_priority +%reserved_local_accept_rst_stream +%reserved_local_reject_push_promise %% do we even care? we reject it always +%reserved_local_accept_window_update +% +%half-closed (remote): +% If an endpoint receives additional frames, other than +% WINDOW_UPDATE, PRIORITY, or RST_STREAM, for a stream that is in +% this state, it MUST respond with a stream error (Section 5.4.2) of +% type STREAM_CLOSED. + +half_closed_remote_reject_data(Config) -> + doc("DATA frames received on a half-closed (remote) stream must be rejected " + "with a STREAM_CLOSED stream error. (RFC7540 5.1)"), + {ok, Socket} = do_handshake(Config), + %% Send a HEADERS frame with the FIN flag set. + {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)), + %% Send a DATA frame on that now half-closed (remote) stream. + ok = gen_tcp:send(Socket, cow_http2:data(1, fin, <<"Unexpected DATA frame.">>)), + %% Receive a STREAM_CLOSED stream error. + {ok, << _:24, 3:8, _:8, 1:32, 5:32 >>} = gen_tcp:recv(Socket, 13, 6000), + ok. + +half_closed_remote_reject_headers(Config) -> + doc("HEADERS frames received on a half-closed (remote) stream must be rejected " + "with a STREAM_CLOSED stream error. (RFC7540 5.1)"), + {ok, Socket} = do_handshake(Config), + %% Send a HEADERS frame with the FIN flag set. + {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)), + %% Send a HEADERS frame on that now half-closed (remote) stream. + ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), + %% Receive a STREAM_CLOSED stream error. + {ok, << _:24, 3:8, _:8, 1:32, 5:32 >>} = gen_tcp:recv(Socket, 13, 6000), + ok. + +half_closed_remote_accept_priority(Config) -> + doc("PRIORITY frames received on a half-closed stream must be accepted. (RFC7540 5.1)"), + {ok, Socket} = do_handshake(Config), + %% Send a HEADERS frame with the FIN flag set. + {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)), + %% Send a PRIORITY frame on that now half-closed (remote) stream. + ok = gen_tcp:send(Socket, cow_http2:priority(1, shared, 3, 123)), + %% Receive a HEADERS frame as a response. + {ok, << _:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), + ok. + +half_closed_remote_accept_rst_stream(Config) -> + doc("RST_STREAM frames received on a half-closed stream must be accepted. (RFC7540 5.1)"), + {ok, Socket} = do_handshake(Config), + %% Send a HEADERS frame with the FIN flag set. + {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)), + %% Send an RST_STREAM frame on that now half-closed (remote) stream. + ok = gen_tcp:send(Socket, cow_http2:rst_stream(1, no_error)), + %% Receive nothing back. + {error, timeout} = gen_tcp:recv(Socket, 9, 6000), + ok. + +%% half_closed_remote_reject_push_promise +%% +%% We respond to all PUSH_PROMISE frames with a PROTOCOL_ERROR connection error +%% because PUSH is disabled in that direction. We therefore cannot test other +%% error conditions. + +half_closed_remote_accept_window_update(Config) -> + doc("WINDOW_UPDATE frames received on a half-closed stream must be accepted. (RFC7540 5.1)"), + {ok, Socket} = do_handshake(Config), + %% Send a HEADERS frame with the FIN flag set. + {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)), + %% Send a WINDOW_UPDATE frame on that now half-closed (remote) stream. + ok = gen_tcp:send(Socket, cow_http2:window_update(1, 12345)), + %% Receive a HEADERS frame as a response. + {ok, << _:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), + ok. + +rst_stream_closed_reject_data(Config) -> + doc("DATA frames received on a stream closed via RST_STREAM must be rejected " + "with a STREAM_CLOSED stream error. (RFC7540 5.1)"), + {ok, Socket} = do_handshake(Config), + %% Send a HEADERS frame. + {HeadersBlock, _} = cow_hpack:encode([ + {<<":method">>, <<"GET">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. + {<<":path">>, <<"/">>} + ]), + ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, HeadersBlock)), + %% Send an RST_STREAM frame to close the stream. + ok = gen_tcp:send(Socket, cow_http2:rst_stream(1, cancel)), + %% Send a DATA frame on the now RST_STREAM closed stream. + ok = gen_tcp:send(Socket, cow_http2:data(1, fin, <<"Unexpected DATA frame.">>)), + %% Receive a STREAM_CLOSED stream error. + {ok, << _:24, 3:8, _:8, 1:32, 5:32 >>} = gen_tcp:recv(Socket, 13, 6000), + ok. + +rst_stream_closed_reject_headers(Config) -> + doc("HEADERS frames received on a stream closed via RST_STREAM must be rejected " + "with a STREAM_CLOSED stream error. (RFC7540 5.1)"), + {ok, Socket} = do_handshake(Config), + %% Send a HEADERS frame. + {HeadersBlock, _} = cow_hpack:encode([ + {<<":method">>, <<"GET">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. + {<<":path">>, <<"/">>} + ]), + ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, HeadersBlock)), + %% Send an RST_STREAM frame to close the stream. + ok = gen_tcp:send(Socket, cow_http2:rst_stream(1, cancel)), + %% Send a HEADERS frame on the now RST_STREAM closed stream. + ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, HeadersBlock)), + %% Receive a STREAM_CLOSED stream error. + {ok, << _:24, 3:8, _:8, 1:32, 5:32 >>} = gen_tcp:recv(Socket, 13, 6000), + ok. + +rst_stream_closed_accept_priority(Config) -> + doc("PRIORITY frames received on a stream closed via RST_STREAM " + "must be accepted. (RFC7540 5.1)"), + {ok, Socket} = do_handshake(Config), + %% Send a HEADERS frame. + {HeadersBlock, _} = cow_hpack:encode([ + {<<":method">>, <<"GET">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. + {<<":path">>, <<"/">>} + ]), + ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, HeadersBlock)), + %% Send an RST_STREAM frame to close the stream. + ok = gen_tcp:send(Socket, cow_http2:rst_stream(1, cancel)), + %% Send a PRIORITY frame on that now RST_STREAM closed stream. + ok = gen_tcp:send(Socket, cow_http2:priority(1, shared, 3, 123)), + %% Receive nothing back. + {error, timeout} = gen_tcp:recv(Socket, 9, 6000), + ok. + +rst_stream_closed_ignore_rst_stream(Config) -> + doc("RST_STREAM frames received on a stream closed via RST_STREAM " + "must be ignored to avoid looping. (RFC7540 5.1, RFC7540 5.4.2)"), + {ok, Socket} = do_handshake(Config), + %% Send a HEADERS frame. + {HeadersBlock, _} = cow_hpack:encode([ + {<<":method">>, <<"GET">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. + {<<":path">>, <<"/">>} + ]), + ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, HeadersBlock)), + %% Send an RST_STREAM frame to close the stream. + ok = gen_tcp:send(Socket, cow_http2:rst_stream(1, cancel)), + %% Send an extra RST_STREAM. + ok = gen_tcp:send(Socket, cow_http2:rst_stream(1, cancel)), + %% Receive nothing back. + {error, timeout} = gen_tcp:recv(Socket, 9, 6000), + ok. + +%% rst_stream_closed_reject_push_promise +%% +%% We respond to all PUSH_PROMISE frames with a PROTOCOL_ERROR connection error +%% because PUSH is disabled in that direction. We therefore cannot test other +%% error conditions. + +rst_stream_closed_reject_window_update(Config) -> + doc("WINDOW_UPDATE frames received on a stream closed via RST_STREAM " + "must be rejected with a STREAM_CLOSED stream error. (RFC7540 5.1)"), + {ok, Socket} = do_handshake(Config), + %% Send a HEADERS frame. + {HeadersBlock, _} = cow_hpack:encode([ + {<<":method">>, <<"GET">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. + {<<":path">>, <<"/">>} + ]), + ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, HeadersBlock)), + %% Send an RST_STREAM frame to close the stream. + ok = gen_tcp:send(Socket, cow_http2:rst_stream(1, cancel)), + %% Send a WINDOW_UPDATE frame on the now RST_STREAM closed stream. + ok = gen_tcp:send(Socket, cow_http2:window_update(1, 12345)), + %% Receive a STREAM_CLOSED stream error. + {ok, << _:24, 3:8, _:8, 1:32, 5:32 >>} = gen_tcp:recv(Socket, 13, 6000), + ok. + +stream_closed_reject_data(Config) -> + doc("DATA frames received on a stream closed normally must be rejected " + "with a STREAM_CLOSED connection error. (RFC7540 5.1)"), + {ok, Socket} = do_handshake(Config), + %% Send a HEADERS frame. + {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)), + %% Receive the response. + {ok, << Length1:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), + {ok, _} = gen_tcp:recv(Socket, Length1, 6000), + {ok, << Length2:24, 0:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), + {ok, _} = gen_tcp:recv(Socket, Length2, 6000), + %% Send a DATA frame on the now closed stream. + ok = gen_tcp:send(Socket, cow_http2:data(1, fin, <<"Unexpected DATA frame.">>)), + %% Receive a STREAM_CLOSED connection error. + {ok, << _:24, 7:8, _:72, 5:32 >>} = gen_tcp:recv(Socket, 17, 6000), + ok. + +stream_closed_reject_headers(Config) -> + doc("HEADERS frames received on a stream closed normally must be rejected " + "with a STREAM_CLOSED connection error. (RFC7540 5.1)"), + {ok, Socket} = do_handshake(Config), + %% Send a HEADERS frame. + {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)), + %% Receive the response. + {ok, << Length1:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), + {ok, _} = gen_tcp:recv(Socket, Length1, 6000), + {ok, << Length2:24, 0:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), + {ok, _} = gen_tcp:recv(Socket, Length2, 6000), + %% Send a HEADERS frame on the now closed stream. + ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), + %% Receive a STREAM_CLOSED connection error. + {ok, << _:24, 7:8, _:72, 5:32 >>} = gen_tcp:recv(Socket, 17, 6000), + ok. + +stream_closed_accept_priority(Config) -> + doc("PRIORITY frames received on a stream closed normally must be accepted. (RFC7540 5.1)"), + {ok, Socket} = do_handshake(Config), + %% Send a HEADERS frame. + {HeadersBlock, _} = cow_hpack:encode([ + {<<":method">>, <<"GET">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. + {<<":path">>, <<"/">>} + ]), + ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, HeadersBlock)), + %% Receive the response. + {ok, << Length1:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), + {ok, _} = gen_tcp:recv(Socket, Length1, 6000), + {ok, << Length2:24, 0:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), + {ok, _} = gen_tcp:recv(Socket, Length2, 6000), + %% Send a PRIORITY frame on the now closed stream. + ok = gen_tcp:send(Socket, cow_http2:priority(1, shared, 3, 123)), + %% Receive nothing back. + {error, timeout} = gen_tcp:recv(Socket, 9, 6000), + ok. + +stream_closed_accept_rst_stream(Config) -> + doc("RST_STREAM frames received on a stream closed normally " + "must be accepted for a short period. (RFC7540 5.1)"), + {ok, Socket} = do_handshake(Config), + %% Send a HEADERS frame. + {HeadersBlock, _} = cow_hpack:encode([ + {<<":method">>, <<"GET">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. + {<<":path">>, <<"/">>} + ]), + ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, HeadersBlock)), + %% Receive the response. + {ok, << Length1:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), + {ok, _} = gen_tcp:recv(Socket, Length1, 6000), + {ok, << Length2:24, 0:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), + {ok, _} = gen_tcp:recv(Socket, Length2, 6000), + %% Send an RST_STREAM frame on the now closed stream. + ok = gen_tcp:send(Socket, cow_http2:rst_stream(1, cancel)), + %% Receive nothing back. + {error, timeout} = gen_tcp:recv(Socket, 9, 6000), + ok. + +%% stream_closed_reject_push_promise +%% +%% We respond to all PUSH_PROMISE frames with a PROTOCOL_ERROR connection error +%% because PUSH is disabled in that direction. We therefore cannot test other +%% error conditions. + +stream_closed_accept_window_update(Config) -> + doc("WINDOW_UPDATE frames received on a stream closed normally " + "must be accepted for a short period. (RFC7540 5.1)"), + {ok, Socket} = do_handshake(Config), + %% Send a HEADERS frame. + {HeadersBlock, _} = cow_hpack:encode([ + {<<":method">>, <<"GET">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. + {<<":path">>, <<"/">>} + ]), + ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, HeadersBlock)), + %% Receive the response. + {ok, << Length1:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), + {ok, _} = gen_tcp:recv(Socket, Length1, 6000), + {ok, << Length2:24, 0:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), + {ok, _} = gen_tcp:recv(Socket, Length2, 6000), + %% Send a WINDOW_UPDATE frame on the now closed stream. + ok = gen_tcp:send(Socket, cow_http2:window_update(1, 12345)), + %% Receive nothing back. + {error, timeout} = gen_tcp:recv(Socket, 9, 6000), + ok. + +%% @todo While we accept RST_STREAM and WINDOW_UPDATE for a short period +%% after the stream closed normally, we may want to reject the ones coming +%% a significant amount of time after that. + +%% @todo Frames may arrive on a stream after we send an RST_STREAM for it. +%% They must be ignored for a short period of time: +% +% If this state is reached as a result of sending a RST_STREAM +% frame, the peer that receives the RST_STREAM might have already +% sent -- or enqueued for sending -- frames on the stream that +% cannot be withdrawn. An endpoint MUST ignore frames that it +% receives on closed streams after it has sent a RST_STREAM frame. +% An endpoint MAY choose to limit the period over which it ignores +% frames and treat frames that arrive after this time as being in +% error. + +%% @todo Ensure that rejected DATA frames result in the connection +%% flow-control window being updated. How to test this? +% +% Flow-controlled frames (i.e., DATA) received after sending +% RST_STREAM are counted toward the connection flow-control window. +% Even though these frames might be ignored, because they are sent +% before the sender receives the RST_STREAM, the sender will +% consider the frames to count against the flow-control window. + +%% Stream identifiers. + +reject_streamid_even(Config) -> + doc("HEADERS frames received with an even-numbered streamid " + "must be rejected with a PROTOCOL_ERROR connection error. (RFC7540 5.1.1)"), + {ok, Socket} = do_handshake(Config), + %% Send a HEADERS frame with an even-numbered streamid. + {HeadersBlock, _} = cow_hpack:encode([ + {<<":method">>, <<"GET">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. + {<<":path">>, <<"/">>} + ]), + ok = gen_tcp:send(Socket, cow_http2:headers(2, fin, HeadersBlock)), + %% Receive a PROTOCOL_ERROR connection error. + {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), + ok. + +reject_streamid_0(Config) -> + doc("HEADERS frames received with streamid 0 (zero) " + "must be rejected with a PROTOCOL_ERROR connection error. (RFC7540 5.1.1)"), + {ok, Socket} = do_handshake(Config), + %% Send a HEADERS frame with an streamid 0. + {HeadersBlock, _} = cow_hpack:encode([ + {<<":method">>, <<"GET">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. + {<<":path">>, <<"/">>} + ]), + ok = gen_tcp:send(Socket, cow_http2:headers(0, fin, HeadersBlock)), + %% Receive a PROTOCOL_ERROR connection error. + {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), + ok. + +http_upgrade_reject_reuse_streamid_1(Config) -> + doc("Attempts to reuse streamid 1 after upgrading to HTTP/2 " + "must be rejected with a PROTOCOL_ERROR connection error. (RFC7540 5.1.1)"), + {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), + ok = gen_tcp:send(Socket, [ + "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: Upgrade, HTTP2-Settings\r\n" + "Upgrade: h2c\r\n" + "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", + "\r\n"]), + ok = do_recv_101(Socket), + %% Send a valid preface. + ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2: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), + %% Send the SETTINGS ack. + ok = gen_tcp:send(Socket, cow_http2:settings_ack()), + %% Receive the SETTINGS ack, and the response HEADERS and DATA (Stream ID 1). + Received = lists:reverse(lists:foldl(fun(_, Acc) -> + case gen_tcp:recv(Socket, 9, 1000) of + {ok, << 0:24, 4:8, 1:8, 0:32 >>} -> + [settings_ack|Acc]; + {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} -> + {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), + [headers|Acc]; + {ok, << SkipLen:24, 0:8, _:8, 1:32 >>} -> + {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), + [data|Acc] + end + end, [], [1, 2, 3])), + case Received of + [settings_ack, headers, data] -> ok; + [headers, settings_ack, data] -> ok; + [headers, data, settings_ack] -> ok + end, + %% Send a HEADERS frame with streamid 1. + {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)), + %% Receive a PROTOCOL_ERROR connection error. + {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), + ok. + +reject_streamid_lower(Config) -> + doc("HEADERS frames received with streamid lower than the previous stream " + "must be rejected with a PROTOCOL_ERROR connection error. (RFC7540 5.1.1)"), + {ok, Socket} = do_handshake(Config), + %% Send a HEADERS frame with streamid 5. + {HeadersBlock, _} = cow_hpack:encode([ + {<<":method">>, <<"GET">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. + {<<":path">>, <<"/">>} + ]), + ok = gen_tcp:send(Socket, cow_http2:headers(5, fin, HeadersBlock)), + %% Receive the response. + {ok, << Length1:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), + {ok, _} = gen_tcp:recv(Socket, Length1, 6000), + {ok, << Length2:24, 0:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), + {ok, _} = gen_tcp:recv(Socket, Length2, 6000), + %% Send a HEADERS frame with streamid 3. + ok = gen_tcp:send(Socket, cow_http2:headers(3, fin, HeadersBlock)), + %% Receive a PROTOCOL_ERROR connection error. + {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), + ok. + +%% @todo We need an option to limit the number of streams one can open +%% on a connection. And we need to enforce it. +% +% Stream identifiers cannot be reused. Long-lived connections can +% result in an endpoint exhausting the available range of stream +% identifiers. A server +% 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. -- cgit v1.2.3