0
Fork 0
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:
Loïc Hoguin 2023-01-31 11:07:31 +01:00
parent 3ea8395eb8
commit 8cb9d242b0
No known key found for this signature in database
GPG key ID: 8A9DF795F6FED764
39 changed files with 5130 additions and 238 deletions

View file

@ -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.

View file

@ -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,

View file

@ -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).

View file

@ -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),

View file

@ -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.

View file

@ -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.

View file

@ -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", [

View file

@ -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) ->

View file

@ -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) ->

View file

@ -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) ->

View file

@ -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([{"[...]", [

View file

@ -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) ->

View file

@ -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([{"[...]", [

View file

@ -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 "

View file

@ -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([{"[...]", [

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgVJakPYfQA1Hr6Gnq
GYmpMfXpxUi2QwDBrZfw8dBcVqKhRANCAAQDHeeAvjwD7p+Mg1F+G9FBNy+7Wcms
HEw4sGMzhUL4wjwsqKHpoiuQg3qUXXK0gamx0l77vFjrUc6X1al4+ZM5
-----END PRIVATE KEY-----

View 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-----

View file

@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgvykUYMOS2gW8XTTh
HgmeJM36NT8GGTNXzzt4sIs0o9ahRANCAATnQOMkKbLFQCZY/cxf8otEJG2tVuG6
QvLqUdERV2+gzE+4ROGDqbb2Jk1szyz4CfBMB4ZfLA/PdSiO+KrOeOcj
-----END PRIVATE KEY-----

View 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
View 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
View 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

View file

@ -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.

View file

@ -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;

View file

@ -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) ->

View file

@ -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(),