mirror of
https://github.com/ninenines/cowboy.git
synced 2025-07-14 04:10:24 +00:00
Initial HTTP/3 implementation
This includes Websocket over HTTP/3. Since quicer, which provides the QUIC implementation, is a NIF, Cowboy cannot depend directly on it. In order to enable QUIC and HTTP/3, users have to set the COWBOY_QUICER environment variable: export COWBOY_QUICER=1 In order to run the test suites, the same must be done for Gun: export GUN_QUICER=1 HTTP/3 support is currently not available on Windows due to compilation issues of quicer which have yet to be looked at or resolved. HTTP/3 support is also unavailable on the upcoming OTP-27 due to compilation errors in quicer dependencies. Once resolved HTTP/3 should work on OTP-27. Because of how QUIC currently works, it's possible that streams that get reset after sending a response do not receive that response. The test suite was modified to accomodate for that. A future extension to QUIC will allow us to gracefully reset streams. This also updates Erlang.mk.
This commit is contained in:
parent
3ea8395eb8
commit
8cb9d242b0
39 changed files with 5130 additions and 238 deletions
|
@ -23,12 +23,20 @@
|
|||
%% ct.
|
||||
|
||||
all() ->
|
||||
[
|
||||
All = [
|
||||
{group, http_compress},
|
||||
{group, https_compress},
|
||||
{group, h2_compress},
|
||||
{group, h2c_compress}
|
||||
].
|
||||
{group, h2c_compress},
|
||||
{group, h3_compress}
|
||||
],
|
||||
%% Don't run HTTP/3 tests on Windows for now.
|
||||
case os:type() of
|
||||
{win32, _} ->
|
||||
All -- [{group, h3_compress}];
|
||||
_ ->
|
||||
All
|
||||
end.
|
||||
|
||||
groups() ->
|
||||
cowboy_test:common_groups(ct_helper:all(?MODULE)).
|
||||
|
@ -37,7 +45,7 @@ init_per_group(Name, Config) ->
|
|||
cowboy_test:init_common_groups(Name, Config, ?MODULE).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
%% Routes.
|
||||
|
||||
|
|
|
@ -37,35 +37,82 @@ init_http2(Ref, ProtoOpts, Config) ->
|
|||
Port = ranch:get_port(Ref),
|
||||
[{ref, Ref}, {type, ssl}, {protocol, http2}, {port, Port}, {opts, Opts}|Config].
|
||||
|
||||
%% @todo This will probably require TransOpts as argument.
|
||||
init_http3(Ref, ProtoOpts, Config) ->
|
||||
%% @todo Quicer does not currently support non-file cert/key,
|
||||
%% so we use quicer test certificates for now.
|
||||
%% @todo Quicer also does not support cacerts which means
|
||||
%% we currently have no authentication based security.
|
||||
DataDir = filename:dirname(filename:dirname(config(data_dir, Config)))
|
||||
++ "/rfc9114_SUITE_data",
|
||||
TransOpts = #{
|
||||
socket_opts => [
|
||||
{certfile, DataDir ++ "/server.pem"},
|
||||
{keyfile, DataDir ++ "/server.key"}
|
||||
]
|
||||
},
|
||||
{ok, Listener} = cowboy:start_quic(Ref, TransOpts, ProtoOpts),
|
||||
{ok, {_, Port}} = quicer:sockname(Listener),
|
||||
%% @todo Keep listener information around in a better place.
|
||||
persistent_term:put({cowboy_test_quic, Ref}, Listener),
|
||||
[{ref, Ref}, {type, quic}, {protocol, http3}, {port, Port}, {opts, TransOpts}|Config].
|
||||
|
||||
stop_group(Ref) ->
|
||||
case persistent_term:get({cowboy_test_quic, Ref}, undefined) of
|
||||
undefined ->
|
||||
cowboy:stop_listener(Ref);
|
||||
Listener ->
|
||||
quicer:close_listener(Listener)
|
||||
end.
|
||||
|
||||
%% Common group of listeners used by most suites.
|
||||
|
||||
common_all() ->
|
||||
[
|
||||
All = [
|
||||
{group, http},
|
||||
{group, https},
|
||||
{group, h2},
|
||||
{group, h2c},
|
||||
{group, h3},
|
||||
{group, http_compress},
|
||||
{group, https_compress},
|
||||
{group, h2_compress},
|
||||
{group, h2c_compress}
|
||||
].
|
||||
{group, h2c_compress},
|
||||
{group, h3_compress}
|
||||
],
|
||||
%% Don't run HTTP/3 tests on Windows for now.
|
||||
case os:type() of
|
||||
{win32, _} ->
|
||||
All -- [{group, h3}, {group, h3_compress}];
|
||||
_ ->
|
||||
All
|
||||
end.
|
||||
|
||||
common_groups(Tests) ->
|
||||
Opts = case os:getenv("NO_PARALLEL") of
|
||||
false -> [parallel];
|
||||
_ -> []
|
||||
end,
|
||||
[
|
||||
Groups = [
|
||||
{http, Opts, Tests},
|
||||
{https, Opts, Tests},
|
||||
{h2, Opts, Tests},
|
||||
{h2c, Opts, Tests},
|
||||
{h3, Opts, Tests},
|
||||
{http_compress, Opts, Tests},
|
||||
{https_compress, Opts, Tests},
|
||||
{h2_compress, Opts, Tests},
|
||||
{h2c_compress, Opts, Tests}
|
||||
].
|
||||
{h2c_compress, Opts, Tests},
|
||||
{h3_compress, Opts, Tests}
|
||||
],
|
||||
%% Don't run HTTP/3 tests on Windows for now.
|
||||
case os:type() of
|
||||
{win32, _} ->
|
||||
Groups -- [{h3, Opts, Tests}, {h3_compress, Opts, Tests}];
|
||||
_ ->
|
||||
Groups
|
||||
end.
|
||||
|
||||
|
||||
init_common_groups(Name = http, Config, Mod) ->
|
||||
init_http(Name, #{
|
||||
|
@ -84,6 +131,10 @@ init_common_groups(Name = h2c, Config, Mod) ->
|
|||
env => #{dispatch => Mod:init_dispatch(Config)}
|
||||
}, [{flavor, vanilla}|Config]),
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2});
|
||||
init_common_groups(Name = h3, Config, Mod) ->
|
||||
init_http3(Name, #{
|
||||
env => #{dispatch => Mod:init_dispatch(Config)}
|
||||
}, [{flavor, vanilla}|Config]);
|
||||
init_common_groups(Name = http_compress, Config, Mod) ->
|
||||
init_http(Name, #{
|
||||
env => #{dispatch => Mod:init_dispatch(Config)},
|
||||
|
@ -104,7 +155,12 @@ init_common_groups(Name = h2c_compress, Config, Mod) ->
|
|||
env => #{dispatch => Mod:init_dispatch(Config)},
|
||||
stream_handlers => [cowboy_compress_h, cowboy_stream_h]
|
||||
}, [{flavor, compress}|Config]),
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2}).
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2});
|
||||
init_common_groups(Name = h3_compress, Config, Mod) ->
|
||||
init_http3(Name, #{
|
||||
env => #{dispatch => Mod:init_dispatch(Config)},
|
||||
stream_handlers => [cowboy_compress_h, cowboy_stream_h]
|
||||
}, [{flavor, compress}|Config]).
|
||||
|
||||
%% Support functions for testing using Gun.
|
||||
|
||||
|
@ -114,7 +170,7 @@ gun_open(Config) ->
|
|||
gun_open(Config, Opts) ->
|
||||
TlsOpts = case proplists:get_value(no_cert, Config, false) of
|
||||
true -> [{verify, verify_none}];
|
||||
false -> ct_helper:get_certs_from_ets()
|
||||
false -> ct_helper:get_certs_from_ets() %% @todo Wrong in current quicer.
|
||||
end,
|
||||
{ok, ConnPid} = gun:open("localhost", config(port, Config), Opts#{
|
||||
retry => 0,
|
||||
|
|
|
@ -38,6 +38,8 @@ init_per_group(Name = h2, Config) ->
|
|||
init_per_group(Name = h2c, Config) ->
|
||||
Config1 = cowboy_test:init_http(Name, init_plain_opts(Config), Config),
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2});
|
||||
init_per_group(Name = h3, Config) ->
|
||||
cowboy_test:init_http3(Name, init_plain_opts(Config), Config);
|
||||
init_per_group(Name = http_compress, Config) ->
|
||||
cowboy_test:init_http(Name, init_compress_opts(Config), Config);
|
||||
init_per_group(Name = https_compress, Config) ->
|
||||
|
@ -46,7 +48,9 @@ init_per_group(Name = h2_compress, Config) ->
|
|||
cowboy_test:init_http2(Name, init_compress_opts(Config), Config);
|
||||
init_per_group(Name = h2c_compress, Config) ->
|
||||
Config1 = cowboy_test:init_http(Name, init_compress_opts(Config), Config),
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2}).
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2});
|
||||
init_per_group(Name = h3_compress, Config) ->
|
||||
cowboy_test:init_http3(Name, init_compress_opts(Config), Config).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
|
|
|
@ -182,6 +182,7 @@ do(<<"reply2">>, Req0, Opts) ->
|
|||
<<"twice">> ->
|
||||
ct_helper:ignore(cowboy_req, reply, 4),
|
||||
Req1 = cowboy_req:reply(200, Req0),
|
||||
timer:sleep(100),
|
||||
cowboy_req:reply(200, Req1);
|
||||
Status ->
|
||||
cowboy_req:reply(binary_to_integer(Status), Req0)
|
||||
|
@ -245,6 +246,7 @@ do(<<"stream_reply2">>, Req0, Opts) ->
|
|||
<<"twice">> ->
|
||||
ct_helper:ignore(cowboy_req, stream_reply, 3),
|
||||
Req1 = cowboy_req:stream_reply(200, Req0),
|
||||
timer:sleep(100),
|
||||
%% We will crash here so the body shouldn't be sent.
|
||||
Req = cowboy_req:stream_reply(200, Req1),
|
||||
stream_body(Req),
|
||||
|
|
|
@ -32,7 +32,7 @@ init_per_group(Name, Config) ->
|
|||
cowboy_test:init_common_groups(Name, Config, ?MODULE).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
%% Dispatch configuration.
|
||||
|
||||
|
|
|
@ -44,6 +44,8 @@ init_per_group(Name = h2, Config) ->
|
|||
init_per_group(Name = h2c, Config) ->
|
||||
Config1 = cowboy_test:init_http(Name, init_plain_opts(Config), Config),
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2});
|
||||
init_per_group(Name = h3, Config) ->
|
||||
cowboy_test:init_http3(Name, init_plain_opts(Config), Config);
|
||||
init_per_group(Name = http_compress, Config) ->
|
||||
cowboy_test:init_http(Name, init_compress_opts(Config), Config);
|
||||
init_per_group(Name = https_compress, Config) ->
|
||||
|
@ -52,10 +54,12 @@ init_per_group(Name = h2_compress, Config) ->
|
|||
cowboy_test:init_http2(Name, init_compress_opts(Config), Config);
|
||||
init_per_group(Name = h2c_compress, Config) ->
|
||||
Config1 = cowboy_test:init_http(Name, init_compress_opts(Config), Config),
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2}).
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2});
|
||||
init_per_group(Name = h3_compress, Config) ->
|
||||
cowboy_test:init_http3(Name, init_compress_opts(Config), Config).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
init_plain_opts(Config) ->
|
||||
#{
|
||||
|
@ -157,16 +161,24 @@ do_get(Path, UserData, Config) ->
|
|||
#{
|
||||
ref := _,
|
||||
pid := From,
|
||||
streamid := 1,
|
||||
reason := normal,
|
||||
streamid := StreamID,
|
||||
reason := normal, %% @todo Getting h3_no_error here.
|
||||
req := #{},
|
||||
informational := [],
|
||||
user_data := UserData
|
||||
} = Metrics,
|
||||
do_check_streamid(StreamID, Config),
|
||||
%% All good!
|
||||
gun:close(ConnPid)
|
||||
end.
|
||||
|
||||
do_check_streamid(StreamID, Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> 1 = StreamID;
|
||||
http2 -> 1 = StreamID;
|
||||
http3 -> 0 = StreamID
|
||||
end.
|
||||
|
||||
post_body(Config) ->
|
||||
doc("Confirm metrics are correct for a normal POST request."),
|
||||
%% Perform a POST request.
|
||||
|
@ -218,12 +230,13 @@ post_body(Config) ->
|
|||
#{
|
||||
ref := _,
|
||||
pid := From,
|
||||
streamid := 1,
|
||||
streamid := StreamID,
|
||||
reason := normal,
|
||||
req := #{},
|
||||
informational := [],
|
||||
user_data := #{}
|
||||
} = Metrics,
|
||||
do_check_streamid(StreamID, Config),
|
||||
%% All good!
|
||||
gun:close(ConnPid)
|
||||
end.
|
||||
|
@ -273,12 +286,13 @@ no_resp_body(Config) ->
|
|||
#{
|
||||
ref := _,
|
||||
pid := From,
|
||||
streamid := 1,
|
||||
streamid := StreamID,
|
||||
reason := normal,
|
||||
req := #{},
|
||||
informational := [],
|
||||
user_data := #{}
|
||||
} = Metrics,
|
||||
do_check_streamid(StreamID, Config),
|
||||
%% All good!
|
||||
gun:close(ConnPid)
|
||||
end.
|
||||
|
@ -291,7 +305,8 @@ early_error(Config) ->
|
|||
%% reason in both protocols.
|
||||
{Method, Headers, Status, Error} = case config(protocol, Config) of
|
||||
http -> {<<"GET">>, [{<<"host">>, <<"host:port">>}], 400, protocol_error};
|
||||
http2 -> {<<"TRACE">>, [], 501, no_error}
|
||||
http2 -> {<<"TRACE">>, [], 501, no_error};
|
||||
http3 -> {<<"TRACE">>, [], 501, h3_no_error}
|
||||
end,
|
||||
Ref = gun:request(ConnPid, Method, "/", [
|
||||
{<<"accept-encoding">>, <<"gzip">>},
|
||||
|
@ -305,7 +320,7 @@ early_error(Config) ->
|
|||
#{
|
||||
ref := _,
|
||||
pid := From,
|
||||
streamid := 1,
|
||||
streamid := StreamID,
|
||||
reason := {stream_error, Error, _},
|
||||
partial_req := #{},
|
||||
resp_status := Status,
|
||||
|
@ -313,6 +328,7 @@ early_error(Config) ->
|
|||
early_error_time := _,
|
||||
resp_body_length := 0
|
||||
} = Metrics,
|
||||
do_check_streamid(StreamID, Config),
|
||||
ExpectedRespHeaders = maps:from_list(RespHeaders),
|
||||
%% All good!
|
||||
gun:close(ConnPid)
|
||||
|
@ -321,7 +337,8 @@ early_error(Config) ->
|
|||
early_error_request_line(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_early_error_request_line(Config);
|
||||
http2 -> doc("There are no request lines in HTTP/2.")
|
||||
http2 -> doc("There are no request lines in HTTP/2.");
|
||||
http3 -> doc("There are no request lines in HTTP/3.")
|
||||
end.
|
||||
|
||||
do_early_error_request_line(Config) ->
|
||||
|
@ -341,7 +358,7 @@ do_early_error_request_line(Config) ->
|
|||
#{
|
||||
ref := _,
|
||||
pid := From,
|
||||
streamid := 1,
|
||||
streamid := StreamID,
|
||||
reason := {connection_error, protocol_error, _},
|
||||
partial_req := #{},
|
||||
resp_status := 400,
|
||||
|
@ -349,6 +366,7 @@ do_early_error_request_line(Config) ->
|
|||
early_error_time := _,
|
||||
resp_body_length := 0
|
||||
} = Metrics,
|
||||
do_check_streamid(StreamID, Config),
|
||||
ExpectedRespHeaders = maps:from_list(RespHeaders),
|
||||
%% All good!
|
||||
ok
|
||||
|
@ -362,7 +380,9 @@ stream_reply(Config) ->
|
|||
ws(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_ws(Config);
|
||||
http2 -> doc("It is not currently possible to switch to Websocket over HTTP/2.")
|
||||
%% @todo The test can be implemented for HTTP/2.
|
||||
http2 -> doc("It is not currently possible to switch to Websocket over HTTP/2.");
|
||||
http3 -> {skip, "Gun does not currently support Websocket over HTTP/3."}
|
||||
end.
|
||||
|
||||
do_ws(Config) ->
|
||||
|
@ -405,7 +425,7 @@ do_ws(Config) ->
|
|||
#{
|
||||
ref := _,
|
||||
pid := From,
|
||||
streamid := 1,
|
||||
streamid := StreamID,
|
||||
reason := switch_protocol,
|
||||
req := #{},
|
||||
%% A 101 upgrade response was sent.
|
||||
|
@ -420,6 +440,7 @@ do_ws(Config) ->
|
|||
}],
|
||||
user_data := #{}
|
||||
} = Metrics,
|
||||
do_check_streamid(StreamID, Config),
|
||||
%% All good!
|
||||
ok
|
||||
end,
|
||||
|
@ -438,7 +459,15 @@ error_response(Config) ->
|
|||
{<<"accept-encoding">>, <<"gzip">>},
|
||||
{<<"x-test-pid">>, pid_to_list(self())}
|
||||
]),
|
||||
{response, fin, 500, RespHeaders} = gun:await(ConnPid, Ref, infinity),
|
||||
Protocol = config(protocol, Config),
|
||||
RespHeaders = case gun:await(ConnPid, Ref, infinity) of
|
||||
{response, fin, 500, RespHeaders0} ->
|
||||
RespHeaders0;
|
||||
%% The RST_STREAM arrived before the start of the response.
|
||||
%% See maybe_h3_error comment for details.
|
||||
{error, {stream_error, {stream_error, h3_internal_error, _}}} when Protocol =:= http3 ->
|
||||
unknown
|
||||
end,
|
||||
timer:sleep(100),
|
||||
%% Receive the metrics and validate them.
|
||||
receive
|
||||
|
@ -463,7 +492,14 @@ error_response(Config) ->
|
|||
resp_headers := ExpectedRespHeaders,
|
||||
resp_body_length := 0
|
||||
} = Metrics,
|
||||
ExpectedRespHeaders = maps:from_list(RespHeaders),
|
||||
case RespHeaders of
|
||||
%% The HTTP/3 stream has reset too early so we can't
|
||||
%% verify the response headers.
|
||||
unknown ->
|
||||
ok;
|
||||
_ ->
|
||||
ExpectedRespHeaders = maps:from_list(RespHeaders)
|
||||
end,
|
||||
%% The request process executed normally.
|
||||
#{procs := Procs} = Metrics,
|
||||
[{_, #{
|
||||
|
@ -476,12 +512,13 @@ error_response(Config) ->
|
|||
#{
|
||||
ref := _,
|
||||
pid := From,
|
||||
streamid := 1,
|
||||
streamid := StreamID,
|
||||
reason := {internal_error, {'EXIT', _Pid, {crash, StackTrace}}, 'Stream process crashed.'},
|
||||
req := #{},
|
||||
informational := [],
|
||||
user_data := #{}
|
||||
} = Metrics,
|
||||
do_check_streamid(StreamID, Config),
|
||||
%% All good!
|
||||
gun:close(ConnPid)
|
||||
end.
|
||||
|
@ -495,7 +532,15 @@ error_response_after_reply(Config) ->
|
|||
{<<"accept-encoding">>, <<"gzip">>},
|
||||
{<<"x-test-pid">>, pid_to_list(self())}
|
||||
]),
|
||||
{response, fin, 200, RespHeaders} = gun:await(ConnPid, Ref, infinity),
|
||||
Protocol = config(protocol, Config),
|
||||
RespHeaders = case gun:await(ConnPid, Ref, infinity) of
|
||||
{response, fin, 200, RespHeaders0} ->
|
||||
RespHeaders0;
|
||||
%% The RST_STREAM arrived before the start of the response.
|
||||
%% See maybe_h3_error comment for details.
|
||||
{error, {stream_error, {stream_error, h3_internal_error, _}}} when Protocol =:= http3 ->
|
||||
unknown
|
||||
end,
|
||||
timer:sleep(100),
|
||||
%% Receive the metrics and validate them.
|
||||
receive
|
||||
|
@ -520,7 +565,14 @@ error_response_after_reply(Config) ->
|
|||
resp_headers := ExpectedRespHeaders,
|
||||
resp_body_length := 0
|
||||
} = Metrics,
|
||||
ExpectedRespHeaders = maps:from_list(RespHeaders),
|
||||
case RespHeaders of
|
||||
%% The HTTP/3 stream has reset too early so we can't
|
||||
%% verify the response headers.
|
||||
unknown ->
|
||||
ok;
|
||||
_ ->
|
||||
ExpectedRespHeaders = maps:from_list(RespHeaders)
|
||||
end,
|
||||
%% The request process executed normally.
|
||||
#{procs := Procs} = Metrics,
|
||||
[{_, #{
|
||||
|
@ -533,12 +585,13 @@ error_response_after_reply(Config) ->
|
|||
#{
|
||||
ref := _,
|
||||
pid := From,
|
||||
streamid := 1,
|
||||
streamid := StreamID,
|
||||
reason := {internal_error, {'EXIT', _Pid, {crash, StackTrace}}, 'Stream process crashed.'},
|
||||
req := #{},
|
||||
informational := [],
|
||||
user_data := #{}
|
||||
} = Metrics,
|
||||
do_check_streamid(StreamID, Config),
|
||||
%% All good!
|
||||
gun:close(ConnPid)
|
||||
end.
|
||||
|
|
|
@ -43,7 +43,7 @@ init_per_group(Name, Config) ->
|
|||
end_per_group(env, _) ->
|
||||
ok;
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
init_dispatch(_) ->
|
||||
cowboy_router:compile([{"localhost", [
|
||||
|
|
|
@ -39,7 +39,7 @@ init_per_group(Name, Config) ->
|
|||
cowboy_test:init_common_groups(Name, Config, ?MODULE).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
%% Routes.
|
||||
|
||||
|
@ -58,8 +58,15 @@ crash_after_reply(Config) ->
|
|||
Ref = gun:get(ConnPid, "/crash/reply", [
|
||||
{<<"accept-encoding">>, <<"gzip">>}
|
||||
]),
|
||||
{response, fin, 200, _} = gun:await(ConnPid, Ref),
|
||||
{error, timeout} = gun:await(ConnPid, Ref, 1000),
|
||||
Protocol = config(protocol, Config),
|
||||
_ = case gun:await(ConnPid, Ref) of
|
||||
{response, fin, 200, _} ->
|
||||
{error, timeout} = gun:await(ConnPid, Ref, 1000);
|
||||
%% See maybe_h3_error comment for details.
|
||||
{error, {stream_error, {stream_error, h3_internal_error, _}}}
|
||||
when Protocol =:= http3 ->
|
||||
ok
|
||||
end,
|
||||
gun:close(ConnPid).
|
||||
|
||||
crash_before_reply(Config) ->
|
||||
|
|
|
@ -46,7 +46,7 @@ init_per_group(Name, Config) ->
|
|||
cowboy_test:init_common_groups(Name, Config, ?MODULE).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
%% Routes.
|
||||
|
||||
|
@ -107,13 +107,17 @@ do_get(Path, Config) ->
|
|||
do_get(Path, Headers, Config) ->
|
||||
ConnPid = gun_open(Config),
|
||||
Ref = gun:get(ConnPid, Path, [{<<"accept-encoding">>, <<"gzip">>}|Headers]),
|
||||
{response, IsFin, Status, RespHeaders} = gun:await(ConnPid, Ref, infinity),
|
||||
{ok, RespBody} = case IsFin of
|
||||
nofin -> gun:await_body(ConnPid, Ref, infinity);
|
||||
fin -> {ok, <<>>}
|
||||
end,
|
||||
gun:close(ConnPid),
|
||||
{Status, RespHeaders, do_decode(RespHeaders, RespBody)}.
|
||||
case gun:await(ConnPid, Ref, infinity) of
|
||||
{response, IsFin, Status, RespHeaders} ->
|
||||
{ok, RespBody} = case IsFin of
|
||||
nofin -> gun:await_body(ConnPid, Ref, infinity);
|
||||
fin -> {ok, <<>>}
|
||||
end,
|
||||
gun:close(ConnPid),
|
||||
{Status, RespHeaders, do_decode(RespHeaders, RespBody)};
|
||||
{error, {stream_error, Error}} ->
|
||||
Error
|
||||
end.
|
||||
|
||||
do_get_body(Path, Config) ->
|
||||
do_get_body(Path, [], Config).
|
||||
|
@ -142,7 +146,9 @@ do_get_inform(Path, Config) ->
|
|||
fin -> {ok, <<>>}
|
||||
end,
|
||||
gun:close(ConnPid),
|
||||
{InfoStatus, InfoHeaders, RespStatus, RespHeaders, do_decode(RespHeaders, RespBody)}
|
||||
{InfoStatus, InfoHeaders, RespStatus, RespHeaders, do_decode(RespHeaders, RespBody)};
|
||||
{error, {stream_error, Error}} ->
|
||||
Error
|
||||
end.
|
||||
|
||||
do_decode(Headers, Body) ->
|
||||
|
@ -184,7 +190,8 @@ bindings(Config) ->
|
|||
cert(Config) ->
|
||||
case config(type, Config) of
|
||||
tcp -> doc("TLS certificates can only be provided over TLS.");
|
||||
ssl -> do_cert(Config)
|
||||
ssl -> do_cert(Config);
|
||||
quic -> do_cert(Config)
|
||||
end.
|
||||
|
||||
do_cert(Config) ->
|
||||
|
@ -386,7 +393,8 @@ port(Config) ->
|
|||
Port = do_get_body("/direct/port", Config),
|
||||
ExpectedPort = case config(type, Config) of
|
||||
tcp -> <<"80">>;
|
||||
ssl -> <<"443">>
|
||||
ssl -> <<"443">>;
|
||||
quic -> <<"443">>
|
||||
end,
|
||||
ExpectedPort = do_get_body("/port", [{<<"host">>, <<"localhost">>}], Config),
|
||||
ExpectedPort = do_get_body("/direct/port", [{<<"host">>, <<"localhost">>}], Config),
|
||||
|
@ -412,7 +420,8 @@ do_scheme(Path, Config) ->
|
|||
Transport = config(type, Config),
|
||||
case do_get_body(Path, Config) of
|
||||
<<"http">> when Transport =:= tcp -> ok;
|
||||
<<"https">> when Transport =:= ssl -> ok
|
||||
<<"https">> when Transport =:= ssl -> ok;
|
||||
<<"https">> when Transport =:= quic -> ok
|
||||
end.
|
||||
|
||||
sock(Config) ->
|
||||
|
@ -425,7 +434,8 @@ uri(Config) ->
|
|||
doc("Request URI building/modification."),
|
||||
Scheme = case config(type, Config) of
|
||||
tcp -> <<"http">>;
|
||||
ssl -> <<"https">>
|
||||
ssl -> <<"https">>;
|
||||
quic -> <<"https">>
|
||||
end,
|
||||
SLen = byte_size(Scheme),
|
||||
Port = integer_to_binary(config(port, Config)),
|
||||
|
@ -459,7 +469,8 @@ do_version(Path, Config) ->
|
|||
Protocol = config(protocol, Config),
|
||||
case do_get_body(Path, Config) of
|
||||
<<"HTTP/1.1">> when Protocol =:= http -> ok;
|
||||
<<"HTTP/2">> when Protocol =:= http2 -> ok
|
||||
<<"HTTP/2">> when Protocol =:= http2 -> ok;
|
||||
<<"HTTP/3">> when Protocol =:= http3 -> ok
|
||||
end.
|
||||
|
||||
%% Tests: Request body.
|
||||
|
@ -513,11 +524,19 @@ read_body_period(Config) ->
|
|||
%% for 2 seconds. The test succeeds if we get some of the data back
|
||||
%% (meaning the function will have returned after the period ends).
|
||||
gun:data(ConnPid, Ref, nofin, Body),
|
||||
{response, nofin, 200, _} = gun:await(ConnPid, Ref, infinity),
|
||||
{data, _, Data} = gun:await(ConnPid, Ref, infinity),
|
||||
%% We expect to read at least some data.
|
||||
true = Data =/= <<>>,
|
||||
gun:close(ConnPid).
|
||||
Response = gun:await(ConnPid, Ref, infinity),
|
||||
case Response of
|
||||
{response, nofin, 200, _} ->
|
||||
{data, _, Data} = gun:await(ConnPid, Ref, infinity),
|
||||
%% We expect to read at least some data.
|
||||
true = Data =/= <<>>,
|
||||
gun:close(ConnPid);
|
||||
%% We got a crash, likely because the environment
|
||||
%% was overloaded and the timeout triggered. Try again.
|
||||
{response, _, 500, _} ->
|
||||
gun:close(ConnPid),
|
||||
read_body_period(Config)
|
||||
end.
|
||||
|
||||
%% We expect a crash.
|
||||
do_read_body_timeout(Path, Body, Config) ->
|
||||
|
@ -525,7 +544,13 @@ do_read_body_timeout(Path, Body, Config) ->
|
|||
Ref = gun:headers(ConnPid, "POST", Path, [
|
||||
{<<"content-length">>, integer_to_binary(byte_size(Body))}
|
||||
]),
|
||||
{response, _, 500, _} = gun:await(ConnPid, Ref, infinity),
|
||||
case gun:await(ConnPid, Ref, infinity) of
|
||||
{response, _, 500, _} ->
|
||||
ok;
|
||||
%% See do_maybe_h3_error comment for details.
|
||||
{error, {stream_error, {stream_error, h3_internal_error, _}}} ->
|
||||
ok
|
||||
end,
|
||||
gun:close(ConnPid).
|
||||
|
||||
read_body_auto(Config) ->
|
||||
|
@ -620,15 +645,19 @@ do_read_urlencoded_body_too_long(Path, Body, Config) ->
|
|||
{<<"content-length">>, integer_to_binary(byte_size(Body) * 2)}
|
||||
]),
|
||||
gun:data(ConnPid, Ref, nofin, Body),
|
||||
{response, _, 408, RespHeaders} = gun:await(ConnPid, Ref, infinity),
|
||||
_ = case config(protocol, Config) of
|
||||
http ->
|
||||
Protocol = config(protocol, Config),
|
||||
case gun:await(ConnPid, Ref, infinity) of
|
||||
{response, _, 408, RespHeaders} when Protocol =:= http ->
|
||||
%% 408 error responses should close HTTP/1.1 connections.
|
||||
{_, <<"close">>} = lists:keyfind(<<"connection">>, 1, RespHeaders);
|
||||
http2 ->
|
||||
ok
|
||||
end,
|
||||
gun:close(ConnPid).
|
||||
{_, <<"close">>} = lists:keyfind(<<"connection">>, 1, RespHeaders),
|
||||
gun:close(ConnPid);
|
||||
{response, _, 408, _} when Protocol =:= http2; Protocol =:= http3 ->
|
||||
gun:close(ConnPid);
|
||||
%% We must have hit the timeout due to busy CI environment. Retry.
|
||||
{response, _, 500, _} ->
|
||||
gun:close(ConnPid),
|
||||
do_read_urlencoded_body_too_long(Path, Body, Config)
|
||||
end.
|
||||
|
||||
read_and_match_urlencoded_body(Config) ->
|
||||
doc("Read and match an application/x-www-form-urlencoded request body."),
|
||||
|
@ -824,7 +853,7 @@ set_resp_header(Config) ->
|
|||
{200, Headers, <<"OK">>} = do_get("/resp/set_resp_header", Config),
|
||||
true = lists:keymember(<<"content-type">>, 1, Headers),
|
||||
%% The set-cookie header is special. set_resp_cookie must be used.
|
||||
{500, _, _} = do_get("/resp/set_resp_header_cookie", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/resp/set_resp_header_cookie", Config)),
|
||||
ok.
|
||||
|
||||
set_resp_headers(Config) ->
|
||||
|
@ -833,7 +862,7 @@ set_resp_headers(Config) ->
|
|||
true = lists:keymember(<<"content-type">>, 1, Headers),
|
||||
true = lists:keymember(<<"content-encoding">>, 1, Headers),
|
||||
%% The set-cookie header is special. set_resp_cookie must be used.
|
||||
{500, _, _} = do_get("/resp/set_resp_headers_cookie", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/resp/set_resp_headers_cookie", Config)),
|
||||
ok.
|
||||
|
||||
resp_header(Config) ->
|
||||
|
@ -895,28 +924,52 @@ delete_resp_header(Config) ->
|
|||
false = lists:keymember(<<"content-type">>, 1, Headers),
|
||||
ok.
|
||||
|
||||
%% Data may be lost due to how RESET_STREAM QUIC frame works.
|
||||
%% Because there is ongoing work for a better way to reset streams
|
||||
%% (https://www.ietf.org/archive/id/draft-ietf-quic-reliable-stream-reset-03.html)
|
||||
%% we convert the error to a 500 to keep the tests more explicit
|
||||
%% at what we expect.
|
||||
%% @todo When RESET_STREAM_AT gets added we can remove this function.
|
||||
do_maybe_h3_error2({stream_error, h3_internal_error, _}) -> {500, []};
|
||||
do_maybe_h3_error2(Result) -> Result.
|
||||
|
||||
do_maybe_h3_error3({stream_error, h3_internal_error, _}) -> {500, [], <<>>};
|
||||
do_maybe_h3_error3(Result) -> Result.
|
||||
|
||||
inform2(Config) ->
|
||||
doc("Informational response(s) without headers, followed by the real response."),
|
||||
{102, [], 200, _, _} = do_get_inform("/resp/inform2/102", Config),
|
||||
{102, [], 200, _, _} = do_get_inform("/resp/inform2/binary", Config),
|
||||
{500, _} = do_get_inform("/resp/inform2/error", Config),
|
||||
{500, _} = do_maybe_h3_error2(do_get_inform("/resp/inform2/error", Config)),
|
||||
{102, [], 200, _, _} = do_get_inform("/resp/inform2/twice", Config),
|
||||
%% @todo How to test this properly? This isn't enough.
|
||||
{200, _} = do_get_inform("/resp/inform2/after_reply", Config),
|
||||
ok.
|
||||
%% With HTTP/1.1 and HTTP/2 we will not get an error.
|
||||
%% With HTTP/3 however the stream will occasionally
|
||||
%% be reset before Gun receives the response.
|
||||
case do_get_inform("/resp/inform2/after_reply", Config) of
|
||||
{200, _} ->
|
||||
ok;
|
||||
{stream_error, h3_internal_error, _} ->
|
||||
ok
|
||||
end.
|
||||
|
||||
inform3(Config) ->
|
||||
doc("Informational response(s) with headers, followed by the real response."),
|
||||
Headers = [{<<"ext-header">>, <<"ext-value">>}],
|
||||
{102, Headers, 200, _, _} = do_get_inform("/resp/inform3/102", Config),
|
||||
{102, Headers, 200, _, _} = do_get_inform("/resp/inform3/binary", Config),
|
||||
{500, _} = do_get_inform("/resp/inform3/error", Config),
|
||||
{500, _} = do_maybe_h3_error2(do_get_inform("/resp/inform3/error", Config)),
|
||||
%% The set-cookie header is special. set_resp_cookie must be used.
|
||||
{500, _} = do_get_inform("/resp/inform3/set_cookie", Config),
|
||||
{500, _} = do_maybe_h3_error2(do_get_inform("/resp/inform3/set_cookie", Config)),
|
||||
{102, Headers, 200, _, _} = do_get_inform("/resp/inform3/twice", Config),
|
||||
%% @todo How to test this properly? This isn't enough.
|
||||
{200, _} = do_get_inform("/resp/inform3/after_reply", Config),
|
||||
ok.
|
||||
%% With HTTP/1.1 and HTTP/2 we will not get an error.
|
||||
%% With HTTP/3 however the stream will occasionally
|
||||
%% be reset before Gun receives the response.
|
||||
case do_get_inform("/resp/inform3/after_reply", Config) of
|
||||
{200, _} ->
|
||||
ok;
|
||||
{stream_error, h3_internal_error, _} ->
|
||||
ok
|
||||
end.
|
||||
|
||||
reply2(Config) ->
|
||||
doc("Response with default headers and no body."),
|
||||
|
@ -924,7 +977,7 @@ reply2(Config) ->
|
|||
{201, _, _} = do_get("/resp/reply2/201", Config),
|
||||
{404, _, _} = do_get("/resp/reply2/404", Config),
|
||||
{200, _, _} = do_get("/resp/reply2/binary", Config),
|
||||
{500, _, _} = do_get("/resp/reply2/error", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/resp/reply2/error", Config)),
|
||||
%% @todo How to test this properly? This isn't enough.
|
||||
{200, _, _} = do_get("/resp/reply2/twice", Config),
|
||||
ok.
|
||||
|
@ -937,9 +990,9 @@ reply3(Config) ->
|
|||
true = lists:keymember(<<"content-type">>, 1, Headers2),
|
||||
{404, Headers3, _} = do_get("/resp/reply3/404", Config),
|
||||
true = lists:keymember(<<"content-type">>, 1, Headers3),
|
||||
{500, _, _} = do_get("/resp/reply3/error", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/resp/reply3/error", Config)),
|
||||
%% The set-cookie header is special. set_resp_cookie must be used.
|
||||
{500, _, _} = do_get("/resp/reply3/set_cookie", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/resp/reply3/set_cookie", Config)),
|
||||
ok.
|
||||
|
||||
reply4(Config) ->
|
||||
|
@ -947,9 +1000,9 @@ reply4(Config) ->
|
|||
{200, _, <<"OK">>} = do_get("/resp/reply4/200", Config),
|
||||
{201, _, <<"OK">>} = do_get("/resp/reply4/201", Config),
|
||||
{404, _, <<"OK">>} = do_get("/resp/reply4/404", Config),
|
||||
{500, _, _} = do_get("/resp/reply4/error", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/resp/reply4/error", Config)),
|
||||
%% The set-cookie header is special. set_resp_cookie must be used.
|
||||
{500, _, _} = do_get("/resp/reply4/set_cookie", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/resp/reply4/set_cookie", Config)),
|
||||
ok.
|
||||
|
||||
stream_reply2(Config) ->
|
||||
|
@ -959,12 +1012,11 @@ stream_reply2(Config) ->
|
|||
{201, _, Body} = do_get("/resp/stream_reply2/201", Config),
|
||||
{404, _, Body} = do_get("/resp/stream_reply2/404", Config),
|
||||
{200, _, Body} = do_get("/resp/stream_reply2/binary", Config),
|
||||
{500, _, _} = do_get("/resp/stream_reply2/error", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/resp/stream_reply2/error", Config)),
|
||||
ok.
|
||||
|
||||
stream_reply2_twice(Config) ->
|
||||
doc("Attempting to stream a response twice results in a crash. "
|
||||
"This crash can only be properly detected in HTTP/2."),
|
||||
doc("Attempting to stream a response twice results in a crash."),
|
||||
ConnPid = gun_open(Config),
|
||||
Ref = gun:get(ConnPid, "/resp/stream_reply2/twice",
|
||||
[{<<"accept-encoding">>, <<"gzip">>}]),
|
||||
|
@ -983,8 +1035,10 @@ stream_reply2_twice(Config) ->
|
|||
zlib:inflateInit(Z, 31),
|
||||
0 = iolist_size(zlib:inflate(Z, Data)),
|
||||
ok;
|
||||
%% In HTTP/2 the stream gets reset with an appropriate error.
|
||||
%% In HTTP/2 and HTTP/3 the stream gets reset with an appropriate error.
|
||||
{http2, _, {error, {stream_error, {stream_error, internal_error, _}}}} ->
|
||||
ok;
|
||||
{http3, _, {error, {stream_error, {stream_error, h3_internal_error, _}}}} ->
|
||||
ok
|
||||
end,
|
||||
gun:close(ConnPid).
|
||||
|
@ -998,9 +1052,9 @@ stream_reply3(Config) ->
|
|||
true = lists:keymember(<<"content-type">>, 1, Headers2),
|
||||
{404, Headers3, Body} = do_get("/resp/stream_reply3/404", Config),
|
||||
true = lists:keymember(<<"content-type">>, 1, Headers3),
|
||||
{500, _, _} = do_get("/resp/stream_reply3/error", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/resp/stream_reply3/error", Config)),
|
||||
%% The set-cookie header is special. set_resp_cookie must be used.
|
||||
{500, _, _} = do_get("/resp/stream_reply3/set_cookie", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/resp/stream_reply3/set_cookie", Config)),
|
||||
ok.
|
||||
|
||||
stream_body_fin0(Config) ->
|
||||
|
@ -1084,8 +1138,11 @@ stream_body_content_length_nofin_error(Config) ->
|
|||
end
|
||||
end;
|
||||
http2 ->
|
||||
%% @todo HTTP2 should have the same content-length checks
|
||||
ok
|
||||
%% @todo HTTP/2 should have the same content-length checks.
|
||||
{skip, "Implement the test for HTTP/2."};
|
||||
http3 ->
|
||||
%% @todo HTTP/3 should have the same content-length checks.
|
||||
{skip, "Implement the test for HTTP/3."}
|
||||
end.
|
||||
|
||||
stream_body_concurrent(Config) ->
|
||||
|
@ -1187,16 +1244,24 @@ stream_trailers_set_cookie(Config) ->
|
|||
{<<"accept-encoding">>, <<"gzip">>},
|
||||
{<<"te">>, <<"trailers">>}
|
||||
]),
|
||||
{response, nofin, 200, _} = gun:await(ConnPid, Ref, infinity),
|
||||
case config(protocol, Config) of
|
||||
http ->
|
||||
Protocol = config(protocol, Config),
|
||||
case gun:await(ConnPid, Ref, infinity) of
|
||||
{response, nofin, 200, _} when Protocol =:= http ->
|
||||
%% Trailers are not sent because of the stream error.
|
||||
{ok, _Body} = gun:await_body(ConnPid, Ref, infinity),
|
||||
{error, timeout} = gun:await_body(ConnPid, Ref, 1000),
|
||||
ok;
|
||||
http2 ->
|
||||
{response, nofin, 200, _} when Protocol =:= http2 ->
|
||||
{error, {stream_error, {stream_error, internal_error, _}}}
|
||||
= gun:await_body(ConnPid, Ref, infinity),
|
||||
ok;
|
||||
{response, nofin, 200, _} when Protocol =:= http3 ->
|
||||
{error, {stream_error, {stream_error, h3_internal_error, _}}}
|
||||
= gun:await_body(ConnPid, Ref, infinity),
|
||||
ok;
|
||||
%% The RST_STREAM arrived before the start of the response.
|
||||
%% See maybe_h3_error comment for details.
|
||||
{error, {stream_error, {stream_error, h3_internal_error, _}}} when Protocol =:= http3 ->
|
||||
ok
|
||||
end,
|
||||
gun:close(ConnPid).
|
||||
|
@ -1224,34 +1289,45 @@ do_trailers(Path, Config) ->
|
|||
push(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_push_http("/resp/push", Config);
|
||||
http2 -> do_push_http2(Config)
|
||||
http2 -> do_push_http2(Config);
|
||||
http3 -> {skip, "Implement server push for HTTP/3."}
|
||||
end.
|
||||
|
||||
push_after_reply(Config) ->
|
||||
doc("Trying to push a response after the final response results in a crash."),
|
||||
ConnPid = gun_open(Config),
|
||||
Ref = gun:get(ConnPid, "/resp/push/after_reply", []),
|
||||
%% @todo How to test this properly? This isn't enough.
|
||||
{response, fin, 200, _} = gun:await(ConnPid, Ref, infinity),
|
||||
%% With HTTP/1.1 and HTTP/2 we will not get an error.
|
||||
%% With HTTP/3 however the stream will occasionally
|
||||
%% be reset before Gun receives the response.
|
||||
case gun:await(ConnPid, Ref, infinity) of
|
||||
{response, fin, 200, _} ->
|
||||
ok;
|
||||
{error, {stream_error, {stream_error, h3_internal_error, _}}} ->
|
||||
ok
|
||||
end,
|
||||
gun:close(ConnPid).
|
||||
|
||||
push_method(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_push_http("/resp/push/method", Config);
|
||||
http2 -> do_push_http2_method(Config)
|
||||
http2 -> do_push_http2_method(Config);
|
||||
http3 -> {skip, "Implement server push for HTTP/3."}
|
||||
end.
|
||||
|
||||
|
||||
push_origin(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_push_http("/resp/push/origin", Config);
|
||||
http2 -> do_push_http2_origin(Config)
|
||||
http2 -> do_push_http2_origin(Config);
|
||||
http3 -> {skip, "Implement server push for HTTP/3."}
|
||||
end.
|
||||
|
||||
push_qs(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_push_http("/resp/push/qs", Config);
|
||||
http2 -> do_push_http2_qs(Config)
|
||||
http2 -> do_push_http2_qs(Config);
|
||||
http3 -> {skip, "Implement server push for HTTP/3."}
|
||||
end.
|
||||
|
||||
do_push_http(Path, Config) ->
|
||||
|
|
|
@ -32,7 +32,7 @@ init_per_group(Name, Config) ->
|
|||
cowboy_test:init_common_groups(Name, Config, ?MODULE).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
%% Dispatch configuration.
|
||||
|
||||
|
@ -85,7 +85,7 @@ accept_callback_missing(Config) ->
|
|||
{<<"accept-encoding">>, <<"gzip">>},
|
||||
{<<"content-type">>, <<"text/plain">>}
|
||||
], <<"Missing!">>),
|
||||
{response, fin, 500, _} = gun:await(ConnPid, Ref),
|
||||
{response, fin, 500, _} = do_maybe_h3_error(gun:await(ConnPid, Ref)),
|
||||
ok.
|
||||
|
||||
accept_callback_patch_false(Config) ->
|
||||
|
@ -472,7 +472,7 @@ delete_resource_missing(Config) ->
|
|||
Ref = gun:delete(ConnPid, "/delete_resource?missing", [
|
||||
{<<"accept-encoding">>, <<"gzip">>}
|
||||
]),
|
||||
{response, _, 500, _} = gun:await(ConnPid, Ref),
|
||||
{response, _, 500, _} = do_maybe_h3_error(gun:await(ConnPid, Ref)),
|
||||
ok.
|
||||
|
||||
create_resource_created(Config) ->
|
||||
|
@ -650,10 +650,16 @@ do_generate_etag(Config, Qs, ReqHeaders, Status, Etag) ->
|
|||
{<<"accept-encoding">>, <<"gzip">>}
|
||||
|ReqHeaders
|
||||
]),
|
||||
{response, _, Status, RespHeaders} = gun:await(ConnPid, Ref),
|
||||
{response, _, Status, RespHeaders} = do_maybe_h3_error(gun:await(ConnPid, Ref)),
|
||||
Etag = lists:keyfind(<<"etag">>, 1, RespHeaders),
|
||||
ok.
|
||||
|
||||
%% See do_maybe_h3_error2 comment.
|
||||
do_maybe_h3_error({error, {stream_error, {stream_error, h3_internal_error, _}}}) ->
|
||||
{response, fin, 500, []};
|
||||
do_maybe_h3_error(Result) ->
|
||||
Result.
|
||||
|
||||
if_range_etag_equal(Config) ->
|
||||
doc("When the if-range header matches, a 206 partial content "
|
||||
"response is expected for an otherwise valid range request. (RFC7233 3.2)"),
|
||||
|
@ -806,7 +812,7 @@ provide_callback_missing(Config) ->
|
|||
doc("A 500 response must be sent when the ProvideCallback can't be called."),
|
||||
ConnPid = gun_open(Config),
|
||||
Ref = gun:get(ConnPid, "/provide_callback_missing", [{<<"accept-encoding">>, <<"gzip">>}]),
|
||||
{response, fin, 500, _} = gun:await(ConnPid, Ref),
|
||||
{response, fin, 500, _} = do_maybe_h3_error(gun:await(ConnPid, Ref)),
|
||||
ok.
|
||||
|
||||
provide_range_callback(Config) ->
|
||||
|
@ -962,7 +968,7 @@ provide_range_callback_missing(Config) ->
|
|||
{<<"accept-encoding">>, <<"gzip">>},
|
||||
{<<"range">>, <<"bytes=0-">>}
|
||||
]),
|
||||
{response, fin, 500, _} = gun:await(ConnPid, Ref),
|
||||
{response, fin, 500, _} = do_maybe_h3_error(gun:await(ConnPid, Ref)),
|
||||
ok.
|
||||
|
||||
range_ignore_unknown_unit(Config) ->
|
||||
|
|
|
@ -30,7 +30,7 @@ init_per_group(Name, Config) ->
|
|||
cowboy_test:init_common_groups(Name, Config, ?MODULE).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
init_dispatch(_) ->
|
||||
cowboy_router:compile([{"[...]", [
|
||||
|
|
|
@ -35,7 +35,7 @@ init_per_group(Name, Config) ->
|
|||
cowboy_test:init_common_groups(Name, Config, ?MODULE).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
init_dispatch(_) ->
|
||||
cowboy_router:compile([{"[...]", [
|
||||
|
@ -237,6 +237,8 @@ http10_expect(Config) ->
|
|||
http ->
|
||||
do_http10_expect(Config);
|
||||
http2 ->
|
||||
expect(Config);
|
||||
http3 ->
|
||||
expect(Config)
|
||||
end.
|
||||
|
||||
|
@ -303,6 +305,9 @@ expect_discard_body_close(Config) ->
|
|||
do_expect_discard_body_close(Config);
|
||||
http2 ->
|
||||
doc("There's no reason to close the connection when using HTTP/2, "
|
||||
"even if a stream body is too large. We just cancel the stream.");
|
||||
http3 ->
|
||||
doc("There's no reason to close the connection when using HTTP/3, "
|
||||
"even if a stream body is too large. We just cancel the stream.")
|
||||
end.
|
||||
|
||||
|
@ -424,8 +429,10 @@ http10_status_code_100(Config) ->
|
|||
http ->
|
||||
doc("The 100 Continue status code must not "
|
||||
"be sent to HTTP/1.0 endpoints. (RFC7231 6.2)"),
|
||||
do_http10_status_code_1xx(100, Config);
|
||||
do_unsupported_status_code_1xx(100, Config);
|
||||
http2 ->
|
||||
status_code_100(Config);
|
||||
http3 ->
|
||||
status_code_100(Config)
|
||||
end.
|
||||
|
||||
|
@ -434,12 +441,16 @@ http10_status_code_101(Config) ->
|
|||
http ->
|
||||
doc("The 101 Switching Protocols status code must not "
|
||||
"be sent to HTTP/1.0 endpoints. (RFC7231 6.2)"),
|
||||
do_http10_status_code_1xx(101, Config);
|
||||
do_unsupported_status_code_1xx(101, Config);
|
||||
http2 ->
|
||||
status_code_101(Config);
|
||||
http3 ->
|
||||
%% While 101 is not supported by HTTP/3, there is no
|
||||
%% wording in RFC9114 that forbids sending it.
|
||||
status_code_101(Config)
|
||||
end.
|
||||
|
||||
do_http10_status_code_1xx(StatusCode, Config) ->
|
||||
do_unsupported_status_code_1xx(StatusCode, Config) ->
|
||||
ConnPid = gun_open(Config, #{http_opts => #{version => 'HTTP/1.0'}}),
|
||||
Ref = gun:get(ConnPid, "/resp/inform2/" ++ integer_to_list(StatusCode), [
|
||||
{<<"accept-encoding">>, <<"gzip">>}
|
||||
|
@ -653,7 +664,9 @@ status_code_408_connection_close(Config) ->
|
|||
http ->
|
||||
do_http11_status_code_408_connection_close(Config);
|
||||
http2 ->
|
||||
doc("HTTP/2 connections are not closed on 408 responses.")
|
||||
doc("HTTP/2 connections are not closed on 408 responses.");
|
||||
http3 ->
|
||||
doc("HTTP/3 connections are not closed on 408 responses.")
|
||||
end.
|
||||
|
||||
do_http11_status_code_408_connection_close(Config) ->
|
||||
|
@ -744,7 +757,9 @@ status_code_426_upgrade_header(Config) ->
|
|||
http ->
|
||||
do_status_code_426_upgrade_header(Config);
|
||||
http2 ->
|
||||
doc("HTTP/2 does not support the HTTP/1.1 Upgrade mechanism.")
|
||||
doc("HTTP/2 does not support the HTTP/1.1 Upgrade mechanism.");
|
||||
http3 ->
|
||||
doc("HTTP/3 does not support the HTTP/1.1 Upgrade mechanism.")
|
||||
end.
|
||||
|
||||
do_status_code_426_upgrade_header(Config) ->
|
||||
|
|
|
@ -30,7 +30,7 @@ init_per_group(Name, Config) ->
|
|||
cowboy_test:init_common_groups(Name, Config, ?MODULE).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
init_dispatch(_) ->
|
||||
cowboy_router:compile([{"[...]", [
|
||||
|
|
|
@ -34,9 +34,9 @@
|
|||
all() -> [{group, clear}, {group, tls}].
|
||||
|
||||
groups() ->
|
||||
Modules = ct_helper:all(?MODULE),
|
||||
Clear = [M || M <- Modules, lists:sublist(atom_to_list(M), 4) =/= "alpn"] -- [prior_knowledge_reject_tls],
|
||||
TLS = [M || M <- Modules, lists:sublist(atom_to_list(M), 4) =:= "alpn"] ++ [prior_knowledge_reject_tls],
|
||||
Tests = ct_helper:all(?MODULE),
|
||||
Clear = [T || T <- Tests, lists:sublist(atom_to_list(T), 4) =/= "alpn"] -- [prior_knowledge_reject_tls],
|
||||
TLS = [T || T <- Tests, lists:sublist(atom_to_list(T), 4) =:= "alpn"] ++ [prior_knowledge_reject_tls],
|
||||
[{clear, [parallel], Clear}, {tls, [parallel], TLS}].
|
||||
|
||||
init_per_group(Name = clear, Config) ->
|
||||
|
@ -3893,6 +3893,7 @@ accept_host_header_on_missing_pseudo_header_authority(Config) ->
|
|||
%% When both :authority and host headers are received, the current behavior
|
||||
%% is to favor :authority and ignore the host header. The specification does
|
||||
%% not describe the correct behavior to follow in that case.
|
||||
%% @todo The HTTP/3 spec says both values must be identical and non-empty.
|
||||
|
||||
reject_many_pseudo_header_authority(Config) ->
|
||||
doc("A request containing more than one authority component must be rejected "
|
||||
|
|
|
@ -30,7 +30,7 @@ init_per_group(Name, Config) ->
|
|||
cowboy_test:init_common_groups(Name, Config, ?MODULE).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
init_dispatch(_) ->
|
||||
cowboy_router:compile([{"[...]", [
|
||||
|
|
|
@ -126,6 +126,7 @@ reject_handshake_disabled_by_default(Config0) ->
|
|||
|
||||
% The Extended CONNECT Method.
|
||||
|
||||
%% @todo Refer to RFC9110 7.8 about the case insensitive comparison.
|
||||
accept_uppercase_pseudo_header_protocol(Config) ->
|
||||
doc("The :protocol pseudo header is case insensitive. (draft-01 4)"),
|
||||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||
|
@ -172,6 +173,7 @@ reject_many_pseudo_header_protocol(Config) ->
|
|||
ok.
|
||||
|
||||
reject_unknown_pseudo_header_protocol(Config) ->
|
||||
%% @todo This probably shouldn't send 400 but 501 instead based on RFC 9220.
|
||||
doc("An extended CONNECT request with an unknown protocol must be rejected "
|
||||
"with a 400 error. (draft-01 4)"),
|
||||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||
|
@ -192,10 +194,11 @@ reject_unknown_pseudo_header_protocol(Config) ->
|
|||
{ok, << Len1:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000),
|
||||
{ok, RespHeadersBlock} = gen_tcp:recv(Socket, Len1, 1000),
|
||||
{RespHeaders, _} = cow_hpack:decode(RespHeadersBlock),
|
||||
{_, <<"400">>} = lists:keyfind(<<":status">>, 1, RespHeaders),
|
||||
{_, <<"501">>} = lists:keyfind(<<":status">>, 1, RespHeaders),
|
||||
ok.
|
||||
|
||||
reject_invalid_pseudo_header_protocol(Config) ->
|
||||
%% @todo This probably shouldn't send 400 but 501 instead based on RFC 9220.
|
||||
doc("An extended CONNECT request with an invalid protocol must be rejected "
|
||||
"with a 400 error. (draft-01 4)"),
|
||||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||
|
@ -216,7 +219,7 @@ reject_invalid_pseudo_header_protocol(Config) ->
|
|||
{ok, << Len1:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000),
|
||||
{ok, RespHeadersBlock} = gen_tcp:recv(Socket, Len1, 1000),
|
||||
{RespHeaders, _} = cow_hpack:decode(RespHeadersBlock),
|
||||
{_, <<"400">>} = lists:keyfind(<<":status">>, 1, RespHeaders),
|
||||
{_, <<"501">>} = lists:keyfind(<<":status">>, 1, RespHeaders),
|
||||
ok.
|
||||
|
||||
reject_missing_pseudo_header_scheme(Config) ->
|
||||
|
@ -293,7 +296,7 @@ reject_missing_pseudo_header_protocol(Config) ->
|
|||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||
{ok, Socket, Settings} = do_handshake(Config),
|
||||
#{enable_connect_protocol := true} = Settings,
|
||||
%% Send an extended CONNECT request without a :scheme pseudo-header.
|
||||
%% Send an extended CONNECT request without a :protocol pseudo-header.
|
||||
{ReqHeadersBlock, _} = cow_hpack:encode([
|
||||
{<<":method">>, <<"CONNECT">>},
|
||||
{<<":scheme">>, <<"http">>},
|
||||
|
@ -317,7 +320,7 @@ reject_connection_header(Config) ->
|
|||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||
{ok, Socket, Settings} = do_handshake(Config),
|
||||
#{enable_connect_protocol := true} = Settings,
|
||||
%% Send an extended CONNECT request without a :scheme pseudo-header.
|
||||
%% Send an extended CONNECT request with a connection header.
|
||||
{ReqHeadersBlock, _} = cow_hpack:encode([
|
||||
{<<":method">>, <<"CONNECT">>},
|
||||
{<<":protocol">>, <<"websocket">>},
|
||||
|
@ -339,7 +342,7 @@ reject_upgrade_header(Config) ->
|
|||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||
{ok, Socket, Settings} = do_handshake(Config),
|
||||
#{enable_connect_protocol := true} = Settings,
|
||||
%% Send an extended CONNECT request without a :scheme pseudo-header.
|
||||
%% Send an extended CONNECT request with a upgrade header.
|
||||
{ReqHeadersBlock, _} = cow_hpack:encode([
|
||||
{<<":method">>, <<"CONNECT">>},
|
||||
{<<":protocol">>, <<"websocket">>},
|
||||
|
|
2426
test/rfc9114_SUITE.erl
Normal file
2426
test/rfc9114_SUITE.erl
Normal file
File diff suppressed because it is too large
Load diff
5
test/rfc9114_SUITE_data/client.key
Normal file
5
test/rfc9114_SUITE_data/client.key
Normal file
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgVJakPYfQA1Hr6Gnq
|
||||
GYmpMfXpxUi2QwDBrZfw8dBcVqKhRANCAAQDHeeAvjwD7p+Mg1F+G9FBNy+7Wcms
|
||||
HEw4sGMzhUL4wjwsqKHpoiuQg3qUXXK0gamx0l77vFjrUc6X1al4+ZM5
|
||||
-----END PRIVATE KEY-----
|
12
test/rfc9114_SUITE_data/client.pem
Normal file
12
test/rfc9114_SUITE_data/client.pem
Normal file
|
@ -0,0 +1,12 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIBtTCCAVugAwIBAgIUeAPi9oyMIE/KRpsRdukfx2eMuuswCgYIKoZIzj0EAwIw
|
||||
IDELMAkGA1UEBhMCU0UxETAPBgNVBAoMCE5PQk9EWUFCMB4XDTIzMDcwNTEwMjIy
|
||||
MloXDTI0MTExNjEwMjIyMlowMTELMAkGA1UEBhMCU0UxETAPBgNVBAoMCE5PQk9E
|
||||
WUFCMQ8wDQYDVQQDDAZjbGllbnQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQD
|
||||
HeeAvjwD7p+Mg1F+G9FBNy+7WcmsHEw4sGMzhUL4wjwsqKHpoiuQg3qUXXK0gamx
|
||||
0l77vFjrUc6X1al4+ZM5o2IwYDALBgNVHQ8EBAMCA4gwEQYDVR0RBAowCIIGY2xp
|
||||
ZW50MB0GA1UdDgQWBBTnhPpO+rSIFAxvkwVjlkKOO2jOeDAfBgNVHSMEGDAWgBSD
|
||||
Hw8A4XXG3jB1Atrqux7AUsf+KjAKBggqhkjOPQQDAgNIADBFAiEA2qf29EBp2hcL
|
||||
sEO7MM0ZLm4gnaMdcxtyneF3+c7Lg3cCIBFTVP8xHlhCJyb8ESV7S052VU0bKQFN
|
||||
ioyoYtcycxuZ
|
||||
-----END CERTIFICATE-----
|
5
test/rfc9114_SUITE_data/server.key
Normal file
5
test/rfc9114_SUITE_data/server.key
Normal file
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgvykUYMOS2gW8XTTh
|
||||
HgmeJM36NT8GGTNXzzt4sIs0o9ahRANCAATnQOMkKbLFQCZY/cxf8otEJG2tVuG6
|
||||
QvLqUdERV2+gzE+4ROGDqbb2Jk1szyz4CfBMB4ZfLA/PdSiO+KrOeOcj
|
||||
-----END PRIVATE KEY-----
|
12
test/rfc9114_SUITE_data/server.pem
Normal file
12
test/rfc9114_SUITE_data/server.pem
Normal file
|
@ -0,0 +1,12 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIBtTCCAVugAwIBAgIUeAPi9oyMIE/KRpsRdukfx2eMuuowCgYIKoZIzj0EAwIw
|
||||
IDELMAkGA1UEBhMCU0UxETAPBgNVBAoMCE5PQk9EWUFCMB4XDTIzMDcwNTEwMjIy
|
||||
MloXDTI0MTExNjEwMjIyMlowMTELMAkGA1UEBhMCU0UxETAPBgNVBAoMCE5PQk9E
|
||||
WUFCMQ8wDQYDVQQDDAZzZXJ2ZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATn
|
||||
QOMkKbLFQCZY/cxf8otEJG2tVuG6QvLqUdERV2+gzE+4ROGDqbb2Jk1szyz4CfBM
|
||||
B4ZfLA/PdSiO+KrOeOcjo2IwYDALBgNVHQ8EBAMCA4gwEQYDVR0RBAowCIIGc2Vy
|
||||
dmVyMB0GA1UdDgQWBBS+Np5J8BtmWU534pm9hqhrG/EQ7zAfBgNVHSMEGDAWgBSD
|
||||
Hw8A4XXG3jB1Atrqux7AUsf+KjAKBggqhkjOPQQDAgNIADBFAiEApRfjIEJfO1VH
|
||||
ETgNG3/MzDayYScPocVn4v8U15ygEw8CIFUY3xMZzJ5AmiRe9PhIUgueOKQNMtds
|
||||
wdF9+097+Ey0
|
||||
-----END CERTIFICATE-----
|
357
test/rfc9204_SUITE.erl
Normal file
357
test/rfc9204_SUITE.erl
Normal file
|
@ -0,0 +1,357 @@
|
|||
%% Copyright (c) 2024, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(rfc9204_SUITE).
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
|
||||
-import(ct_helper, [config/2]).
|
||||
-import(ct_helper, [doc/1]).
|
||||
|
||||
-ifdef(COWBOY_QUICER).
|
||||
|
||||
-include_lib("quicer/include/quicer.hrl").
|
||||
|
||||
all() ->
|
||||
[{group, h3}].
|
||||
|
||||
groups() ->
|
||||
%% @todo Enable parallel tests but for this issues in the
|
||||
%% QUIC accept loop need to be figured out (can't connect
|
||||
%% concurrently somehow, no backlog?).
|
||||
[{h3, [], ct_helper:all(?MODULE)}].
|
||||
|
||||
init_per_group(Name = h3, Config) ->
|
||||
cowboy_test:init_http3(Name, #{
|
||||
env => #{dispatch => cowboy_router:compile(init_routes(Config))}
|
||||
}, Config).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
init_routes(_) -> [
|
||||
{"localhost", [
|
||||
{"/", hello_h, []}
|
||||
]}
|
||||
].
|
||||
|
||||
%% Encoder.
|
||||
|
||||
%% 2.1
|
||||
%% QPACK preserves the ordering of field lines within
|
||||
%% each field section. An encoder MUST emit field
|
||||
%% representations in the order they appear in the
|
||||
%% input field section.
|
||||
|
||||
%% 2.1.1
|
||||
%% If the dynamic table does not contain enough room
|
||||
%% for a new entry without evicting other entries,
|
||||
%% and the entries that would be evicted are not evictable,
|
||||
%% the encoder MUST NOT insert that entry into the dynamic
|
||||
%% table (including duplicates of existing entries).
|
||||
%% In order to avoid this, an encoder that uses the
|
||||
%% dynamic table has to keep track of each dynamic
|
||||
%% table entry referenced by each field section until
|
||||
%% those representations are acknowledged by the decoder;
|
||||
%% see Section 4.4.1.
|
||||
|
||||
%% 2.1.2
|
||||
%% The decoder specifies an upper bound on the number
|
||||
%% of streams that can be blocked using the
|
||||
%% SETTINGS_QPACK_BLOCKED_STREAMS setting; see Section 5.
|
||||
%% An encoder MUST limit the number of streams that could
|
||||
%% become blocked to the value of SETTINGS_QPACK_BLOCKED_STREAMS
|
||||
%% at all times. If a decoder encounters more blocked streams
|
||||
%% than it promised to support, it MUST treat this as a
|
||||
%% connection error of type QPACK_DECOMPRESSION_FAILED.
|
||||
|
||||
%% 2.1.3
|
||||
%% To avoid these deadlocks, an encoder SHOULD NOT
|
||||
%% write an instruction unless sufficient stream and
|
||||
%% connection flow-control credit is available for
|
||||
%% the entire instruction.
|
||||
|
||||
%% Decoder.
|
||||
|
||||
%% 2.2
|
||||
%% The decoder MUST emit field lines in the order their
|
||||
%% representations appear in the encoded field section.
|
||||
|
||||
%% 2.2.1
|
||||
%% While blocked, encoded field section data SHOULD
|
||||
%% remain in the blocked stream's flow-control window.
|
||||
|
||||
%% If it encounters a Required Insert Count smaller than
|
||||
%% expected, it MUST treat this as a connection error of
|
||||
%% type QPACK_DECOMPRESSION_FAILED; see Section 2.2.3.
|
||||
|
||||
%% If it encounters a Required Insert Count larger than
|
||||
%% expected, it MAY treat this as a connection error of
|
||||
%% type QPACK_DECOMPRESSION_FAILED.
|
||||
|
||||
%% After the decoder finishes decoding a field section
|
||||
%% encoded using representations containing dynamic table
|
||||
%% references, it MUST emit a Section Acknowledgment
|
||||
%% instruction (Section 4.4.1).
|
||||
|
||||
%% 2.2.2.2
|
||||
%% A decoder with a maximum dynamic table capacity
|
||||
%% (Section 3.2.3) equal to zero MAY omit sending Stream
|
||||
%% Cancellations, because the encoder cannot have any
|
||||
%% dynamic table references.
|
||||
|
||||
%% 2.2.3
|
||||
%% If the decoder encounters a reference in a field line
|
||||
%% representation to a dynamic table entry that has already
|
||||
%% been evicted or that has an absolute index greater than
|
||||
%% or equal to the declared Required Insert Count (Section 4.5.1),
|
||||
%% it MUST treat this as a connection error of type
|
||||
%% QPACK_DECOMPRESSION_FAILED.
|
||||
|
||||
%% If the decoder encounters a reference in an encoder
|
||||
%% instruction to a dynamic table entry that has already
|
||||
%% been evicted, it MUST treat this as a connection error
|
||||
%% of type QPACK_ENCODER_STREAM_ERROR.
|
||||
|
||||
%% Static table.
|
||||
|
||||
%% 3.1
|
||||
%% When the decoder encounters an invalid static table index
|
||||
%% in a field line representation, it MUST treat this as a
|
||||
%% connection error of type QPACK_DECOMPRESSION_FAILED.
|
||||
%%
|
||||
%% If this index is received on the encoder stream, this
|
||||
%% MUST be treated as a connection error of type
|
||||
%% QPACK_ENCODER_STREAM_ERROR.
|
||||
|
||||
%% Dynamic table.
|
||||
|
||||
%% 3.2
|
||||
%% The dynamic table can contain duplicate entries
|
||||
%% (i.e., entries with the same name and same value).
|
||||
%% Therefore, duplicate entries MUST NOT be treated
|
||||
%% as an error by the decoder.
|
||||
|
||||
%% 3.2.2
|
||||
%% The encoder MUST NOT cause a dynamic table entry to be
|
||||
%% evicted unless that entry is evictable; see Section 2.1.1.
|
||||
|
||||
%% It is an error if the encoder attempts to add an entry
|
||||
%% that is larger than the dynamic table capacity; the
|
||||
%% decoder MUST treat this as a connection error of type
|
||||
%% QPACK_ENCODER_STREAM_ERROR.
|
||||
|
||||
%% 3.2.3
|
||||
%% The encoder MUST NOT set a dynamic table capacity that
|
||||
%% exceeds this maximum, but it can choose to use a lower
|
||||
%% dynamic table capacity; see Section 4.3.1.
|
||||
|
||||
%% When the client's 0-RTT value of the SETTING is zero,
|
||||
%% the server MAY set it to a non-zero value in its SETTINGS
|
||||
%% frame. If the remembered value is non-zero, the server
|
||||
%% MUST send the same non-zero value in its SETTINGS frame.
|
||||
%% If it specifies any other value, or omits
|
||||
%% SETTINGS_QPACK_MAX_TABLE_CAPACITY from SETTINGS,
|
||||
%% the encoder must treat this as a connection error of
|
||||
%% type QPACK_DECODER_STREAM_ERROR.
|
||||
|
||||
%% When the maximum table capacity is zero, the encoder
|
||||
%% MUST NOT insert entries into the dynamic table and
|
||||
%% MUST NOT send any encoder instructions on the encoder stream.
|
||||
|
||||
%% Wire format.
|
||||
|
||||
%% 4.1.1
|
||||
%% QPACK implementations MUST be able to decode integers
|
||||
%% up to and including 62 bits long.
|
||||
|
||||
%% Encoder and decoder streams.
|
||||
|
||||
decoder_reject_multiple(Config) ->
|
||||
doc("Endpoints must not create multiple decoder streams. (RFC9204 4.2)"),
|
||||
rfc9114_SUITE:do_critical_reject_multiple(Config, <<3>>).
|
||||
|
||||
encoder_reject_multiple(Config) ->
|
||||
doc("Endpoints must not create multiple encoder streams. (RFC9204 4.2)"),
|
||||
rfc9114_SUITE:do_critical_reject_multiple(Config, <<2>>).
|
||||
|
||||
%% 4.2
|
||||
%% The sender MUST NOT close either of these streams,
|
||||
%% and the receiver MUST NOT request that the sender close
|
||||
%% either of these streams. Closure of either unidirectional
|
||||
%% stream type MUST be treated as a connection error of type
|
||||
%% H3_CLOSED_CRITICAL_STREAM.
|
||||
|
||||
decoder_local_closed_abort(Config) ->
|
||||
doc("Endpoints must not close the decoder stream. (RFC9204 4.2)"),
|
||||
rfc9114_SUITE:do_critical_local_closed_abort(Config, <<3>>).
|
||||
|
||||
decoder_local_closed_graceful(Config) ->
|
||||
doc("Endpoints must not close the decoder stream. (RFC9204 4.2)"),
|
||||
rfc9114_SUITE:do_critical_local_closed_graceful(Config, <<3>>).
|
||||
|
||||
decoder_remote_closed_abort(Config) ->
|
||||
doc("Endpoints must not close the decoder stream. (RFC9204 4.2)"),
|
||||
#{conn := Conn} = rfc9114_SUITE:do_connect(Config, #{peer_unidi_stream_count => 3}),
|
||||
{ok, #{decoder := StreamRef}} = do_wait_unidi_streams(Conn, #{}),
|
||||
%% Close the control stream.
|
||||
quicer:async_shutdown_stream(StreamRef, ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT, 0),
|
||||
%% The connection should have been closed.
|
||||
#{reason := h3_closed_critical_stream} = rfc9114_SUITE:do_wait_connection_closed(Conn),
|
||||
ok.
|
||||
|
||||
encoder_local_closed_abort(Config) ->
|
||||
doc("Endpoints must not close the encoder stream. (RFC9204 4.2)"),
|
||||
rfc9114_SUITE:do_critical_local_closed_abort(Config, <<2>>).
|
||||
|
||||
encoder_local_closed_graceful(Config) ->
|
||||
doc("Endpoints must not close the encoder stream. (RFC9204 4.2)"),
|
||||
rfc9114_SUITE:do_critical_local_closed_graceful(Config, <<2>>).
|
||||
|
||||
encoder_remote_closed_abort(Config) ->
|
||||
doc("Endpoints must not close the encoder stream. (RFC9204 4.2)"),
|
||||
#{conn := Conn} = rfc9114_SUITE:do_connect(Config, #{peer_unidi_stream_count => 3}),
|
||||
{ok, #{encoder := StreamRef}} = do_wait_unidi_streams(Conn, #{}),
|
||||
%% Close the control stream.
|
||||
quicer:async_shutdown_stream(StreamRef, ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT, 0),
|
||||
%% The connection should have been closed.
|
||||
#{reason := h3_closed_critical_stream} = rfc9114_SUITE:do_wait_connection_closed(Conn),
|
||||
ok.
|
||||
|
||||
do_wait_unidi_streams(_, Acc=#{decoder := _, encoder := _}) ->
|
||||
{ok, Acc};
|
||||
do_wait_unidi_streams(Conn, Acc) ->
|
||||
receive
|
||||
{quic, new_stream, StreamRef, #{flags := Flags}} ->
|
||||
ok = quicer:setopt(StreamRef, active, true),
|
||||
true = quicer:is_unidirectional(Flags),
|
||||
receive {quic, <<TypeValue>>, StreamRef, _} ->
|
||||
Type = case TypeValue of
|
||||
2 -> encoder;
|
||||
3 -> decoder
|
||||
end,
|
||||
do_wait_unidi_streams(Conn, Acc#{Type => StreamRef})
|
||||
after 5000 ->
|
||||
{error, timeout}
|
||||
end
|
||||
after 5000 ->
|
||||
{error, timeout}
|
||||
end.
|
||||
|
||||
%% An endpoint MAY avoid creating an encoder stream if it will
|
||||
%% not be used (for example, if its encoder does not wish to
|
||||
%% use the dynamic table or if the maximum size of the dynamic
|
||||
%% table permitted by the peer is zero).
|
||||
|
||||
%% An endpoint MAY avoid creating a decoder stream if its
|
||||
%% decoder sets the maximum capacity of the dynamic table to zero.
|
||||
|
||||
%% An endpoint MUST allow its peer to create an encoder stream
|
||||
%% and a decoder stream even if the connection's settings
|
||||
%% prevent their use.
|
||||
|
||||
%% Encoder instructions.
|
||||
|
||||
%% 4.3.1
|
||||
%% The new capacity MUST be lower than or equal to the limit
|
||||
%% described in Section 3.2.3. In HTTP/3, this limit is the
|
||||
%% value of the SETTINGS_QPACK_MAX_TABLE_CAPACITY parameter
|
||||
%% (Section 5) received from the decoder. The decoder MUST
|
||||
%% treat a new dynamic table capacity value that exceeds this
|
||||
%% limit as a connection error of type QPACK_ENCODER_STREAM_ERROR.
|
||||
|
||||
%% Reducing the dynamic table capacity can cause entries to be
|
||||
%% evicted; see Section 3.2.2. This MUST NOT cause the eviction
|
||||
%% of entries that are not evictable; see Section 2.1.1.
|
||||
|
||||
%% Decoder instructions.
|
||||
|
||||
%% 4.4.1
|
||||
%% If an encoder receives a Section Acknowledgment instruction
|
||||
%% referring to a stream on which every encoded field section
|
||||
%% with a non-zero Required Insert Count has already been
|
||||
%% acknowledged, this MUST be treated as a connection error
|
||||
%% of type QPACK_DECODER_STREAM_ERROR.
|
||||
|
||||
%% 4.4.3
|
||||
%% An encoder that receives an Increment field equal to zero,
|
||||
%% or one that increases the Known Received Count beyond what
|
||||
%% the encoder has sent, MUST treat this as a connection error
|
||||
%% of type QPACK_DECODER_STREAM_ERROR.
|
||||
|
||||
%% Field line representation.
|
||||
|
||||
%% 4.5.1.1
|
||||
%% If the decoder encounters a value of EncodedInsertCount that
|
||||
%% could not have been produced by a conformant encoder, it MUST
|
||||
%% treat this as a connection error of type QPACK_DECOMPRESSION_FAILED.
|
||||
|
||||
%% 4.5.1.2
|
||||
%% The value of Base MUST NOT be negative. Though the protocol
|
||||
%% might operate correctly with a negative Base using post-Base
|
||||
%% indexing, it is unnecessary and inefficient. An endpoint MUST
|
||||
%% treat a field block with a Sign bit of 1 as invalid if the
|
||||
%% value of Required Insert Count is less than or equal to the
|
||||
%% value of Delta Base.
|
||||
|
||||
%% 4.5.4
|
||||
%% When the 'N' bit is set, the encoded field line MUST always
|
||||
%% be encoded with a literal representation. In particular,
|
||||
%% when a peer sends a field line that it received represented
|
||||
%% as a literal field line with the 'N' bit set, it MUST use a
|
||||
%% literal representation to forward this field line. This bit
|
||||
%% is intended for protecting field values that are not to be
|
||||
%% put at risk by compressing them; see Section 7.1 for more details.
|
||||
|
||||
%% Configuration.
|
||||
|
||||
%% 5
|
||||
%% SETTINGS_QPACK_MAX_TABLE_CAPACITY
|
||||
%% SETTINGS_QPACK_BLOCKED_STREAMS
|
||||
|
||||
%% Security considerations.
|
||||
|
||||
%% 7.1.2
|
||||
%% (security if used as a proxy merging many connections into one)
|
||||
%% An ideal solution segregates access to the dynamic table
|
||||
%% based on the entity that is constructing the message.
|
||||
%% Field values that are added to the table are attributed
|
||||
%% to an entity, and only the entity that created a particular
|
||||
%% value can extract that value.
|
||||
|
||||
%% 7.1.3
|
||||
%% An intermediary MUST NOT re-encode a value that uses a
|
||||
%% literal representation with the 'N' bit set with another
|
||||
%% representation that would index it. If QPACK is used for
|
||||
%% re-encoding, a literal representation with the 'N' bit set
|
||||
%% MUST be used. If HPACK is used for re-encoding, the
|
||||
%% never-indexed literal representation (see Section 6.2.3
|
||||
%% of [RFC7541]) MUST be used.
|
||||
|
||||
%% 7.4
|
||||
%% An implementation has to set a limit for the values it
|
||||
%% accepts for integers, as well as for the encoded length;
|
||||
%% see Section 4.1.1. In the same way, it has to set a limit
|
||||
%% to the length it accepts for string literals; see Section 4.1.2.
|
||||
%% These limits SHOULD be large enough to process the largest
|
||||
%% individual field the HTTP implementation can be configured
|
||||
%% to accept.
|
||||
|
||||
%% If an implementation encounters a value larger than it is
|
||||
%% able to decode, this MUST be treated as a stream error of
|
||||
%% type QPACK_DECOMPRESSION_FAILED if on a request stream or
|
||||
%% a connection error of the appropriate type if on the
|
||||
%% encoder or decoder stream.
|
||||
|
||||
-endif.
|
485
test/rfc9220_SUITE.erl
Normal file
485
test/rfc9220_SUITE.erl
Normal file
|
@ -0,0 +1,485 @@
|
|||
%% Copyright (c) 2018, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(rfc9220_SUITE).
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
|
||||
-import(ct_helper, [config/2]).
|
||||
-import(ct_helper, [doc/1]).
|
||||
|
||||
all() ->
|
||||
[{group, enabled}].
|
||||
|
||||
groups() ->
|
||||
Tests = ct_helper:all(?MODULE),
|
||||
[{enabled, [], Tests}]. %% @todo Enable parallel when all is better.
|
||||
|
||||
init_per_group(Name = enabled, Config) ->
|
||||
cowboy_test:init_http3(Name, #{
|
||||
enable_connect_protocol => true,
|
||||
env => #{dispatch => cowboy_router:compile(init_routes(Config))}
|
||||
}, Config).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
init_routes(_) -> [
|
||||
{"localhost", [
|
||||
{"/ws", ws_echo, []}
|
||||
]}
|
||||
].
|
||||
|
||||
% The SETTINGS_ENABLE_CONNECT_PROTOCOL SETTINGS Parameter.
|
||||
|
||||
% The new parameter name is SETTINGS_ENABLE_CONNECT_PROTOCOL. The
|
||||
% value of the parameter MUST be 0 or 1.
|
||||
|
||||
% Upon receipt of SETTINGS_ENABLE_CONNECT_PROTOCOL with a value of 1 a
|
||||
% client MAY use the Extended CONNECT definition of this document when
|
||||
% creating new streams. Receipt of this parameter by a server does not
|
||||
% have any impact.
|
||||
%% @todo ignore_client_enable_setting(Config) ->
|
||||
|
||||
reject_handshake_when_disabled(Config0) ->
|
||||
doc("Extended CONNECT requests MUST be rejected with a "
|
||||
"H3_MESSAGE_ERROR stream error when enable_connect_protocol=false. "
|
||||
"(RFC9220, RFC8441 4)"),
|
||||
Config = cowboy_test:init_http3(disabled, #{
|
||||
enable_connect_protocol => false,
|
||||
env => #{dispatch => cowboy_router:compile(init_routes(Config0))}
|
||||
}, Config0),
|
||||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 0.
|
||||
#{
|
||||
conn := Conn,
|
||||
settings := Settings
|
||||
} = rfc9114_SUITE:do_connect(Config),
|
||||
case Settings of
|
||||
#{enable_connect_protocol := false} -> ok;
|
||||
_ when map_size(Settings) =:= 0 -> ok
|
||||
end,
|
||||
%% Send a CONNECT :protocol request to upgrade the stream to Websocket.
|
||||
{ok, StreamRef} = quicer:start_stream(Conn, #{}),
|
||||
{ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([
|
||||
{<<":method">>, <<"CONNECT">>},
|
||||
{<<":protocol">>, <<"websocket">>},
|
||||
{<<":scheme">>, <<"https">>},
|
||||
{<<":path">>, <<"/ws">>},
|
||||
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
|
||||
{<<"sec-websocket-version">>, <<"13">>},
|
||||
{<<"origin">>, <<"http://localhost">>}
|
||||
], 0, cow_qpack:init(encoder)),
|
||||
{ok, _} = quicer:send(StreamRef, [
|
||||
<<1>>, %% HEADERS frame.
|
||||
cow_http3:encode_int(iolist_size(EncodedRequest)),
|
||||
EncodedRequest
|
||||
]),
|
||||
%% The stream should have been aborted.
|
||||
#{reason := h3_message_error} = rfc9114_SUITE:do_wait_stream_aborted(StreamRef),
|
||||
ok.
|
||||
|
||||
reject_handshake_disabled_by_default(Config0) ->
|
||||
doc("Extended CONNECT requests MUST be rejected with a "
|
||||
"H3_MESSAGE_ERROR stream error when enable_connect_protocol=false. "
|
||||
"(RFC9220, RFC8441 4)"),
|
||||
Config = cowboy_test:init_http3(disabled, #{
|
||||
env => #{dispatch => cowboy_router:compile(init_routes(Config0))}
|
||||
}, Config0),
|
||||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 0.
|
||||
#{
|
||||
conn := Conn,
|
||||
settings := Settings
|
||||
} = rfc9114_SUITE:do_connect(Config),
|
||||
case Settings of
|
||||
#{enable_connect_protocol := false} -> ok;
|
||||
_ when map_size(Settings) =:= 0 -> ok
|
||||
end,
|
||||
%% Send a CONNECT :protocol request to upgrade the stream to Websocket.
|
||||
{ok, StreamRef} = quicer:start_stream(Conn, #{}),
|
||||
{ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([
|
||||
{<<":method">>, <<"CONNECT">>},
|
||||
{<<":protocol">>, <<"websocket">>},
|
||||
{<<":scheme">>, <<"https">>},
|
||||
{<<":path">>, <<"/ws">>},
|
||||
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
|
||||
{<<"sec-websocket-version">>, <<"13">>},
|
||||
{<<"origin">>, <<"http://localhost">>}
|
||||
], 0, cow_qpack:init(encoder)),
|
||||
{ok, _} = quicer:send(StreamRef, [
|
||||
<<1>>, %% HEADERS frame.
|
||||
cow_http3:encode_int(iolist_size(EncodedRequest)),
|
||||
EncodedRequest
|
||||
]),
|
||||
%% The stream should have been aborted.
|
||||
#{reason := h3_message_error} = rfc9114_SUITE:do_wait_stream_aborted(StreamRef),
|
||||
ok.
|
||||
|
||||
% The Extended CONNECT Method.
|
||||
|
||||
accept_uppercase_pseudo_header_protocol(Config) ->
|
||||
doc("The :protocol pseudo header is case insensitive. (RFC9220, RFC8441 4, RFC9110 7.8)"),
|
||||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||
#{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config),
|
||||
#{enable_connect_protocol := true} = Settings,
|
||||
%% Send a CONNECT :protocol request to upgrade the stream to Websocket.
|
||||
{ok, StreamRef} = quicer:start_stream(Conn, #{}),
|
||||
{ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([
|
||||
{<<":method">>, <<"CONNECT">>},
|
||||
{<<":protocol">>, <<"WEBSOCKET">>},
|
||||
{<<":scheme">>, <<"https">>},
|
||||
{<<":path">>, <<"/ws">>},
|
||||
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
|
||||
{<<"sec-websocket-version">>, <<"13">>},
|
||||
{<<"origin">>, <<"http://localhost">>}
|
||||
], 0, cow_qpack:init(encoder)),
|
||||
{ok, _} = quicer:send(StreamRef, [
|
||||
<<1>>, %% HEADERS frame.
|
||||
cow_http3:encode_int(iolist_size(EncodedRequest)),
|
||||
EncodedRequest
|
||||
]),
|
||||
%% Receive a 200 response.
|
||||
{ok, Data} = rfc9114_SUITE:do_receive_data(StreamRef),
|
||||
{HLenEnc, HLenBits} = rfc9114_SUITE:do_guess_int_encoding(Data),
|
||||
<<
|
||||
1, %% HEADERS frame.
|
||||
HLenEnc:2, HLen:HLenBits,
|
||||
EncodedResponse:HLen/bytes
|
||||
>> = Data,
|
||||
{ok, DecodedResponse, _DecData, _DecSt}
|
||||
= cow_qpack:decode_field_section(EncodedResponse, 0, cow_qpack:init(decoder)),
|
||||
#{<<":status">> := <<"200">>} = maps:from_list(DecodedResponse),
|
||||
ok.
|
||||
|
||||
reject_many_pseudo_header_protocol(Config) ->
|
||||
doc("An extended CONNECT request containing more than one "
|
||||
"protocol component must be rejected with a H3_MESSAGE_ERROR "
|
||||
"stream error. (RFC9220, RFC9114 4.3.1, RFC9114 4.1.2)"),
|
||||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||
#{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config),
|
||||
#{enable_connect_protocol := true} = Settings,
|
||||
%% Send an extended CONNECT request with more than one :protocol pseudo-header.
|
||||
{ok, StreamRef} = quicer:start_stream(Conn, #{}),
|
||||
{ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([
|
||||
{<<":method">>, <<"CONNECT">>},
|
||||
{<<":protocol">>, <<"websocket">>},
|
||||
{<<":protocol">>, <<"mqtt">>},
|
||||
{<<":scheme">>, <<"https">>},
|
||||
{<<":path">>, <<"/ws">>},
|
||||
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
|
||||
{<<"sec-websocket-version">>, <<"13">>},
|
||||
{<<"origin">>, <<"http://localhost">>}
|
||||
], 0, cow_qpack:init(encoder)),
|
||||
{ok, _} = quicer:send(StreamRef, [
|
||||
<<1>>, %% HEADERS frame.
|
||||
cow_http3:encode_int(iolist_size(EncodedRequest)),
|
||||
EncodedRequest
|
||||
]),
|
||||
%% The stream should have been aborted.
|
||||
#{reason := h3_message_error} = rfc9114_SUITE:do_wait_stream_aborted(StreamRef),
|
||||
ok.
|
||||
|
||||
reject_unknown_pseudo_header_protocol(Config) ->
|
||||
doc("An extended CONNECT request containing more than one "
|
||||
"protocol component must be rejected with a 501 Not Implemented "
|
||||
"response. (RFC9220, RFC8441 4)"),
|
||||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||
#{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config),
|
||||
#{enable_connect_protocol := true} = Settings,
|
||||
%% Send an extended CONNECT request with an unknown protocol.
|
||||
{ok, StreamRef} = quicer:start_stream(Conn, #{}),
|
||||
{ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([
|
||||
{<<":method">>, <<"CONNECT">>},
|
||||
{<<":protocol">>, <<"mqtt">>},
|
||||
{<<":scheme">>, <<"https">>},
|
||||
{<<":path">>, <<"/ws">>},
|
||||
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
|
||||
{<<"sec-websocket-version">>, <<"13">>},
|
||||
{<<"origin">>, <<"http://localhost">>}
|
||||
], 0, cow_qpack:init(encoder)),
|
||||
{ok, _} = quicer:send(StreamRef, [
|
||||
<<1>>, %% HEADERS frame.
|
||||
cow_http3:encode_int(iolist_size(EncodedRequest)),
|
||||
EncodedRequest
|
||||
]),
|
||||
%% The stream should have been rejected with a 501 Not Implemented.
|
||||
#{headers := #{<<":status">> := <<"501">>}} = rfc9114_SUITE:do_receive_response(StreamRef),
|
||||
ok.
|
||||
|
||||
reject_invalid_pseudo_header_protocol(Config) ->
|
||||
doc("An extended CONNECT request with an invalid protocol "
|
||||
"component must be rejected with a 501 Not Implemented "
|
||||
"response. (RFC9220, RFC8441 4)"),
|
||||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||
#{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config),
|
||||
#{enable_connect_protocol := true} = Settings,
|
||||
%% Send an extended CONNECT request with an invalid protocol.
|
||||
{ok, StreamRef} = quicer:start_stream(Conn, #{}),
|
||||
{ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([
|
||||
{<<":method">>, <<"CONNECT">>},
|
||||
{<<":protocol">>, <<"websocket mqtt">>},
|
||||
{<<":scheme">>, <<"https">>},
|
||||
{<<":path">>, <<"/ws">>},
|
||||
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
|
||||
{<<"sec-websocket-version">>, <<"13">>},
|
||||
{<<"origin">>, <<"http://localhost">>}
|
||||
], 0, cow_qpack:init(encoder)),
|
||||
{ok, _} = quicer:send(StreamRef, [
|
||||
<<1>>, %% HEADERS frame.
|
||||
cow_http3:encode_int(iolist_size(EncodedRequest)),
|
||||
EncodedRequest
|
||||
]),
|
||||
%% The stream should have been rejected with a 501 Not Implemented.
|
||||
#{headers := #{<<":status">> := <<"501">>}} = rfc9114_SUITE:do_receive_response(StreamRef),
|
||||
ok.
|
||||
|
||||
reject_missing_pseudo_header_scheme(Config) ->
|
||||
doc("An extended CONNECT request whtout a scheme component "
|
||||
"must be rejected with a H3_MESSAGE_ERROR stream error. "
|
||||
"(RFC9220, RFC9114 4.3.1, RFC9114 4.1.2)"),
|
||||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||
#{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config),
|
||||
#{enable_connect_protocol := true} = Settings,
|
||||
%% Send an extended CONNECT request without a :scheme pseudo-header.
|
||||
{ok, StreamRef} = quicer:start_stream(Conn, #{}),
|
||||
{ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([
|
||||
{<<":method">>, <<"CONNECT">>},
|
||||
{<<":protocol">>, <<"websocket">>},
|
||||
{<<":path">>, <<"/ws">>},
|
||||
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
|
||||
{<<"sec-websocket-version">>, <<"13">>},
|
||||
{<<"origin">>, <<"http://localhost">>}
|
||||
], 0, cow_qpack:init(encoder)),
|
||||
{ok, _} = quicer:send(StreamRef, [
|
||||
<<1>>, %% HEADERS frame.
|
||||
cow_http3:encode_int(iolist_size(EncodedRequest)),
|
||||
EncodedRequest
|
||||
]),
|
||||
%% The stream should have been aborted.
|
||||
#{reason := h3_message_error} = rfc9114_SUITE:do_wait_stream_aborted(StreamRef),
|
||||
ok.
|
||||
|
||||
reject_missing_pseudo_header_path(Config) ->
|
||||
doc("An extended CONNECT request whtout a path component "
|
||||
"must be rejected with a H3_MESSAGE_ERROR stream error. "
|
||||
"(RFC9220, RFC9114 4.3.1, RFC9114 4.1.2)"),
|
||||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||
#{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config),
|
||||
#{enable_connect_protocol := true} = Settings,
|
||||
%% Send an extended CONNECT request without a :path pseudo-header.
|
||||
{ok, StreamRef} = quicer:start_stream(Conn, #{}),
|
||||
{ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([
|
||||
{<<":method">>, <<"CONNECT">>},
|
||||
{<<":protocol">>, <<"websocket">>},
|
||||
{<<":scheme">>, <<"https">>},
|
||||
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
|
||||
{<<"sec-websocket-version">>, <<"13">>},
|
||||
{<<"origin">>, <<"http://localhost">>}
|
||||
], 0, cow_qpack:init(encoder)),
|
||||
{ok, _} = quicer:send(StreamRef, [
|
||||
<<1>>, %% HEADERS frame.
|
||||
cow_http3:encode_int(iolist_size(EncodedRequest)),
|
||||
EncodedRequest
|
||||
]),
|
||||
%% The stream should have been aborted.
|
||||
#{reason := h3_message_error} = rfc9114_SUITE:do_wait_stream_aborted(StreamRef),
|
||||
ok.
|
||||
|
||||
% On requests bearing the :protocol pseudo-header, the :authority
|
||||
% pseudo-header field is interpreted according to Section 8.1.2.3 of
|
||||
% [RFC7540] instead of Section 8.3 of [RFC7540]. In particular the
|
||||
% server MUST not make a new TCP connection to the host and port
|
||||
% indicated by the :authority.
|
||||
|
||||
reject_missing_pseudo_header_authority(Config) ->
|
||||
doc("An extended CONNECT request whtout an authority component "
|
||||
"must be rejected with a H3_MESSAGE_ERROR stream error. "
|
||||
"(RFC9220, RFC9114 4.3.1, RFC9114 4.1.2)"),
|
||||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||
#{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config),
|
||||
#{enable_connect_protocol := true} = Settings,
|
||||
%% Send an extended CONNECT request without an :authority pseudo-header.
|
||||
{ok, StreamRef} = quicer:start_stream(Conn, #{}),
|
||||
{ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([
|
||||
{<<":method">>, <<"CONNECT">>},
|
||||
{<<":protocol">>, <<"websocket">>},
|
||||
{<<":scheme">>, <<"https">>},
|
||||
{<<":path">>, <<"/ws">>},
|
||||
{<<"sec-websocket-version">>, <<"13">>},
|
||||
{<<"origin">>, <<"http://localhost">>}
|
||||
], 0, cow_qpack:init(encoder)),
|
||||
{ok, _} = quicer:send(StreamRef, [
|
||||
<<1>>, %% HEADERS frame.
|
||||
cow_http3:encode_int(iolist_size(EncodedRequest)),
|
||||
EncodedRequest
|
||||
]),
|
||||
%% The stream should have been aborted.
|
||||
#{reason := h3_message_error} = rfc9114_SUITE:do_wait_stream_aborted(StreamRef),
|
||||
ok.
|
||||
|
||||
% Using Extended CONNECT To Bootstrap The WebSocket Protocol.
|
||||
|
||||
reject_missing_pseudo_header_protocol(Config) ->
|
||||
doc("An extended CONNECT request whtout a protocol component "
|
||||
"must be rejected with a H3_MESSAGE_ERROR stream error. "
|
||||
"(RFC9220, RFC9114 4.3.1, RFC9114 4.1.2)"),
|
||||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||
#{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config),
|
||||
#{enable_connect_protocol := true} = Settings,
|
||||
%% Send an extended CONNECT request without a :protocol pseudo-header.
|
||||
{ok, StreamRef} = quicer:start_stream(Conn, #{}),
|
||||
{ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([
|
||||
{<<":method">>, <<"CONNECT">>},
|
||||
{<<":scheme">>, <<"https">>},
|
||||
{<<":path">>, <<"/ws">>},
|
||||
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
|
||||
{<<"sec-websocket-version">>, <<"13">>},
|
||||
{<<"origin">>, <<"http://localhost">>}
|
||||
], 0, cow_qpack:init(encoder)),
|
||||
{ok, _} = quicer:send(StreamRef, [
|
||||
<<1>>, %% HEADERS frame.
|
||||
cow_http3:encode_int(iolist_size(EncodedRequest)),
|
||||
EncodedRequest
|
||||
]),
|
||||
%% The stream should have been aborted.
|
||||
#{reason := h3_message_error} = rfc9114_SUITE:do_wait_stream_aborted(StreamRef),
|
||||
ok.
|
||||
|
||||
% The scheme of the Target URI [RFC7230] MUST be https for wss schemed
|
||||
% WebSockets. HTTP/3 does not provide support for ws schemed WebSockets.
|
||||
% The websocket URI is still used for proxy autoconfiguration.
|
||||
|
||||
reject_connection_header(Config) ->
|
||||
doc("An extended CONNECT request with a connection header "
|
||||
"must be rejected with a H3_MESSAGE_ERROR stream error. "
|
||||
"(RFC9220, RFC8441 4, RFC9114 4.2, RFC9114 4.5, RFC9114 4.1.2)"),
|
||||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||
#{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config),
|
||||
#{enable_connect_protocol := true} = Settings,
|
||||
%% Send an extended CONNECT request with a connection header.
|
||||
{ok, StreamRef} = quicer:start_stream(Conn, #{}),
|
||||
{ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([
|
||||
{<<":method">>, <<"CONNECT">>},
|
||||
{<<":protocol">>, <<"websocket">>},
|
||||
{<<":scheme">>, <<"https">>},
|
||||
{<<":path">>, <<"/ws">>},
|
||||
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
|
||||
{<<"connection">>, <<"upgrade">>},
|
||||
{<<"sec-websocket-version">>, <<"13">>},
|
||||
{<<"origin">>, <<"http://localhost">>}
|
||||
], 0, cow_qpack:init(encoder)),
|
||||
{ok, _} = quicer:send(StreamRef, [
|
||||
<<1>>, %% HEADERS frame.
|
||||
cow_http3:encode_int(iolist_size(EncodedRequest)),
|
||||
EncodedRequest
|
||||
]),
|
||||
%% The stream should have been aborted.
|
||||
#{reason := h3_message_error} = rfc9114_SUITE:do_wait_stream_aborted(StreamRef),
|
||||
ok.
|
||||
|
||||
reject_upgrade_header(Config) ->
|
||||
doc("An extended CONNECT request with a upgrade header "
|
||||
"must be rejected with a H3_MESSAGE_ERROR stream error. "
|
||||
"(RFC9220, RFC8441 4, RFC9114 4.2, RFC9114 4.5, RFC9114 4.1.2)"),
|
||||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||
#{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config),
|
||||
#{enable_connect_protocol := true} = Settings,
|
||||
%% Send an extended CONNECT request with a upgrade header.
|
||||
{ok, StreamRef} = quicer:start_stream(Conn, #{}),
|
||||
{ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([
|
||||
{<<":method">>, <<"CONNECT">>},
|
||||
{<<":protocol">>, <<"websocket">>},
|
||||
{<<":scheme">>, <<"https">>},
|
||||
{<<":path">>, <<"/ws">>},
|
||||
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
|
||||
{<<"upgrade">>, <<"websocket">>},
|
||||
{<<"sec-websocket-version">>, <<"13">>},
|
||||
{<<"origin">>, <<"http://localhost">>}
|
||||
], 0, cow_qpack:init(encoder)),
|
||||
{ok, _} = quicer:send(StreamRef, [
|
||||
<<1>>, %% HEADERS frame.
|
||||
cow_http3:encode_int(iolist_size(EncodedRequest)),
|
||||
EncodedRequest
|
||||
]),
|
||||
%% The stream should have been aborted.
|
||||
#{reason := h3_message_error} = rfc9114_SUITE:do_wait_stream_aborted(StreamRef),
|
||||
ok.
|
||||
|
||||
% After successfully processing the opening handshake the peers should
|
||||
% proceed with The WebSocket Protocol [RFC6455] using the HTTP/2 stream
|
||||
% from the CONNECT transaction as if it were the TCP connection
|
||||
% referred to in [RFC6455]. The state of the WebSocket connection at
|
||||
% this point is OPEN as defined by [RFC6455], Section 4.1.
|
||||
%% @todo I'm guessing we should test for things like RST_STREAM,
|
||||
%% closing the connection and others?
|
||||
|
||||
% Examples.
|
||||
|
||||
accept_handshake_when_enabled(Config) ->
|
||||
doc("Confirm the example for Websocket over HTTP/2 works. (RFC9220, RFC8441 5.1)"),
|
||||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||
#{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config),
|
||||
#{enable_connect_protocol := true} = Settings,
|
||||
%% Send a CONNECT :protocol request to upgrade the stream to Websocket.
|
||||
{ok, StreamRef} = quicer:start_stream(Conn, #{}),
|
||||
{ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([
|
||||
{<<":method">>, <<"CONNECT">>},
|
||||
{<<":protocol">>, <<"websocket">>},
|
||||
{<<":scheme">>, <<"https">>},
|
||||
{<<":path">>, <<"/ws">>},
|
||||
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
|
||||
{<<"sec-websocket-version">>, <<"13">>},
|
||||
{<<"origin">>, <<"http://localhost">>}
|
||||
], 0, cow_qpack:init(encoder)),
|
||||
{ok, _} = quicer:send(StreamRef, [
|
||||
<<1>>, %% HEADERS frame.
|
||||
cow_http3:encode_int(iolist_size(EncodedRequest)),
|
||||
EncodedRequest
|
||||
]),
|
||||
%% Receive a 200 response.
|
||||
{ok, Data} = rfc9114_SUITE:do_receive_data(StreamRef),
|
||||
{HLenEnc, HLenBits} = rfc9114_SUITE:do_guess_int_encoding(Data),
|
||||
<<
|
||||
1, %% HEADERS frame.
|
||||
HLenEnc:2, HLen:HLenBits,
|
||||
EncodedResponse:HLen/bytes
|
||||
>> = Data,
|
||||
{ok, DecodedResponse, _DecData, _DecSt}
|
||||
= cow_qpack:decode_field_section(EncodedResponse, 0, cow_qpack:init(decoder)),
|
||||
#{<<":status">> := <<"200">>} = maps:from_list(DecodedResponse),
|
||||
%% Masked text hello echoed back clear by the server.
|
||||
Mask = 16#37fa213d,
|
||||
MaskedHello = ws_SUITE:do_mask(<<"Hello">>, Mask, <<>>),
|
||||
{ok, _} = quicer:send(StreamRef, cow_http3:data(
|
||||
<<1:1, 0:3, 1:4, 1:1, 5:7, Mask:32, MaskedHello/binary>>)),
|
||||
{ok, WsData} = rfc9114_SUITE:do_receive_data(StreamRef),
|
||||
<<
|
||||
0, %% DATA frame.
|
||||
0:2, 7:6, %% Length (2 bytes header + "Hello").
|
||||
1:1, 0:3, 1:4, 0:1, 5:7, "Hello" %% Websocket frame.
|
||||
>> = WsData,
|
||||
ok.
|
||||
|
||||
%% Closing a Websocket stream.
|
||||
|
||||
% The HTTP/3 stream closure is also analogous to the TCP connection
|
||||
% closure of [RFC6455]. Orderly TCP-level closures are represented
|
||||
% as a FIN bit on the stream (Section 4.4 of [HTTP/3]). RST exceptions
|
||||
% are represented with a stream error (Section 8 of [HTTP/3]) of type
|
||||
% H3_REQUEST_CANCELLED (Section 8.1 of [HTTP/3]).
|
||||
|
||||
%% @todo client close frame with FIN
|
||||
%% @todo server close frame with FIN
|
||||
%% @todo client other frame with FIN
|
||||
%% @todo server other frame with FIN
|
||||
%% @todo client close connection
|
|
@ -49,10 +49,12 @@ groups() ->
|
|||
{https, [parallel], Tests ++ H1Tests},
|
||||
{h2, [parallel], Tests},
|
||||
{h2c, [parallel], Tests ++ H2CTests},
|
||||
{h3, [], Tests},
|
||||
{http_compress, [parallel], Tests ++ H1Tests},
|
||||
{https_compress, [parallel], Tests ++ H1Tests},
|
||||
{h2_compress, [parallel], Tests},
|
||||
{h2c_compress, [parallel], Tests ++ H2CTests}
|
||||
{h2c_compress, [parallel], Tests ++ H2CTests},
|
||||
{h3_compress, [], Tests}
|
||||
].
|
||||
|
||||
init_per_suite(Config) ->
|
||||
|
@ -66,7 +68,7 @@ init_per_group(Name, Config) ->
|
|||
cowboy_test:init_common_groups(Name, Config, ?MODULE).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
%% Routes.
|
||||
|
||||
|
|
|
@ -20,6 +20,12 @@
|
|||
-import(ct_helper, [doc/1]).
|
||||
-import(cowboy_test, [gun_open/1]).
|
||||
|
||||
%% Import useful functions from req_SUITE.
|
||||
%% @todo Maybe move these functions to cowboy_test.
|
||||
-import(req_SUITE, [do_get/2]).
|
||||
-import(req_SUITE, [do_get/3]).
|
||||
-import(req_SUITE, [do_maybe_h3_error3/1]).
|
||||
|
||||
%% ct.
|
||||
|
||||
all() ->
|
||||
|
@ -39,16 +45,22 @@ groups() ->
|
|||
{dir, [parallel], DirTests},
|
||||
{priv_dir, [parallel], DirTests}
|
||||
],
|
||||
GroupTestsNoParallel = OtherTests ++ [
|
||||
{dir, [], DirTests},
|
||||
{priv_dir, [], DirTests}
|
||||
],
|
||||
[
|
||||
{http, [parallel], GroupTests},
|
||||
{https, [parallel], GroupTests},
|
||||
{h2, [parallel], GroupTests},
|
||||
{h2c, [parallel], GroupTests},
|
||||
{h3, [], GroupTestsNoParallel}, %% @todo Enable parallel when it works better.
|
||||
{http_compress, [parallel], GroupTests},
|
||||
{https_compress, [parallel], GroupTests},
|
||||
{h2_compress, [parallel], GroupTests},
|
||||
{h2c_compress, [parallel], GroupTests},
|
||||
%% No real need to test sendfile disabled against https or h2.
|
||||
{h3_compress, [], GroupTestsNoParallel}, %% @todo Enable parallel when it works better.
|
||||
%% No real need to test sendfile disabled against https, h2 or h3.
|
||||
{http_no_sendfile, [parallel], GroupTests},
|
||||
{h2c_no_sendfile, [parallel], GroupTests}
|
||||
].
|
||||
|
@ -116,6 +128,17 @@ init_per_group(Name=h2c_no_sendfile, Config) ->
|
|||
sendfile => false
|
||||
}, [{flavor, vanilla}|Config]),
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2});
|
||||
init_per_group(Name=h3, Config) ->
|
||||
cowboy_test:init_http3(Name, #{
|
||||
env => #{dispatch => init_dispatch(Config)},
|
||||
middlewares => [?MODULE, cowboy_router, cowboy_handler]
|
||||
}, [{flavor, vanilla}|Config]);
|
||||
init_per_group(Name=h3_compress, Config) ->
|
||||
cowboy_test:init_http3(Name, #{
|
||||
env => #{dispatch => init_dispatch(Config)},
|
||||
middlewares => [?MODULE, cowboy_router, cowboy_handler],
|
||||
stream_handlers => [cowboy_compress_h, cowboy_stream_h]
|
||||
}, [{flavor, vanilla}|Config]);
|
||||
init_per_group(Name, Config) ->
|
||||
Config1 = cowboy_test:init_common_groups(Name, Config, ?MODULE),
|
||||
Opts = ranch:get_protocol_options(Name),
|
||||
|
@ -129,7 +152,7 @@ end_per_group(dir, _) ->
|
|||
end_per_group(priv_dir, _) ->
|
||||
ok;
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
%% Large file.
|
||||
|
||||
|
@ -248,25 +271,11 @@ do_mime_custom(Path) ->
|
|||
_ -> {<<"application">>, <<"octet-stream">>, []}
|
||||
end.
|
||||
|
||||
do_get(Path, Config) ->
|
||||
do_get(Path, [], Config).
|
||||
|
||||
do_get(Path, ReqHeaders, Config) ->
|
||||
ConnPid = gun_open(Config),
|
||||
Ref = gun:get(ConnPid, Path, [{<<"accept-encoding">>, <<"gzip">>}|ReqHeaders]),
|
||||
{response, IsFin, Status, RespHeaders} = gun:await(ConnPid, Ref),
|
||||
{ok, Body} = case IsFin of
|
||||
nofin -> gun:await_body(ConnPid, Ref);
|
||||
fin -> {ok, <<>>}
|
||||
end,
|
||||
gun:close(ConnPid),
|
||||
{Status, RespHeaders, Body}.
|
||||
|
||||
%% Tests.
|
||||
|
||||
bad(Config) ->
|
||||
doc("Bad cowboy_static options: not a tuple."),
|
||||
{500, _, _} = do_get("/bad", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/bad", Config)),
|
||||
ok.
|
||||
|
||||
bad_dir_path(Config) ->
|
||||
|
@ -276,7 +285,7 @@ bad_dir_path(Config) ->
|
|||
|
||||
bad_dir_route(Config) ->
|
||||
doc("Bad cowboy_static options: missing [...] in route."),
|
||||
{500, _, _} = do_get("/bad/dir/route", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/bad/dir/route", Config)),
|
||||
ok.
|
||||
|
||||
bad_file_in_priv_dir_in_ez_archive(Config) ->
|
||||
|
@ -291,27 +300,27 @@ bad_file_path(Config) ->
|
|||
|
||||
bad_options(Config) ->
|
||||
doc("Bad cowboy_static extra options: not a list."),
|
||||
{500, _, _} = do_get("/bad/options", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/bad/options", Config)),
|
||||
ok.
|
||||
|
||||
bad_options_charset(Config) ->
|
||||
doc("Bad cowboy_static extra options: invalid charset option."),
|
||||
{500, _, _} = do_get("/bad/options/charset", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/bad/options/charset", Config)),
|
||||
ok.
|
||||
|
||||
bad_options_etag(Config) ->
|
||||
doc("Bad cowboy_static extra options: invalid etag option."),
|
||||
{500, _, _} = do_get("/bad/options/etag", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/bad/options/etag", Config)),
|
||||
ok.
|
||||
|
||||
bad_options_mime(Config) ->
|
||||
doc("Bad cowboy_static extra options: invalid mimetypes option."),
|
||||
{500, _, _} = do_get("/bad/options/mime", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/bad/options/mime", Config)),
|
||||
ok.
|
||||
|
||||
bad_priv_dir_app(Config) ->
|
||||
doc("Bad cowboy_static options: wrong application name."),
|
||||
{500, _, _} = do_get("/bad/priv_dir/app/style.css", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/bad/priv_dir/app/style.css", Config)),
|
||||
ok.
|
||||
|
||||
bad_priv_dir_in_ez_archive(Config) ->
|
||||
|
@ -331,12 +340,12 @@ bad_priv_dir_path(Config) ->
|
|||
|
||||
bad_priv_dir_route(Config) ->
|
||||
doc("Bad cowboy_static options: missing [...] in route."),
|
||||
{500, _, _} = do_get("/bad/priv_dir/route", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/bad/priv_dir/route", Config)),
|
||||
ok.
|
||||
|
||||
bad_priv_file_app(Config) ->
|
||||
doc("Bad cowboy_static options: wrong application name."),
|
||||
{500, _, _} = do_get("/bad/priv_file/app", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/bad/priv_file/app", Config)),
|
||||
ok.
|
||||
|
||||
bad_priv_file_in_ez_archive(Config) ->
|
||||
|
@ -535,7 +544,7 @@ dir_unknown(Config) ->
|
|||
|
||||
etag_crash(Config) ->
|
||||
doc("Get a file with a crashing etag function."),
|
||||
{500, _, _} = do_get("/etag/crash", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/etag/crash", Config)),
|
||||
ok.
|
||||
|
||||
etag_custom(Config) ->
|
||||
|
@ -813,7 +822,7 @@ mime_all_uppercase(Config) ->
|
|||
|
||||
mime_crash(Config) ->
|
||||
doc("Get a file with a crashing mimetype function."),
|
||||
{500, _, _} = do_get("/mime/crash/style.css", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/mime/crash/style.css", Config)),
|
||||
ok.
|
||||
|
||||
mime_custom_cowboy(Config) ->
|
||||
|
@ -848,7 +857,7 @@ mime_hardcode_tuple(Config) ->
|
|||
|
||||
charset_crash(Config) ->
|
||||
doc("Get a file with a crashing charset function."),
|
||||
{500, _, _} = do_get("/charset/crash/style.css", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/charset/crash/style.css", Config)),
|
||||
ok.
|
||||
|
||||
charset_custom_cowboy(Config) ->
|
||||
|
@ -933,7 +942,8 @@ unicode_basic_error(Config) ->
|
|||
%% # and ? indicate fragment and query components
|
||||
%% and are therefore not part of the path.
|
||||
http -> "\r\s#?";
|
||||
http2 -> "#?"
|
||||
http2 -> "#?";
|
||||
http3 -> "#?"
|
||||
end,
|
||||
_ = [case do_get("/char/" ++ [C], Config) of
|
||||
{400, _, _} -> ok;
|
||||
|
|
|
@ -31,50 +31,42 @@ groups() ->
|
|||
|
||||
%% We set this module as a logger in order to silence expected errors.
|
||||
init_per_group(Name = http, Config) ->
|
||||
cowboy_test:init_http(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [stream_handler_h]
|
||||
}, Config);
|
||||
cowboy_test:init_http(Name, init_plain_opts(), Config);
|
||||
init_per_group(Name = https, Config) ->
|
||||
cowboy_test:init_https(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [stream_handler_h]
|
||||
}, Config);
|
||||
cowboy_test:init_https(Name, init_plain_opts(), Config);
|
||||
init_per_group(Name = h2, Config) ->
|
||||
cowboy_test:init_http2(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [stream_handler_h]
|
||||
}, Config);
|
||||
cowboy_test:init_http2(Name, init_plain_opts(), Config);
|
||||
init_per_group(Name = h2c, Config) ->
|
||||
Config1 = cowboy_test:init_http(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [stream_handler_h]
|
||||
}, Config),
|
||||
Config1 = cowboy_test:init_http(Name, init_plain_opts(), Config),
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2});
|
||||
init_per_group(Name = h3, Config) ->
|
||||
cowboy_test:init_http3(Name, init_plain_opts(), Config);
|
||||
init_per_group(Name = http_compress, Config) ->
|
||||
cowboy_test:init_http(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [cowboy_compress_h, stream_handler_h]
|
||||
}, Config);
|
||||
cowboy_test:init_http(Name, init_compress_opts(), Config);
|
||||
init_per_group(Name = https_compress, Config) ->
|
||||
cowboy_test:init_https(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [cowboy_compress_h, stream_handler_h]
|
||||
}, Config);
|
||||
cowboy_test:init_https(Name, init_compress_opts(), Config);
|
||||
init_per_group(Name = h2_compress, Config) ->
|
||||
cowboy_test:init_http2(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [cowboy_compress_h, stream_handler_h]
|
||||
}, Config);
|
||||
cowboy_test:init_http2(Name, init_compress_opts(), Config);
|
||||
init_per_group(Name = h2c_compress, Config) ->
|
||||
Config1 = cowboy_test:init_http(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [cowboy_compress_h, stream_handler_h]
|
||||
}, Config),
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2}).
|
||||
Config1 = cowboy_test:init_http(Name, init_compress_opts(), Config),
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2});
|
||||
init_per_group(Name = h3_compress, Config) ->
|
||||
cowboy_test:init_http3(Name, init_compress_opts(), Config).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
init_plain_opts() ->
|
||||
#{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [stream_handler_h]
|
||||
}.
|
||||
|
||||
init_compress_opts() ->
|
||||
#{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [cowboy_compress_h, stream_handler_h]
|
||||
}.
|
||||
|
||||
%% Logger function silencing the expected crashes.
|
||||
|
||||
|
@ -99,15 +91,20 @@ crash_in_init(Config) ->
|
|||
%% Confirm terminate/3 is NOT called. We have no state to give to it.
|
||||
receive {Self, Pid, terminate, _, _, _} -> error(terminate) after 1000 -> ok end,
|
||||
%% Confirm early_error/5 is called in HTTP/1.1's case.
|
||||
%% HTTP/2 does not send a response back so there is no early_error call.
|
||||
%% HTTP/2 and HTTP/3 do not send a response back so there is no early_error call.
|
||||
case config(protocol, Config) of
|
||||
http -> receive {Self, Pid, early_error, _, _, _, _, _} -> ok after 1000 -> error(timeout) end;
|
||||
http2 -> ok
|
||||
http2 -> ok;
|
||||
http3 -> ok
|
||||
end,
|
||||
%% Receive a 500 error response.
|
||||
case gun:await(ConnPid, Ref) of
|
||||
{response, fin, 500, _} -> ok;
|
||||
{error, {stream_error, {stream_error, internal_error, _}}} -> ok
|
||||
do_await_internal_error(ConnPid, Ref, Config).
|
||||
|
||||
do_await_internal_error(ConnPid, Ref, Config) ->
|
||||
Protocol = config(protocol, Config),
|
||||
case {Protocol, gun:await(ConnPid, Ref)} of
|
||||
{http, {response, fin, 500, _}} -> ok;
|
||||
{http2, {error, {stream_error, {stream_error, internal_error, _}}}} -> ok;
|
||||
{http3, {error, {stream_error, {stream_error, h3_internal_error, _}}}} -> ok
|
||||
end.
|
||||
|
||||
crash_in_data(Config) ->
|
||||
|
@ -126,11 +123,7 @@ crash_in_data(Config) ->
|
|||
gun:data(ConnPid, Ref, fin, <<"Hello!">>),
|
||||
%% Confirm terminate/3 is called, indicating the stream ended.
|
||||
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
|
||||
%% Receive a 500 error response.
|
||||
case gun:await(ConnPid, Ref) of
|
||||
{response, fin, 500, _} -> ok;
|
||||
{error, {stream_error, {stream_error, internal_error, _}}} -> ok
|
||||
end.
|
||||
do_await_internal_error(ConnPid, Ref, Config).
|
||||
|
||||
crash_in_info(Config) ->
|
||||
doc("Confirm an error is sent when a stream handler crashes in info/3."),
|
||||
|
@ -144,14 +137,14 @@ crash_in_info(Config) ->
|
|||
%% Confirm init/3 is called.
|
||||
Pid = receive {Self, P, init, _, _, _} -> P after 1000 -> error(timeout) end,
|
||||
%% Send a message to make the stream handler crash.
|
||||
Pid ! {{Pid, 1}, crash},
|
||||
StreamID = case config(protocol, Config) of
|
||||
http3 -> 0;
|
||||
_ -> 1
|
||||
end,
|
||||
Pid ! {{Pid, StreamID}, crash},
|
||||
%% Confirm terminate/3 is called, indicating the stream ended.
|
||||
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
|
||||
%% Receive a 500 error response.
|
||||
case gun:await(ConnPid, Ref) of
|
||||
{response, fin, 500, _} -> ok;
|
||||
{error, {stream_error, {stream_error, internal_error, _}}} -> ok
|
||||
end.
|
||||
do_await_internal_error(ConnPid, Ref, Config).
|
||||
|
||||
crash_in_terminate(Config) ->
|
||||
doc("Confirm the state is correct when a stream handler crashes in terminate/3."),
|
||||
|
@ -185,10 +178,12 @@ crash_in_terminate(Config) ->
|
|||
{ok, <<"Hello world!">>} = gun:await_body(ConnPid, Ref2),
|
||||
ok.
|
||||
|
||||
%% @todo The callbacks ARE used for HTTP/2 and HTTP/3 CONNECT/TRACE requests.
|
||||
crash_in_early_error(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_crash_in_early_error(Config);
|
||||
http2 -> doc("The callback early_error/5 is not currently used for HTTP/2.")
|
||||
http2 -> doc("The callback early_error/5 is not currently used for HTTP/2.");
|
||||
http3 -> doc("The callback early_error/5 is not currently used for HTTP/3.")
|
||||
end.
|
||||
|
||||
do_crash_in_early_error(Config) ->
|
||||
|
@ -225,10 +220,12 @@ do_crash_in_early_error(Config) ->
|
|||
{response, fin, 500, _} = gun:await(ConnPid, Ref2),
|
||||
ok.
|
||||
|
||||
%% @todo The callbacks ARE used for HTTP/2 and HTTP/3 CONNECT/TRACE requests.
|
||||
crash_in_early_error_fatal(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_crash_in_early_error_fatal(Config);
|
||||
http2 -> doc("The callback early_error/5 is not currently used for HTTP/2.")
|
||||
http2 -> doc("The callback early_error/5 is not currently used for HTTP/2.");
|
||||
http3 -> doc("The callback early_error/5 is not currently used for HTTP/3.")
|
||||
end.
|
||||
|
||||
do_crash_in_early_error_fatal(Config) ->
|
||||
|
@ -262,7 +259,8 @@ early_error_stream_error_reason(Config) ->
|
|||
%% reason in both protocols.
|
||||
{Method, Headers, Status, Error} = case config(protocol, Config) of
|
||||
http -> {<<"GET">>, [{<<"host">>, <<"host:port">>}], 400, protocol_error};
|
||||
http2 -> {<<"TRACE">>, [], 501, no_error}
|
||||
http2 -> {<<"TRACE">>, [], 501, no_error};
|
||||
http3 -> {<<"TRACE">>, [], 501, h3_no_error}
|
||||
end,
|
||||
Ref = gun:request(ConnPid, Method, "/long_polling", [
|
||||
{<<"accept-encoding">>, <<"gzip">>},
|
||||
|
@ -355,11 +353,20 @@ shutdown_on_socket_close(Config) ->
|
|||
Spawn ! {Self, ready},
|
||||
%% Close the socket.
|
||||
ok = gun:close(ConnPid),
|
||||
%% Confirm terminate/3 is called, indicating the stream ended.
|
||||
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
|
||||
%% Confirm we receive a DOWN message for the child process.
|
||||
receive {'DOWN', MRef, process, Spawn, shutdown} -> ok after 1000 -> error(timeout) end,
|
||||
ok.
|
||||
Protocol = config(protocol, Config),
|
||||
try
|
||||
%% Confirm terminate/3 is called, indicating the stream ended.
|
||||
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
|
||||
%% Confirm we receive a DOWN message for the child process.
|
||||
receive {'DOWN', MRef, process, Spawn, shutdown} -> ok after 1000 -> error(timeout) end,
|
||||
ok
|
||||
catch error:timeout when Protocol =:= http3 ->
|
||||
%% @todo Figure out why this happens. Could be a timing issue
|
||||
%% or a legitimate bug. I suspect that the server just
|
||||
%% doesn't receive the GOAWAY frame from Gun because
|
||||
%% Gun is too quick to close the connection.
|
||||
shutdown_on_socket_close(Config)
|
||||
end.
|
||||
|
||||
shutdown_timeout_on_stream_stop(Config) ->
|
||||
doc("Confirm supervised processes are killed "
|
||||
|
@ -406,33 +413,45 @@ shutdown_timeout_on_socket_close(Config) ->
|
|||
Spawn ! {Self, ready},
|
||||
%% Close the socket.
|
||||
ok = gun:close(ConnPid),
|
||||
%% Confirm terminate/3 is called, indicating the stream ended.
|
||||
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
|
||||
%% We should NOT receive a DOWN message immediately.
|
||||
receive {'DOWN', MRef, process, Spawn, killed} -> error(killed) after 1500 -> ok end,
|
||||
%% We should receive it now.
|
||||
receive {'DOWN', MRef, process, Spawn, killed} -> ok after 1000 -> error(timeout) end,
|
||||
ok.
|
||||
Protocol = config(protocol, Config),
|
||||
try
|
||||
%% Confirm terminate/3 is called, indicating the stream ended.
|
||||
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
|
||||
%% We should NOT receive a DOWN message immediately.
|
||||
receive {'DOWN', MRef, process, Spawn, killed} -> error(killed) after 1500 -> ok end,
|
||||
%% We should receive it now.
|
||||
receive {'DOWN', MRef, process, Spawn, killed} -> ok after 1000 -> error(timeout) end,
|
||||
ok
|
||||
catch error:timeout when Protocol =:= http3 ->
|
||||
%% @todo Figure out why this happens. Could be a timing issue
|
||||
%% or a legitimate bug. I suspect that the server just
|
||||
%% doesn't receive the GOAWAY frame from Gun because
|
||||
%% Gun is too quick to close the connection.
|
||||
shutdown_timeout_on_socket_close(Config)
|
||||
end.
|
||||
|
||||
switch_protocol_after_headers(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_switch_protocol_after_response(
|
||||
<<"switch_protocol_after_headers">>, Config);
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.")
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.");
|
||||
http3 -> doc("The switch_protocol command is not currently supported for HTTP/3.")
|
||||
end.
|
||||
|
||||
switch_protocol_after_headers_data(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_switch_protocol_after_response(
|
||||
<<"switch_protocol_after_headers_data">>, Config);
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.")
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.");
|
||||
http3 -> doc("The switch_protocol command is not currently supported for HTTP/3.")
|
||||
end.
|
||||
|
||||
switch_protocol_after_response(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_switch_protocol_after_response(
|
||||
<<"switch_protocol_after_response">>, Config);
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.")
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.");
|
||||
http3 -> doc("The switch_protocol command is not currently supported for HTTP/3.")
|
||||
end.
|
||||
|
||||
do_switch_protocol_after_response(TestCase, Config) ->
|
||||
|
@ -502,7 +521,12 @@ terminate_on_stop(Config) ->
|
|||
{response, fin, 204, _} = gun:await(ConnPid, Ref),
|
||||
%% Confirm the stream is still alive even though we
|
||||
%% received the response fully, and tell it to stop.
|
||||
Pid ! {{Pid, 1}, please_stop},
|
||||
StreamID = case config(protocol, Config) of
|
||||
http -> 1;
|
||||
http2 -> 1;
|
||||
http3 -> 0
|
||||
end,
|
||||
Pid ! {{Pid, StreamID}, please_stop},
|
||||
receive {Self, Pid, info, _, please_stop, _} -> ok after 1000 -> error(timeout) end,
|
||||
%% Confirm terminate/3 is called.
|
||||
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
|
||||
|
@ -511,7 +535,8 @@ terminate_on_stop(Config) ->
|
|||
terminate_on_switch_protocol(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_terminate_on_switch_protocol(Config);
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.")
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.");
|
||||
http3 -> doc("The switch_protocol command is not currently supported for HTTP/3.")
|
||||
end.
|
||||
|
||||
do_terminate_on_switch_protocol(Config) ->
|
||||
|
|
|
@ -29,7 +29,8 @@ suite() ->
|
|||
%% We initialize trace patterns here. Appropriate would be in
|
||||
%% init_per_suite/1, but this works just as well.
|
||||
all() ->
|
||||
cowboy_test:common_all().
|
||||
%% @todo Implement these tests for HTTP/3.
|
||||
cowboy_test:common_all() -- [{group, h3}, {group, h3_compress}].
|
||||
|
||||
init_per_suite(Config) ->
|
||||
cowboy_tracer_h:set_trace_patterns(),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue