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

Add many test cases covering RFC7540 4.2

These tests cover frame sizes. It's mostly edge cases for sure
(ie misbehaving clients and us having to reject them properly).
I had these almost ready for a long time, so I'm glad I can
push them out.

This requires updating Cowlib too (we currently track master).
This commit is contained in:
Loïc Hoguin 2017-02-25 20:05:31 +01:00
parent 2af0506d51
commit a3636adcac
No known key found for this signature in database
GPG key ID: 71366FF21851DF03
2 changed files with 447 additions and 12 deletions

View file

@ -52,7 +52,14 @@
%% @todo Since the ack is required, we must timeout if we don't receive it. %% @todo Since the ack is required, we must timeout if we don't receive it.
%% @todo I haven't put as much thought as I should have on this, %% @todo I haven't put as much thought as I should have on this,
%% the final settings handling will be very different. %% the final settings handling will be very different.
local_settings = #{} :: map(), local_settings = #{
% header_table_size => 4096,
% enable_push => false, %% We are the server. Push is never enabled.
% max_concurrent_streams => infinity,
% initial_window_size => 65535,
max_frame_size => 16384
% max_header_list_size => infinity
} :: map(),
%% @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.
@ -60,8 +67,8 @@
remote_settings = #{} :: map(), remote_settings = #{} :: map(),
%% Stream identifiers. %% Stream identifiers.
client_streamid = 0 :: non_neg_integer(),
server_streamid = 2 :: pos_integer(), server_streamid = 2 :: pos_integer(),
%% @todo last known good streamid
%% Currently active HTTP/2 streams. Streams may be initiated either %% Currently active HTTP/2 streams. Streams may be initiated either
%% by the client or by the server through PUSH_PROMISE frames. %% by the client or by the server through PUSH_PROMISE frames.
@ -212,8 +219,9 @@ parse(State=#state{socket=Socket, transport=Transport, parse_state={preface, seq
end end
end; end;
%% @todo Perhaps instead of just more we can have {more, Len} to avoid all the checks. %% @todo Perhaps instead of just more we can have {more, Len} to avoid all the checks.
parse(State=#state{parse_state=ParseState}, Data) -> parse(State=#state{local_settings=#{max_frame_size := MaxFrameSize},
case cow_http2:parse(Data) of parse_state=ParseState}, Data) ->
case cow_http2:parse(Data, MaxFrameSize) of
{ok, Frame, Rest} -> {ok, Frame, Rest} ->
case ParseState of case ParseState of
normal -> normal ->
@ -529,13 +537,22 @@ sendfile(Socket, Transport, StreamID, IsFin, Offset, Bytes, Path, Length) ->
end. end.
-spec terminate(#state{}, _) -> no_return(). -spec terminate(#state{}, _) -> no_return().
terminate(#state{socket=Socket, transport=Transport, terminate(undefined, Reason) ->
exit({shutdown, Reason});
terminate(#state{socket=Socket, transport=Transport, client_streamid=LastStreamID,
streams=Streams, children=Children}, Reason) -> streams=Streams, children=Children}, Reason) ->
%% @todo Send GOAWAY frame; need to keep track of last good stream id; how? %% @todo We might want to optionally send the Reason value
%% as debug data in the GOAWAY frame here. Perhaps more.
Transport:send(Socket, cow_http2:goaway(LastStreamID, terminate_reason(Reason), <<>>)),
terminate_all_streams(Streams, Reason, Children), terminate_all_streams(Streams, Reason, Children),
Transport:close(Socket), Transport:close(Socket),
exit({shutdown, Reason}). exit({shutdown, Reason}).
terminate_reason({connection_error, Reason, _}) -> Reason;
terminate_reason({stop, _, _}) -> no_error;
terminate_reason({socket_error, _, _}) -> internal_error;
terminate_reason({internal_error, _, _}) -> internal_error.
terminate_all_streams([], _, []) -> terminate_all_streams([], _, []) ->
ok; ok;
terminate_all_streams([#stream{id=StreamID, state=StreamState}|Tail], Reason, Children0) -> terminate_all_streams([#stream{id=StreamID, state=StreamState}|Tail], Reason, Children0) ->
@ -603,7 +620,8 @@ stream_init(State0=#state{ref=Ref, socket=Socket, transport=Transport, peer=Peer
stream_handler_init(State=#state{opts=Opts}, StreamID, IsFin, Req) -> stream_handler_init(State=#state{opts=Opts}, StreamID, IsFin, Req) ->
try cowboy_stream:init(StreamID, Req, Opts) of try cowboy_stream:init(StreamID, Req, Opts) of
{Commands, StreamState} -> {Commands, StreamState} ->
commands(State, #stream{id=StreamID, state=StreamState, remote=IsFin}, Commands) commands(State#state{client_streamid=StreamID},
#stream{id=StreamID, state=StreamState, remote=IsFin}, Commands)
catch Class:Reason -> catch Class:Reason ->
error_logger:error_msg("Exception occurred in " error_logger:error_msg("Exception occurred in "
"cowboy_stream:init(~p, ~p, ~p) with reason ~p:~p.", "cowboy_stream:init(~p, ~p, ~p) with reason ~p:~p.",

View file

@ -44,7 +44,8 @@ end_per_group(Name, _) ->
init_routes(_) -> [ init_routes(_) -> [
{"localhost", [ {"localhost", [
{"/", hello_h, []} {"/", hello_h, []},
{"/echo/:key", echo_h, []}
]} ]}
]. ].
@ -773,12 +774,10 @@ prior_knowledge_client_preface_settings_ack_timeout(Config) ->
{ok, << _:24, 7:8, _:72, 4:32 >>} = gen_tcp:recv(Socket, 17, 6000), {ok, << _:24, 7:8, _:72, 4:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok. ok.
prior_knowledge(Config) -> %% Do a prior knowledge handshake.
doc("Streams can be initiated after a successful HTTP/2 connection " do_handshake(Config) ->
"with prior knowledge of server capabilities. (RFC7540 3.4)"),
{ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]),
%% Send a valid preface. %% Send a valid preface.
%% @todo Use non-empty SETTINGS here. Just because.
ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]),
%% Receive the server preface. %% Receive the server preface.
{ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000),
@ -787,6 +786,13 @@ prior_knowledge(Config) ->
ok = gen_tcp:send(Socket, cow_http2:settings_ack()), ok = gen_tcp:send(Socket, cow_http2:settings_ack()),
%% Receive the SETTINGS ack. %% Receive the SETTINGS ack.
{ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000),
{ok, Socket}.
prior_knowledge(Config) ->
doc("Streams can be initiated after a successful HTTP/2 connection "
"with prior knowledge of server capabilities. (RFC7540 3.4)"),
%% @todo Use non-empty SETTINGS here. Just because.
{ok, Socket} = do_handshake(Config),
%% Wait until after the SETTINGS ack timeout was supposed to trigger. %% Wait until after the SETTINGS ack timeout was supposed to trigger.
receive after 6000 -> ok end, receive after 6000 -> ok end,
%% Send a PING. %% Send a PING.
@ -802,3 +808,414 @@ prior_knowledge(Config) ->
%% without waiting for the 101 response (3.2, 3.5) %% without waiting for the 101 response (3.2, 3.5)
%% * Prior knowledge handshake fails (3.4) %% * Prior knowledge handshake fails (3.4)
%% * ALPN selects HTTP/1.1 (3.3) %% * ALPN selects HTTP/1.1 (3.3)
%% Frame size.
max_frame_size_allow_exactly_default(Config) ->
doc("All implementations must allow frame sizes of at least 16384. (RFC7540 4.1, RFC7540 4.2)"),
{ok, Socket} = do_handshake(Config),
%% Send a POST request with a DATA frame of exactly 16384 bytes.
{HeadersBlock, _} = cow_hpack:encode([
{<<":method">>, <<"POST">>},
{<<":scheme">>, <<"http">>},
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
{<<":path">>, <<"/echo/read_body">>}
]),
ok = gen_tcp:send(Socket, [
cow_http2:headers(1, nofin, HeadersBlock),
cow_http2:data(1, fin, << 0:16384/unit:8 >>)
]),
%% Receive a response with the same DATA frame.
{ok, << SkipLen:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000),
{ok, _} = gen_tcp:recv(Socket, SkipLen, 1000),
{ok, << 16384:24, 0:8, 1:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000),
{ok, << 0:16384/unit:8 >>} = gen_tcp:recv(Socket, 16384, 1000),
ok.
max_frame_size_reject_larger_than_default(Config) ->
doc("A FRAME_SIZE_ERROR connection error must be sent when receiving "
"frames larger than the default 16384 length. (RFC7540 4.1, RFC7540 4.2)"),
{ok, Socket} = do_handshake(Config),
%% Send a POST request with a DATA frame larger than 16384 bytes.
{HeadersBlock, _} = cow_hpack:encode([
{<<":method">>, <<"POST">>},
{<<":scheme">>, <<"http">>},
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
{<<":path">>, <<"/echo/read_body">>}
]),
ok = gen_tcp:send(Socket, [
cow_http2:headers(1, nofin, HeadersBlock),
cow_http2:data(1, fin, << 0:16385/unit:8 >>)
]),
%% Receive a FRAME_SIZE_ERROR connection error.
{ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
%% @todo We need configurable SETTINGS in Cowboy for these tests.
%% max_frame_size_config_reject_too_small(Config) ->
%% doc("SETTINGS_MAX_FRAME_SIZE configuration values smaller than "
%% "16384 must be rejected. (RFC7540 6.5.2)"),
%% %% @todo This requires us to have a configurable SETTINGS in Cowboy.
%% todo.
%%
%% max_frame_size_config_reject_too_large(Config) ->
%% doc("SETTINGS_MAX_FRAME_SIZE configuration values larger than "
%% "16777215 must be rejected. (RFC7540 6.5.2)"),
%% %% @todo This requires us to have a configurable SETTINGS in Cowboy.
%% todo.
%%
%% max_frame_size_allow_exactly_custom(Config) ->
%% doc("An endpoint that sets SETTINGS_MAX_FRAME_SIZE must allow frames "
%% "of up to that size. (RFC7540 4.2, RFC7540 6.5.2)"),
%% %% @todo This requires us to have a configurable SETTINGS in Cowboy.
%% todo.
%%
%% max_frame_size_reject_larger_than_custom(Config) ->
%% doc("An endpoint that sets SETTINGS_MAX_FRAME_SIZE must reject frames "
%% "of up to that size with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.5.2)"),
%% %% @todo This requires us to have a configurable SETTINGS in Cowboy.
%% todo.
%% @todo How do I test this?
%%
%% max_frame_size_client_default_respect_limits(Config) ->
%% doc("The server must not send frame sizes of more "
%% "than 16384 by default. (RFC7540 4.1, RFC7540 4.2)"),
%% This is about the client sending a SETTINGS frame.
max_frame_size_client_override_reject_too_small(Config) ->
doc("A SETTINGS_MAX_FRAME_SIZE smaller than 16384 must be rejected "
"with a PROTOCOL_ERROR connection error. (RFC7540 6.5.2)"),
{ok, Socket} = do_handshake(Config),
%% Send a SETTINGS frame with a SETTINGS_MAX_FRAME_SIZE lower than 16384.
ok = gen_tcp:send(Socket, << 6:24, 4:8, 0:40, 5:16, 16383:32 >>),
%% Receive a PROTOCOL_ERROR connection error.
{ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
%% This is about the client sending a SETTINGS frame.
max_frame_size_client_override_reject_too_large(Config) ->
doc("A SETTINGS_MAX_FRAME_SIZE larger than 16777215 must be rejected "
"with a PROTOCOL_ERROR connection error. (RFC7540 6.5.2)"),
{ok, Socket} = do_handshake(Config),
%% Send a SETTINGS frame with a SETTINGS_MAX_FRAME_SIZE larger than 16777215.
ok = gen_tcp:send(Socket, << 6:24, 4:8, 0:40, 5:16, 16777216:32 >>),
%% Receive a PROTOCOL_ERROR connection error.
{ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
%% @todo How do I test this?
%%
%% max_frame_size_client_custom_respect_limits(Config) ->
%% doc("The server must not send frame sizes of more than "
%% "client's advertised limits. (RFC7540 4.1, RFC7540 4.2)"),
%% I am using FRAME_SIZE_ERROR here because the information in the
%% frame header tells us this frame is at least 1 byte long, while
%% the given length is smaller; i.e. it is too small to contain
%% mandatory frame data (the pad length).
data_reject_frame_size_0_padded_flag(Config) ->
doc("DATA frames of size 0 with the PADDED flag set must be rejected "
"with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.1)"),
{ok, Socket} = do_handshake(Config),
%% Send a POST request with an incorrect padded DATA frame size.
{HeadersBlock, _} = cow_hpack:encode([
{<<":method">>, <<"POST">>},
{<<":scheme">>, <<"http">>},
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
{<<":path">>, <<"/echo/read_body">>}
]),
ok = gen_tcp:send(Socket, [
cow_http2:headers(1, nofin, HeadersBlock),
<< 0:24, 0:8, 0:4, 1:1, 0:2, 1:1, 0:1, 1:31 >>
]),
%% Receive a FRAME_SIZE_ERROR connection error.
{ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
%% This case on the other hand is noted specifically in the RFC
%% as being a PROTOCOL_ERROR. It can be thought of as the Pad Length
%% being incorrect, rather than the frame size.
data_reject_frame_size_too_small_padded_flag(Config) ->
doc("DATA frames with Pad Length >= Length must be rejected "
"with a PROTOCOL_ERROR connection error. (RFC7540 6.1)"),
{ok, Socket} = do_handshake(Config),
%% Send a POST request with an incorrect padded DATA frame size.
{HeadersBlock, _} = cow_hpack:encode([
{<<":method">>, <<"POST">>},
{<<":scheme">>, <<"http">>},
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
{<<":path">>, <<"/echo/read_body">>}
]),
ok = gen_tcp:send(Socket, [
cow_http2:headers(1, nofin, HeadersBlock),
<< 10:24, 0:8, 0:4, 1:1, 0:2, 1:1, 0:1, 1:31, 10:8, 0:80 >>
]),
%% Receive a PROTOCOL_ERROR connection error.
{ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
headers_reject_frame_size_0_padded_flag(Config) ->
doc("HEADERS frames of size 0 with the PADDED flag set must be rejected "
"with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.2)"),
{ok, Socket} = do_handshake(Config),
%% Send a padded HEADERS frame with an incorrect size.
ok = gen_tcp:send(Socket, << 0:24, 1:8, 0:4, 1:1, 0:2, 1:1, 0:1, 1:31 >>),
%% Receive a FRAME_SIZE_ERROR connection error.
{ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
headers_reject_frame_size_too_small_padded_flag(Config) ->
doc("HEADERS frames with no priority flag and Pad Length >= Length "
"must be rejected with a PROTOCOL_ERROR connection error. (RFC7540 6.2)"),
{ok, Socket} = do_handshake(Config),
%% Send a padded HEADERS frame with an incorrect size.
ok = gen_tcp:send(Socket, << 10:24, 1:8, 0:4, 1:1, 0:2, 1:1, 0:1, 1:31, 10:8, 0:80 >>),
%% Receive a PROTOCOL_ERROR connection error.
{ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
headers_reject_frame_size_too_small_priority_flag(Config) ->
doc("HEADERS frames of size smaller than 5 with the PRIORITY flag set must be rejected "
"with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.2)"),
{ok, Socket} = do_handshake(Config),
%% Send a HEADERS frame with priority set and an incorrect size.
ok = gen_tcp:send(Socket, << 4:24, 1:8,
0:2, 1:1, 0:4, 1:1, 0:1, 1:31, 0:1, 3:31, 0:8 >>),
%% Receive a FRAME_SIZE_ERROR connection error.
{ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
headers_reject_frame_size_5_padded_and_priority_flags(Config) ->
doc("HEADERS frames of size smaller than 6 with the PADDED "
"and PRIORITY flags set must be rejected "
"with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.2)"),
{ok, Socket} = do_handshake(Config),
%% Send a padded HEADERS frame with an incorrect size.
ok = gen_tcp:send(Socket, << 5:24, 1:8,
0:2, 1:1, 0:1, 1:1, 0:2, 1:1, 0:1, 1:31, 0:8, 0:1, 3:31, 0:8 >>),
%% Receive a FRAME_SIZE_ERROR connection error.
{ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
headers_reject_frame_size_too_small_padded_and_priority_flags(Config) ->
doc("HEADERS frames of size smaller than Length+6 with the PADDED and PRIORITY flags set "
"must be rejected with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.2)"),
{ok, Socket} = do_handshake(Config),
%% Send a padded HEADERS frame with an incorrect size.
ok = gen_tcp:send(Socket, << 15:24, 1:8,
0:2, 1:1, 0:1, 1:1, 0:2, 1:1, 0:1, 1:31, 10:8, 0:1, 3:31, 0:8, 0:80 >>),
%% Receive a PROTOCOL_ERROR connection error.
{ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
priority_reject_frame_size_too_small(Config) ->
doc("PRIORITY frames of size smaller than 5 must be rejected "
"with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.3)"),
{ok, Socket} = do_handshake(Config),
%% Send a PRIORITY frame with an incorrect size.
ok = gen_tcp:send(Socket, << 4:24, 2:8, 0:9, 1:31, 0:1, 3:31, 0:8 >>),
%% Receive a FRAME_SIZE_ERROR stream error.
{ok, << _:24, 3:8, _:40, 6:32 >>} = gen_tcp:recv(Socket, 13, 6000),
ok.
priority_reject_frame_size_too_large(Config) ->
doc("PRIORITY frames of size larger than 5 must be rejected "
"with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.3)"),
{ok, Socket} = do_handshake(Config),
%% Send a PRIORITY frame with an incorrect size.
ok = gen_tcp:send(Socket, << 6:24, 2:8, 0:9, 1:31, 0:1, 3:31, 0:16 >>),
%% Receive a FRAME_SIZE_ERROR stream error.
{ok, << _:24, 3:8, _:40, 6:32 >>} = gen_tcp:recv(Socket, 13, 6000),
ok.
rst_stream_reject_frame_size_too_small(Config) ->
doc("RST_STREAM frames of size smaller than 4 must be rejected "
"with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.4)"),
{ok, Socket} = do_handshake(Config),
%% Send a request and reset it immediately.
{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),
<< 3:24, 3:8, 0:9, 1:31, 8:32 >>
]),
%% Receive a FRAME_SIZE_ERROR connection error.
{ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
rst_stream_reject_frame_size_too_large(Config) ->
doc("RST_STREAM frames of size larger than 4 must be rejected "
"with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.4)"),
{ok, Socket} = do_handshake(Config),
%% Send a request and reset it immediately.
{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),
<< 5:24, 3:8, 0:9, 1:31, 8:32 >>
]),
%% Receive a FRAME_SIZE_ERROR connection error.
{ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
settings_reject_bad_frame_size(Config) ->
doc("SETTINGS frames must have a size multiple of 6 or be rejected "
"with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.5)"),
{ok, Socket} = do_handshake(Config),
%% Send a SETTINGS frame with an incorrect size.
ok = gen_tcp:send(Socket, << 5:24, 4:8, 0:40, 1:16, 4096:32 >>),
%% Receive a FRAME_SIZE_ERROR connection error.
{ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
settings_ack_reject_non_empty_frame_size(Config) ->
doc("SETTINGS frames with the ACK flag set and a non-empty payload "
"must be rejected with a FRAME_SIZE_ERROR connection error (RFC7540 4.2, RFC7540 6.5)"),
{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(#{})]),
%% 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 a SETTINGS ack with a payload.
ok = gen_tcp:send(Socket, << 6:24, 4:8, 0:7, 1:1, 0:32, 1:16, 4096:32 >>),
%% Receive the SETTINGS ack.
{ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000),
%% Receive a FRAME_SIZE_ERROR connection error.
{ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
%% Note that clients are not supposed to send PUSH_PROMISE frames.
%% However when they do, we need to be able to parse it in order
%% to reject it, and so these errors may still occur.
push_promise_reject_frame_size_too_small(Config) ->
doc("PUSH_PROMISE frames of size smaller than 4 must be rejected "
"with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.6)"),
{ok, Socket} = do_handshake(Config),
%% Send a PUSH_PROMISE frame with an incorrect size.
{HeadersBlock, _} = cow_hpack:encode([
{<<":method">>, <<"GET">>},
{<<":scheme">>, <<"http">>},
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
{<<":path">>, <<"/">>}
]),
ok = gen_tcp:send(Socket, [
<< 3:24, 5:8, 0:5, 1:1, 0:3, 1:31, 0:1, 3:31 >>,
HeadersBlock
]),
%% Receive a FRAME_SIZE_ERROR connection error.
{ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
push_promise_reject_frame_size_4_padded_flag(Config) ->
doc("PUSH_PROMISE frames of size smaller than 5 with the PADDED flag set must be rejected "
"with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.6)"),
{ok, Socket} = do_handshake(Config),
%% Send a PUSH_PROMISE frame with an incorrect size.
{HeadersBlock, _} = cow_hpack:encode([
{<<":method">>, <<"GET">>},
{<<":scheme">>, <<"http">>},
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
{<<":path">>, <<"/">>}
]),
ok = gen_tcp:send(Socket, [
<< 4:24, 5:8, 0:4, 1:1, 1:1, 0:3, 1:31, 0:1, 0:8, 3:31 >>,
HeadersBlock
]),
%% Receive a FRAME_SIZE_ERROR connection error.
{ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
push_promise_reject_frame_size_too_small_padded_flag(Config) ->
doc("PUSH_PROMISE frames of size smaller than Length+5 with the PADDED flag set "
"must be rejected with a PROTOCOL_ERROR connection error. (RFC7540 4.2, RFC7540 6.6)"),
{ok, Socket} = do_handshake(Config),
%% Send a PUSH_PROMISE frame with an incorrect size.
{HeadersBlock, _} = cow_hpack:encode([
{<<":method">>, <<"GET">>},
{<<":scheme">>, <<"http">>},
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
{<<":path">>, <<"/">>}
]),
ok = gen_tcp:send(Socket, [
<< 14:24, 5:8, 0:4, 1:1, 1:1, 0:3, 1:31, 10:8, 0:1, 3:31 >>,
HeadersBlock,
<< 0:80 >>
]),
%% Receive a PROTOCOL_ERROR connection error.
{ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
ping_reject_frame_size_too_small(Config) ->
doc("PING frames of size smaller than 8 must be rejected "
"with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.7)"),
{ok, Socket} = do_handshake(Config),
%% Send a PING frame with an incorrect size.
ok = gen_tcp:send(Socket, << 7:24, 6:8, 0:40, 0:56 >>),
%% Receive a FRAME_SIZE_ERROR connection error.
{ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
ping_reject_frame_size_too_large(Config) ->
doc("PING frames of size larger than 8 must be rejected "
"with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.7)"),
{ok, Socket} = do_handshake(Config),
%% Send a PING frame with an incorrect size.
ok = gen_tcp:send(Socket, << 9:24, 6:8, 0:40, 0:72 >>),
%% Receive a FRAME_SIZE_ERROR connection error.
{ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
goaway_reject_frame_size_too_small(Config) ->
doc("GOAWAY frames of size smaller than 8 must be rejected "
"with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.8)"),
{ok, Socket} = do_handshake(Config),
%% Send a GOAWAY frame with an incorrect size.
ok = gen_tcp:send(Socket, << 7:24, 7:8, 0:40, 0:56 >>),
%% Receive a FRAME_SIZE_ERROR connection error.
{ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
goaway_allow_frame_size_too_large(Config) ->
doc("GOAWAY frames of size larger than 8 must be allowed. (RFC7540 6.8)"),
{ok, Socket} = do_handshake(Config),
%% Send a GOAWAY frame with debug data.
ok = gen_tcp:send(Socket, << 12:24, 7:8, 0:40, 0:64, 99999:32 >>),
%% Receive a GOAWAY frame back.
{ok, << _:24, 7:8, _:72, 0:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
window_update_reject_frame_size_too_small(Config) ->
doc("WINDOW_UPDATE frames of size smaller than 4 must be rejected "
"with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.9)"),
{ok, Socket} = do_handshake(Config),
%% Send a WINDOW_UPDATE frame with an incorrect size.
ok = gen_tcp:send(Socket, << 3:24, 8:8, 0:40, 1000:24 >>),
%% Receive a FRAME_SIZE_ERROR connection error.
{ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
window_update_reject_frame_size_too_large(Config) ->
doc("WINDOW_UPDATE frames of size larger than 4 must be rejected "
"with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.9)"),
{ok, Socket} = do_handshake(Config),
%% Send a WINDOW_UPDATE frame with an incorrect size.
ok = gen_tcp:send(Socket, << 5:24, 8:8, 0:40, 1000:40 >>),
%% Receive a FRAME_SIZE_ERROR connection error.
{ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000),
ok.
%% Note: There is no particular limits on the size of CONTINUATION frames,
%% they can go from 0 to SETTINGS_MAX_FRAME_SIZE.