From 9a29aea148c55d12ac9141bc8c60b2456b4ef42b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Fri, 27 Apr 2018 17:58:11 +0200 Subject: Add options controlling maximum h2 frame sizes --- test/http2_SUITE.erl | 40 +++++++++++- test/rfc7540_SUITE.erl | 169 ++++++++++++++++++++++++++++++------------------- 2 files changed, 143 insertions(+), 66 deletions(-) (limited to 'test') diff --git a/test/http2_SUITE.erl b/test/http2_SUITE.erl index 5b9ca95..3849303 100644 --- a/test/http2_SUITE.erl +++ b/test/http2_SUITE.erl @@ -28,15 +28,19 @@ groups() -> [{clear, [parallel], ct_helper:all(?MODULE)}]. init_routes(_) -> [ {"localhost", [ {"/", hello_h, []}, + {"/echo/:key", echo_h, []}, {"/resp_iolist_body", resp_iolist_body_h, []} ]} ]. -%% Do a prior knowledge handshake (function copied from rfc7540_SUITE). +%% Do a prior knowledge handshake (function originally copied from rfc7540_SUITE). do_handshake(Config) -> + do_handshake(#{}, Config). + +do_handshake(Settings, Config) -> {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(#{})]), + ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(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), @@ -81,6 +85,38 @@ initial_connection_window_size(Config) -> ConfiguredSize = Size + 65535, ok. +max_frame_size_sent(Config) -> + doc("Confirm that frames sent by Cowboy are limited in size " + "by the max_frame_size_sent configuration value."), + MaxFrameSize = 20000, + ProtoOpts = #{ + env => #{dispatch => cowboy_router:compile(init_routes(Config))}, + max_frame_size_sent => MaxFrameSize + }, + {ok, _} = cowboy:start_clear(name(), [{port, 0}], ProtoOpts), + Port = ranch:get_port(name()), + {ok, Socket} = do_handshake(#{max_frame_size => MaxFrameSize + 10000}, [{port, Port}|Config]), + %% Send a request with a 30000 bytes body. + {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, nofin, <<0:16384/unit:8>>), + cow_http2:data(1, fin, <<0:13616/unit:8>>) + ]), + %% Receive a HEADERS frame as a response. + {ok, <>} = gen_tcp:recv(Socket, 9, 6000), + {ok, _} = gen_tcp:recv(Socket, Len, 6000), + %% The DATA frames following must have lengths of 20000 + %% and then 10000 due to the limit. + {ok, <<20000:24, 0:8, _:40, _:20000/unit:8>>} = gen_tcp:recv(Socket, 20009, 6000), + {ok, <<10000:24, 0:8, _:40, _:10000/unit:8>>} = gen_tcp:recv(Socket, 10009, 6000), + ok. + preface_timeout_infinity(Config) -> doc("Ensure infinity for preface_timeout is accepted."), ProtoOpts = #{ diff --git a/test/rfc7540_SUITE.erl b/test/rfc7540_SUITE.erl index d4d5f30..ca483c2 100644 --- a/test/rfc7540_SUITE.erl +++ b/test/rfc7540_SUITE.erl @@ -1277,65 +1277,63 @@ max_frame_size_reject_larger_than_default(Config) -> {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)"), +max_frame_size_allow_exactly_custom(Config0) -> + doc("An endpoint that sets SETTINGS_MAX_FRAME_SIZE must allow frames " + "of up to that size. (RFC7540 4.2, RFC7540 6.5.2)"), + %% Create a new listener that sets the maximum frame size to 30000. + Config = cowboy_test:init_http(name(), #{ + env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, + max_frame_size_received => 30000 + }, Config0), + %% Do the handshake. {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), + %% Send a HEADERS frame initiating a stream followed by + %% a single 30000 bytes DATA frame. + Headers = [ + {<<":method">>, <<"POST">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. + {<<":path">>, <<"/long_polling">>} + ], + {HeadersBlock, _} = cow_hpack:encode(Headers), + ok = gen_tcp:send(Socket, [ + cow_http2:headers(1, nofin, HeadersBlock), + cow_http2:data(1, fin, <<0:30000/unit:8>>) + ]), + %% Receive a proper response. + {ok, << Len2:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), + {ok, _} = gen_tcp:recv(Socket, Len2, 6000), + %% No errors follow due to our sending of a 25000 bytes frame. + {error, timeout} = gen_tcp:recv(Socket, 0, 1000), 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)"), +max_frame_size_reject_larger_than_custom(Config0) -> + 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)"), + %% Create a new listener that sets the maximum frame size to 30000. + Config = cowboy_test:init_http(name(), #{ + env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, + max_frame_size_received => 30000 + }, Config0), + %% Do the handshake. {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), + %% Send a HEADERS frame initiating a stream followed by + %% a single DATA frame larger than 30000 bytes. + Headers = [ + {<<":method">>, <<"POST">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. + {<<":path">>, <<"/long_polling">>} + ], + {HeadersBlock, _} = cow_hpack:encode(Headers), + ok = gen_tcp:send(Socket, [ + cow_http2:headers(1, nofin, HeadersBlock), + cow_http2:data(1, fin, <<0:30001/unit:8>>) + ]), + %% Receive a FRAME_SIZE_ERROR connection error. + {ok, << _:24, 7:8, _:72, 6: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 @@ -2422,7 +2420,7 @@ continuation_with_extension_frame_interleaved_error(Config) -> % incomplete SETTINGS frame MUST be treated as a connection error % (Section 5.4.1) of type PROTOCOL_ERROR. -%% (RFC7540 6.5.2) +%% Settings. settings_header_table_size_client(Config) -> doc("The SETTINGS_HEADER_TABLE_SIZE setting can be used to " @@ -2581,7 +2579,7 @@ settings_max_concurrent_streams_0(Config0) -> settings_initial_window_size(Config0) -> doc("The SETTINGS_INITIAL_WINDOW_SIZE setting can be used to " "change the initial window size of streams. (RFC7540 6.5.2)"), - %% Create a new listener that allows only a single concurrent stream. + %% Create a new listener that sets initial window sizes to 100000. Config = cowboy_test:init_http(name(), #{ env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, initial_connection_window_size => 100000, @@ -2630,7 +2628,7 @@ settings_initial_window_size_after_ack(Config0) -> doc("The SETTINGS_INITIAL_WINDOW_SIZE setting can be used to " "change the initial window size of streams. It is applied " "to all existing streams upon receipt of the SETTINGS ack. (RFC7540 6.5.2)"), - %% Create a new listener that allows only a single concurrent stream. + %% Create a new listener that sets the initial stream window sizes to 0. Config = cowboy_test:init_http(name(), #{ env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, initial_stream_window_size => 0 @@ -2670,7 +2668,7 @@ settings_initial_window_size_before_ack(Config0) -> doc("The SETTINGS_INITIAL_WINDOW_SIZE setting can be used to " "change the initial window size of streams. It is only " "applied upon receipt of the SETTINGS ack. (RFC7540 6.5.2)"), - %% Create a new listener that allows only a single concurrent stream. + %% Create a new listener that sets the initial stream window sizes to 0. Config = cowboy_test:init_http(name(), #{ env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, initial_stream_window_size => 0 @@ -2711,13 +2709,56 @@ settings_initial_window_size_before_ack(Config0) -> {error, timeout} = gen_tcp:recv(Socket, 0, 1000), ok. -% 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. -% +settings_max_frame_size(Config0) -> + doc("The SETTINGS_MAX_FRAME_SIZE setting can be used to " + "change the maximum frame size allowed. (RFC7540 6.5.2)"), + %% Create a new listener that sets the maximum frame size to 30000. + Config = cowboy_test:init_http(name(), #{ + env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, + max_frame_size_received => 30000 + }, Config0), + %% Do the handshake. + {ok, Socket} = do_handshake(Config), + %% Send a HEADERS frame initiating a stream followed by + %% a single 25000 bytes DATA frame. + Headers = [ + {<<":method">>, <<"POST">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. + {<<":path">>, <<"/long_polling">>} + ], + {HeadersBlock, _} = cow_hpack:encode(Headers), + ok = gen_tcp:send(Socket, [ + cow_http2:headers(1, nofin, HeadersBlock), + cow_http2:data(1, fin, <<0:25000/unit:8>>) + ]), + %% Receive a proper response. + {ok, << Len2:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), + {ok, _} = gen_tcp:recv(Socket, Len2, 6000), + %% No errors follow due to our sending of a 25000 bytes frame. + {error, timeout} = gen_tcp:recv(Socket, 0, 1000), + ok. + +settings_max_frame_size_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. + +settings_max_frame_size_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. + % 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 -- cgit v1.2.3