aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2026-03-30 15:05:50 +0200
committerLoïc Hoguin <[email protected]>2026-03-31 15:16:42 +0200
commit2d6c2f7e3280b220a1fa5fe95482399c1d5954f8 (patch)
treed1c94d68accf6289916520565a47bd29e84e959c
parent5399a7dd365c8968f0782c5fbad6dd9ebf73f4ef (diff)
downloadcowboy-corral.tar.gz
cowboy-corral.tar.bz2
cowboy-corral.zip
-rw-r--r--.github/workflows/ci.yaml6
-rw-r--r--Makefile12
-rw-r--r--ebin/cowboy.app6
-rw-r--r--src/cowboy.erl153
-rw-r--r--src/cowboy_http3.erl172
-rw-r--r--src/cowboy_quic.erl38
-rw-r--r--src/cowboy_quicer.erl247
-rw-r--r--test/cowboy_test.erl26
-rw-r--r--test/decompress_SUITE.erl2
-rw-r--r--test/draft_h3_webtransport_SUITE.erl2
-rw-r--r--test/rfc9114_SUITE.erl2
-rw-r--r--test/rfc9204_SUITE.erl2
-rw-r--r--test/rfc9220_SUITE.erl124
13 files changed, 294 insertions, 498 deletions
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 7279af0..f3a8e4d 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -22,7 +22,7 @@ jobs:
uses: ninenines/ci.erlang.mk/.github/workflows/ci.yaml@master
dialyzer-no-quicer:
- name: Check / Dialyzer (without COWBOY_QUICER)
+ name: Check / Dialyzer (without CORRAL)
runs-on: ubuntu-latest
steps:
@@ -34,8 +34,8 @@ jobs:
with:
otp-version: '> 0'
- - name: Run Dialyzer (without COWBOY_QUICER)
- run: make dialyze COWBOY_QUICER=0
+ - name: Run Dialyzer (without CORRAL)
+ run: make dialyze CORRAL_DEPS=
examples:
name: Check examples
diff --git a/Makefile b/Makefile
index e163148..4ef7093 100644
--- a/Makefile
+++ b/Makefile
@@ -19,9 +19,9 @@ DEPS = cowlib ranch
dep_cowlib = git https://github.com/ninenines/cowlib 2.16.0
dep_ranch = git https://github.com/ninenines/ranch 1.8.1
-ifeq ($(COWBOY_QUICER),1)
-DEPS += quicer
-dep_quicer = git https://github.com/emqx/quic main
+ifdef CORRAL_DEPS
+DEPS += corral
+dep_corral = git https://github.com/ninenines/corral master
endif
DOC_DEPS = asciideck
@@ -76,9 +76,9 @@ endif
ERLC_OPTS += +warn_missing_spec +warn_untyped_record # +bin_opt_info
TEST_ERLC_OPTS += +'{parse_transform, eunit_autoexport}'
-ifeq ($(COWBOY_QUICER),1)
-ERLC_OPTS += -D COWBOY_QUICER=1
-TEST_ERLC_OPTS += -D COWBOY_QUICER=1
+ifdef CORRAL_DEPS
+ERLC_OPTS += -D CORRAL=1
+TEST_ERLC_OPTS += -D CORRAL=1
endif
# Generate rebar.config on build.
diff --git a/ebin/cowboy.app b/ebin/cowboy.app
index 002727e..aad64cd 100644
--- a/ebin/cowboy.app
+++ b/ebin/cowboy.app
@@ -1,10 +1,10 @@
{application, 'cowboy', [
{description, "Small, fast, modern HTTP server."},
{vsn, "2.14.2"},
- {modules, ['cowboy','cowboy_app','cowboy_bstr','cowboy_children','cowboy_clear','cowboy_clock','cowboy_compress_h','cowboy_constraints','cowboy_decompress_h','cowboy_handler','cowboy_http','cowboy_http2','cowboy_http3','cowboy_loop','cowboy_metrics_h','cowboy_middleware','cowboy_quicer','cowboy_req','cowboy_rest','cowboy_router','cowboy_static','cowboy_stream','cowboy_stream_h','cowboy_sub_protocol','cowboy_sup','cowboy_tls','cowboy_tracer_h','cowboy_websocket','cowboy_webtransport']},
+ {modules, ['cowboy','cowboy_app','cowboy_bstr','cowboy_children','cowboy_clear','cowboy_clock','cowboy_compress_h','cowboy_constraints','cowboy_decompress_h','cowboy_handler','cowboy_http','cowboy_http2','cowboy_http3','cowboy_loop','cowboy_metrics_h','cowboy_middleware','cowboy_quic','cowboy_req','cowboy_rest','cowboy_router','cowboy_static','cowboy_stream','cowboy_stream_h','cowboy_sub_protocol','cowboy_sup','cowboy_tls','cowboy_tracer_h','cowboy_websocket','cowboy_webtransport']},
{registered, [cowboy_sup,cowboy_clock]},
- {applications, [kernel,stdlib,crypto,cowlib,ranch]},
+ {applications, [kernel,stdlib,crypto,cowlib,ranch,corral]},
{optional_applications, []},
- {mod, {cowboy_app, []}},
+ {mod, {'cowboy_app', []}},
{env, []}
]}. \ No newline at end of file
diff --git a/src/cowboy.erl b/src/cowboy.erl
index f148a39..304a39a 100644
--- a/src/cowboy.erl
+++ b/src/cowboy.erl
@@ -21,11 +21,8 @@
-export([get_env/3]).
-export([set_env/3]).
--ifdef(COWBOY_QUICER).
+-ifdef(CORRAL).
-export([start_quic/3]).
-
-%% Don't warn about the bad quicer specs.
--dialyzer([{nowarn_function, start_quic/3}]).
-endif.
%% Internal.
@@ -76,94 +73,84 @@ start_tls(Ref, TransOpts0, ProtoOpts0) ->
},
ranch:start_listener(Ref, ranch_ssl, TransOpts, cowboy_tls, ProtoOpts).
--ifdef(COWBOY_QUICER).
+ensure_alpn(TransOpts) ->
+ SocketOpts = maps:get(socket_opts, TransOpts, []),
+ TransOpts#{socket_opts => [
+ {alpn_preferred_protocols, [<<"h2">>, <<"http/1.1">>]}
+ |SocketOpts]}.
+
+-ifdef(CORRAL).
%% @todo Experimental function to start a barebone QUIC listener.
-%% This will need to be reworked to be closer to Ranch
-%% listeners and provide equivalent features.
-%%
-%% @todo Better type for transport options. Might require fixing quicer types.
--spec start_quic(ranch:ref(), #{socket_opts => [{atom(), _}]}, cowboy_http3:opts())
+-spec start_quic(corral:ref(), corral:quic_server_opts(), cowboy_http3:opts())
-> {ok, pid()}.
%% @todo Implement dynamic_buffer for HTTP/3 if/when it applies.
-start_quic(Ref, TransOpts, ProtoOpts) ->
- {ok, _} = application:ensure_all_started(quicer),
- Parent = self(),
- SocketOpts0 = maps:get(socket_opts, TransOpts, []),
- {Port, SocketOpts2} = case lists:keytake(port, 1, SocketOpts0) of
- {value, {port, Port0}, SocketOpts1} ->
- {Port0, SocketOpts1};
- false ->
- {port_0(), SocketOpts0}
- end,
- SocketOpts = [
- {alpn, ["h3"]}, %% @todo Why not binary?
- %% We only need 3 for control and QPACK enc/dec,
- %% but we need more for WebTransport. %% @todo Use 3 if WT is disabled.
- {peer_unidi_stream_count, 100},
- {peer_bidi_stream_count, 100},
- %% For WebTransport.
- %% @todo We probably don't want it enabled if WT isn't used.
- {datagram_send_enabled, 1},
- {datagram_receive_enabled, 1}
- |SocketOpts2],
- _ListenerPid = spawn(fun() ->
- {ok, Listener} = quicer:listen(Port, SocketOpts),
- Parent ! {ok, Listener},
- _AcceptorPid = [spawn(fun AcceptLoop() ->
- {ok, Conn} = quicer:accept(Listener, []),
- Pid = spawn(fun() ->
- receive go -> ok end,
- %% We have to do the handshake after handing control of
- %% the connection otherwise streams may come in before
- %% the controlling process is changed and messages will
- %% not be sent to the correct process.
- {ok, Conn} = quicer:handshake(Conn),
- process_flag(trap_exit, true), %% @todo Only if supervisor though.
- try cowboy_http3:init(Parent, Ref, Conn, ProtoOpts)
- catch
- exit:{shutdown,_} -> ok;
- C:E:S -> log(error, "CRASH ~p:~p:~p", [C,E,S], ProtoOpts)
- end
- end),
- ok = quicer:controlling_process(Conn, Pid),
- Pid ! go,
- AcceptLoop()
- end) || _ <- lists:seq(1, 20)],
- %% Listener process must not terminate.
- receive after infinity -> ok end
- end),
- receive
- {ok, Listener} ->
- {ok, Listener}
- end.
-
-%% Select a random UDP port using gen_udp because quicer
-%% does not provide equivalent functionality. Taken from
-%% quicer test suites.
-port_0() ->
- {ok, Socket} = gen_udp:open(0, [{reuseaddr, true}]),
- {ok, {_, Port}} = inet:sockname(Socket),
- gen_udp:close(Socket),
- case os:type() of
- {unix, darwin} ->
- %% Apparently macOS doesn't free the port immediately.
- timer:sleep(500);
- _ ->
- ok
- end,
- Port.
+start_quic(Ref, QuicOpts0, ProtoOpts0) ->
+ {ok, _} = application:ensure_all_started(corral),
+ {QuicOpts1, ConnectionType} = ensure_connection_type(QuicOpts0),
+ QuicOpts = QuicOpts1#{
+ alpn => [<<"h3">>],
+ backend => corral_quicer
+ },
+ ProtoOpts = ProtoOpts0#{
+ connection_type => ConnectionType
+ },
+ corral:start_listener(Ref, QuicOpts, cowboy_quic, ProtoOpts).
+
+
+% SocketOpts0 = maps:get(socket_opts, TransOpts, []),
+% {Port, SocketOpts2} = case lists:keytake(port, 1, SocketOpts0) of
+% {value, {port, Port0}, SocketOpts1} ->
+% {Port0, SocketOpts1};
+% false ->
+% {port_0(), SocketOpts0}
+% end,
+% SocketOpts = [
+% {alpn, ["h3"]}, %% @todo Why not binary?
+% %% We only need 3 for control and QPACK enc/dec,
+% %% but we need more for WebTransport. %% @todo Use 3 if WT is disabled.
+% {peer_unidi_stream_count, 100},
+% {peer_bidi_stream_count, 100},
+% %% For WebTransport.
+% %% @todo We probably don't want it enabled if WT isn't used.
+% {datagram_send_enabled, 1},
+% {datagram_receive_enabled, 1}
+% |SocketOpts2],
+% _ListenerPid = spawn(fun() ->
+% {ok, Listener} = quicer:listen(Port, SocketOpts),
+% Parent ! {ok, Listener},
+% _AcceptorPid = [spawn(fun AcceptLoop() ->
+% {ok, Conn} = quicer:accept(Listener, []),
+% Pid = spawn(fun() ->
+% receive go -> ok end,
+% %% We have to do the handshake after handing control of
+% %% the connection otherwise streams may come in before
+% %% the controlling process is changed and messages will
+% %% not be sent to the correct process.
+% {ok, Conn} = quicer:handshake(Conn),
+% process_flag(trap_exit, true), %% @todo Only if supervisor though.
+% try cowboy_http3:init(Parent, Ref, Conn, ProtoOpts)
+% catch
+% exit:{shutdown,_} -> ok;
+% C:E:S -> log(error, "CRASH ~p:~p:~p", [C,E,S], ProtoOpts)
+% end
+% end),
+% ok = quicer:controlling_process(Conn, Pid),
+% Pid ! go,
+% AcceptLoop()
+% end) || _ <- lists:seq(1, 20)],
+% %% Listener process must not terminate.
+% receive after infinity -> ok end
+% end),
+% receive
+% {ok, Listener} ->
+% {ok, Listener}
+% end.
-endif.
-ensure_alpn(TransOpts) ->
- SocketOpts = maps:get(socket_opts, TransOpts, []),
- TransOpts#{socket_opts => [
- {alpn_preferred_protocols, [<<"h2">>, <<"http/1.1">>]}
- |SocketOpts]}.
-
ensure_connection_type(TransOpts=#{connection_type := ConnectionType}) ->
{TransOpts, ConnectionType};
ensure_connection_type(TransOpts) ->
diff --git a/src/cowboy_http3.erl b/src/cowboy_http3.erl
index 4f407cd..72b9598 100644
--- a/src/cowboy_http3.erl
+++ b/src/cowboy_http3.erl
@@ -19,9 +19,9 @@
-module(cowboy_http3).
--ifdef(COWBOY_QUICER).
+-ifdef(CORRAL).
--export([init/4]).
+-export([init/5]).
%% Temporary callback to do sendfile over QUIC.
-export([send/2]).
@@ -83,7 +83,8 @@
-record(state, {
parent :: pid(),
ref :: ranch:ref(),
- conn :: cowboy_quicer:quicer_connection_handle(),
+ backend :: module(),
+ conn :: corral_backend:conn(),
opts = #{} :: opts(),
%% Remote address and port for the connection.
@@ -117,32 +118,32 @@
children = cowboy_children:init() :: cowboy_children:children()
}).
--spec init(pid(), ranch:ref(), cowboy_quicer:quicer_connection_handle(), opts())
+-spec init(pid(), ranch:ref(), module(), corral_backend:conn(), opts())
-> no_return().
-init(Parent, Ref, Conn, Opts) ->
+init(Parent, Ref, QuicBackend, Conn, Opts) ->
{ok, SettingsBin, HTTP3Machine0} = cow_http3_machine:init(server, Opts),
%% Immediately open a control, encoder and decoder stream.
%% @todo 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).
%% @todo An endpoint MAY avoid creating a decoder stream if its decoder sets the maximum capacity of the dynamic table to zero.
{ok, ControlID} = maybe_socket_error(undefined,
- cowboy_quicer:start_unidi_stream(Conn, [<<0>>, SettingsBin]),
+ QuicBackend:open_unidi_stream(Conn, [<<0>>, SettingsBin]),
'A socket error occurred when opening the control stream.'),
{ok, EncoderID} = maybe_socket_error(undefined,
- cowboy_quicer:start_unidi_stream(Conn, <<2>>),
+ QuicBackend:open_unidi_stream(Conn, <<2>>),
'A socket error occurred when opening the encoder stream.'),
{ok, DecoderID} = maybe_socket_error(undefined,
- cowboy_quicer:start_unidi_stream(Conn, <<3>>),
+ QuicBackend:open_unidi_stream(Conn, <<3>>),
'A socket error occurred when opening the encoder stream.'),
%% Set the control, encoder and decoder streams in the machine.
HTTP3Machine = cow_http3_machine:init_unidi_local_streams(
ControlID, EncoderID, DecoderID, HTTP3Machine0),
%% Get the peername/sockname/cert.
- {ok, Peer} = maybe_socket_error(undefined, cowboy_quicer:peername(Conn),
+ {ok, Peer} = maybe_socket_error(undefined, QuicBackend:peername(Conn),
'A socket error occurred when retrieving the peer name.'),
- {ok, Sock} = maybe_socket_error(undefined, cowboy_quicer:sockname(Conn),
+ {ok, Sock} = maybe_socket_error(undefined, QuicBackend:sockname(Conn),
'A socket error occurred when retrieving the sock name.'),
- CertResult = case cowboy_quicer:peercert(Conn) of
+ CertResult = case QuicBackend:peercert(Conn) of
{error, no_peercert} ->
{ok, undefined};
Cert0 ->
@@ -151,7 +152,7 @@ init(Parent, Ref, Conn, Opts) ->
{ok, Cert} = maybe_socket_error(undefined, CertResult,
'A socket error occurred when retrieving the client TLS certificate.'),
%% Quick! Let's go!
- loop(#state{parent=Parent, ref=Ref, conn=Conn,
+ loop(#state{parent=Parent, ref=Ref, backend=QuicBackend, conn=Conn,
opts=Opts, peer=Peer, sock=Sock, cert=Cert,
http3_machine=HTTP3Machine, local_control_id=ControlID,
local_encoder_id=EncoderID, local_decoder_id=DecoderID}).
@@ -180,31 +181,38 @@ loop(State0=#state{opts=Opts, children=Children}) ->
loop(State0)
end.
-handle_quic_msg(State0=#state{opts=Opts}, Msg) ->
- case cowboy_quicer:handle(Msg) of
- {data, StreamID, IsFin, Data} ->
+handle_quic_msg(State0=#state{backend=QuicBackend, opts=Opts}, Msg) ->
+ case QuicBackend:make_event(Msg) of
+ {stream_data, StreamID, IsFin, Data} ->
parse(State0, StreamID, Data, IsFin);
{datagram, Data} ->
parse_datagram(State0, Data);
- {stream_started, StreamID, StreamType} ->
+ {stream_opened, StreamID, StreamType} ->
State = stream_new_remote(State0, StreamID, StreamType),
loop(State);
- {stream_closed, StreamID, ErrorCode} ->
- State = stream_closed(State0, StreamID, ErrorCode),
- loop(State);
- {peer_send_shutdown, StreamID} ->
+ {stream_reset, StreamID, _ErrorCode} ->
+ %% @todo Properly handle half-closed.
+ %% @todo Rename this function.
State = stream_peer_send_shutdown(State0, StreamID),
loop(State);
- closed ->
+ {stream_stop_sending, StreamID, ErrorCode} ->
+ %% @todo Properly handle half-closed.
+ State = stream_closed(State0, StreamID, ErrorCode),
+ loop(State);
+ {conn_closed, transport, _QuicErrno} ->
%% @todo Different error reason if graceful?
Reason = {socket_error, closed, 'The socket has been closed.'},
terminate(State0, Reason);
- ok ->
+ {conn_closed, application, _AppErrno} ->
+ %% @todo Different error reason if application shutdown?
+ Reason = {socket_error, closed, 'The socket has been closed.'},
+ terminate(State0, Reason);
+ no_event ->
loop(State0);
- unknown ->
+ unknown_msg ->
cowboy:log(warning, "Received unknown QUIC message ~p.", [Msg], Opts),
loop(State0);
- {socket_error, Reason} ->
+ {error, Reason} ->
terminate(State0, {socket_error, Reason,
'An error has occurred on the socket.'})
end.
@@ -242,18 +250,19 @@ parse1(State=#state{http3_machine=HTTP3Machine0},
terminate(State#state{http3_machine=HTTP3Machine}, Error)
end;
%% @todo Handle when IsFin = fin which must terminate the WT session.
-parse1(State=#state{conn=Conn}, Stream=#stream{id=SessionID, status=
+parse1(State=#state{backend=QuicBackend, conn=Conn}, Stream=#stream{id=SessionID, status=
{webtransport_session, normal}}, Data, IsFin) ->
case cow_capsule:parse(Data) of
{ok, wt_drain_session, Rest} ->
webtransport_event(State, SessionID, close_initiated),
parse1(State, Stream, Rest, IsFin);
+ %% @todo Rename AppCode to AppErrno throughout.
{ok, {wt_close_session, AppCode, AppMsg}, Rest} ->
%% This event will be handled specially and lead
%% to the termination of the session process.
webtransport_event(State, SessionID, {closed, AppCode, AppMsg}),
%% Shutdown the CONNECT stream immediately.
- cowboy_quicer:shutdown_stream(Conn, SessionID),
+ QuicBackend:send(Conn, SessionID, fin, <<>>),
%% @todo Will we receive a {stream_closed,...} after that?
%% If any data is received past that point this is an error.
%% @todo Don't crash, error out properly.
@@ -688,33 +697,34 @@ commands(State0, Stream, [{headers, StatusCode, Headers}|Tail]) ->
State = send_headers(State0, Stream, nofin, StatusCode, Headers),
commands(State, Stream, Tail);
%%% Send a response body chunk.
-commands(State0=#state{conn=Conn}, Stream=#stream{id=StreamID}, [{data, IsFin, Data}|Tail]) ->
+commands(State0=#state{backend=QuicBackend, conn=Conn},
+ Stream=#stream{id=StreamID}, [{data, IsFin, Data}|Tail]) ->
_ = case Data of
{sendfile, Offset, Bytes, Path} ->
%% Temporary solution to do sendfile over QUIC.
- {ok, _} = ranch_transport:sendfile(?MODULE, {Conn, StreamID},
+ {ok, _} = ranch_transport:sendfile(?MODULE, {QuicBackend, Conn, StreamID},
Path, Offset, Bytes, []),
ok = maybe_socket_error(State0,
- cowboy_quicer:send(Conn, StreamID, cow_http3:data(<<>>), IsFin));
+ QuicBackend:send(Conn, StreamID, IsFin, cow_http3:data(<<>>)));
_ ->
ok = maybe_socket_error(State0,
- cowboy_quicer:send(Conn, StreamID, cow_http3:data(Data), IsFin))
+ QuicBackend:send(Conn, StreamID, IsFin, cow_http3:data(Data)))
end,
State = maybe_send_is_fin(State0, Stream, IsFin),
commands(State, Stream, Tail);
%%% Send trailers.
-commands(State0=#state{conn=Conn, http3_machine=HTTP3Machine0},
+commands(State0=#state{backend=QuicBackend, conn=Conn, http3_machine=HTTP3Machine0},
Stream=#stream{id=StreamID}, [{trailers, Trailers}|Tail]) ->
State = case cow_http3_machine:prepare_trailers(
StreamID, HTTP3Machine0, maps:to_list(Trailers)) of
{trailers, HeaderBlock, Instrs, HTTP3Machine} ->
State1 = send_instructions(State0#state{http3_machine=HTTP3Machine}, Instrs),
ok = maybe_socket_error(State1,
- cowboy_quicer:send(Conn, StreamID, cow_http3:headers(HeaderBlock), fin)),
+ QuicBackend:send(Conn, StreamID, fin, cow_http3:headers(HeaderBlock))),
State1;
{no_trailers, HTTP3Machine} ->
ok = maybe_socket_error(State0,
- cowboy_quicer:send(Conn, StreamID, cow_http3:data(<<>>), fin)),
+ QuicBackend:send(Conn, StreamID, fin, cow_http3:data(<<>>))),
State0#state{http3_machine=HTTP3Machine}
end,
commands(State, Stream, Tail);
@@ -824,7 +834,7 @@ commands(State=#state{opts=Opts}, Stream, [Log={log, _, _, _}|Tail]) ->
cowboy:log(Log, Opts),
commands(State, Stream, Tail).
-send_response(State0=#state{conn=Conn, http3_machine=HTTP3Machine0},
+send_response(State0=#state{backend=QuicBackend, conn=Conn, http3_machine=HTTP3Machine0},
Stream=#stream{id=StreamID}, StatusCode, Headers, Body) ->
Size = case Body of
{sendfile, _, Bytes0, _} -> Bytes0;
@@ -846,19 +856,19 @@ send_response(State0=#state{conn=Conn, http3_machine=HTTP3Machine0},
_ = case Body of
{sendfile, Offset, Bytes, Path} ->
ok = maybe_socket_error(State,
- cowboy_quicer:send(Conn, StreamID, cow_http3:headers(HeaderBlock))),
+ QuicBackend:send(Conn, StreamID, cow_http3:headers(HeaderBlock))),
%% Temporary solution to do sendfile over QUIC.
{ok, _} = maybe_socket_error(State,
- ranch_transport:sendfile(?MODULE, {Conn, StreamID},
+ ranch_transport:sendfile(?MODULE, {QuicBackend, Conn, StreamID},
Path, Offset, Bytes, [])),
ok = maybe_socket_error(State,
- cowboy_quicer:send(Conn, StreamID, cow_http3:data(<<>>), fin));
+ QuicBackend:send(Conn, StreamID, fin, cow_http3:data(<<>>)));
_ ->
ok = maybe_socket_error(State,
- cowboy_quicer:send(Conn, StreamID, [
+ QuicBackend:send(Conn, StreamID, fin, [
cow_http3:headers(HeaderBlock),
cow_http3:data(Body)
- ], fin))
+ ]))
end,
maybe_send_is_fin(State, Stream, fin)
end.
@@ -871,13 +881,13 @@ maybe_send_is_fin(State, _, _) ->
State.
%% Temporary callback to do sendfile over QUIC.
--spec send({cowboy_quicer:quicer_connection_handle(), cow_http3:stream_id()},
+-spec send({module(), corral_backend:conn(), cow_http3:stream_id()},
iodata()) -> ok | {error, any()}.
-send({Conn, StreamID}, IoData) ->
- cowboy_quicer:send(Conn, StreamID, cow_http3:data(IoData)).
+send({QuicBackend, Conn, StreamID}, IoData) ->
+ QuicBackend:send(Conn, StreamID, cow_http3:data(IoData)).
-send_headers(State0=#state{conn=Conn, http3_machine=HTTP3Machine0},
+send_headers(State0=#state{backend=QuicBackend, conn=Conn, http3_machine=HTTP3Machine0},
#stream{id=StreamID}, IsFin0, StatusCode, Headers) ->
{ok, IsFin, HeaderBlock, Instrs, HTTP3Machine}
= cow_http3_machine:prepare_headers(StreamID, HTTP3Machine0, IsFin0,
@@ -885,7 +895,7 @@ send_headers(State0=#state{conn=Conn, http3_machine=HTTP3Machine0},
headers_to_list(Headers)),
State = send_instructions(State0#state{http3_machine=HTTP3Machine}, Instrs),
ok = maybe_socket_error(State,
- cowboy_quicer:send(Conn, StreamID, cow_http3:headers(HeaderBlock), IsFin)),
+ QuicBackend:send(Conn, StreamID, IsFin, cow_http3:headers(HeaderBlock))),
State.
%% The set-cookie header is special; we can only send one cookie per header.
@@ -900,16 +910,16 @@ headers_to_list(Headers) ->
send_instructions(State, undefined) ->
State;
%% Decoder instructions.
-send_instructions(State=#state{conn=Conn, local_decoder_id=DecoderID},
+send_instructions(State=#state{backend=QuicBackend, conn=Conn, local_decoder_id=DecoderID},
{decoder_instructions, DecData}) ->
ok = maybe_socket_error(State,
- cowboy_quicer:send(Conn, DecoderID, DecData)),
+ QuicBackend:send(Conn, DecoderID, DecData)),
State;
%% Encoder instructions.
-send_instructions(State=#state{conn=Conn, local_encoder_id=EncoderID},
+send_instructions(State=#state{backend=QuicBackend, conn=Conn, local_encoder_id=EncoderID},
{encoder_instructions, EncData}) ->
ok = maybe_socket_error(State,
- cowboy_quicer:send(Conn, EncoderID, EncData)),
+ QuicBackend:send(Conn, EncoderID, EncData)),
State.
%% Relay data delivery commands.
@@ -917,13 +927,15 @@ send_instructions(State=#state{conn=Conn, local_encoder_id=EncoderID},
relay_command(State, StreamID, DataCmd = {data, _, _}) ->
Stream = stream_get(State, StreamID),
commands(State, Stream, [DataCmd]);
-relay_command(State=#state{conn=Conn}, StreamID, active) ->
+%% @todo This part will not work as the setopt function isn't available.
+%% @todo We may want to optionally make it available if the underlying backend supports it.
+relay_command(State=#state{backend=QuicBackend, conn=Conn}, StreamID, active) ->
ok = maybe_socket_error(State,
- cowboy_quicer:setopt(Conn, StreamID, active, true)),
+ QuicBackend:setopt(Conn, StreamID, active, true)),
State;
-relay_command(State=#state{conn=Conn}, StreamID, passive) ->
+relay_command(State=#state{backend=QuicBackend, conn=Conn}, StreamID, passive) ->
ok = maybe_socket_error(State,
- cowboy_quicer:setopt(Conn, StreamID, active, false)),
+ QuicBackend:setopt(Conn, StreamID, active, false)),
State.
%% We mark the stream as being a WebTransport stream
@@ -964,17 +976,17 @@ webtransport_commands(State, SessionID, Commands) ->
wt_commands(State, _, []) ->
State;
-wt_commands(State0=#state{conn=Conn}, Session=#stream{id=SessionID},
+wt_commands(State0=#state{backend=QuicBackend, conn=Conn}, Session=#stream{id=SessionID},
[{open_stream, OpenStreamRef, StreamType, InitialData}|Tail]) ->
%% Because opening the stream involves sending a short header
%% we necessarily write data. The InitialData variable allows
%% providing additional data to be sent in the same packet.
- StartF = case StreamType of
- bidi -> start_bidi_stream;
- unidi -> start_unidi_stream
+ OpenF = case StreamType of
+ bidi -> open_bidi_stream;
+ unidi -> open_unidi_stream
end,
Header = cow_http3:webtransport_stream_header(SessionID, StreamType),
- case cowboy_quicer:StartF(Conn, [Header, InitialData]) of
+ case QuicBackend:OpenF(Conn, [Header, InitialData]) of
{ok, StreamID} ->
%% @todo Pass Session directly?
webtransport_event(State0, SessionID,
@@ -987,36 +999,36 @@ wt_commands(State0=#state{conn=Conn}, Session=#stream{id=SessionID},
wt_commands(State, Session, [{close_stream, StreamID, Code}|Tail]) ->
%% @todo Check that StreamID belongs to Session.
error({todo, State, Session, [{close_stream, StreamID, Code}|Tail]});
-wt_commands(State=#state{conn=Conn}, Session=#stream{id=SessionID},
+wt_commands(State=#state{backend=QuicBackend, conn=Conn}, Session=#stream{id=SessionID},
[{send, datagram, Data}|Tail]) ->
- case cowboy_quicer:send_datagram(Conn, cow_http3:datagram(SessionID, Data)) of
+ case QuicBackend:send_datagram(Conn, cow_http3:datagram(SessionID, Data)) of
ok ->
wt_commands(State, Session, Tail)
%% @todo Handle errors.
end;
-wt_commands(State=#state{conn=Conn}, Session, [{send, StreamID, Data}|Tail]) ->
+wt_commands(State=#state{backend=QuicBackend, conn=Conn}, Session, [{send, StreamID, Data}|Tail]) ->
%% @todo Check that StreamID belongs to Session.
- case cowboy_quicer:send(Conn, StreamID, Data, nofin) of
+ case QuicBackend:send(Conn, StreamID, Data) of
ok ->
wt_commands(State, Session, Tail)
%% @todo Handle errors.
end;
-wt_commands(State=#state{conn=Conn}, Session, [{send, StreamID, IsFin, Data}|Tail]) ->
+wt_commands(State=#state{backend=QuicBackend, conn=Conn}, Session, [{send, StreamID, IsFin, Data}|Tail]) ->
%% @todo Check that StreamID belongs to Session.
- case cowboy_quicer:send(Conn, StreamID, Data, IsFin) of
+ case QuicBackend:send(Conn, StreamID, IsFin, Data) of
ok ->
wt_commands(State, Session, Tail)
%% @todo Handle errors.
end;
-wt_commands(State=#state{conn=Conn}, Session=#stream{id=SessionID}, [initiate_close|Tail]) ->
+wt_commands(State=#state{backend=QuicBackend, conn=Conn}, Session=#stream{id=SessionID}, [initiate_close|Tail]) ->
%% We must send a WT_DRAIN_SESSION capsule on the CONNECT stream.
Capsule = cow_capsule:wt_drain_session(),
- case cowboy_quicer:send(Conn, SessionID, Capsule, nofin) of
+ case QuicBackend:send(Conn, SessionID, Capsule) of
ok ->
wt_commands(State, Session, Tail)
%% @todo Handle errors.
end;
-wt_commands(State0=#state{conn=Conn}, Session=#stream{id=SessionID}, [Cmd|Tail])
+wt_commands(State0=#state{backend=QuicBackend, conn=Conn}, Session=#stream{id=SessionID}, [Cmd|Tail])
when Cmd =:= close; element(1, Cmd) =:= close ->
%% We must send a WT_CLOSE_SESSION capsule on the CONNECT stream.
{AppCode, AppMsg} = case Cmd of
@@ -1025,7 +1037,7 @@ wt_commands(State0=#state{conn=Conn}, Session=#stream{id=SessionID}, [Cmd|Tail])
{close, AppCode0, AppMsg0} -> {AppCode0, AppMsg0}
end,
Capsule = cow_capsule:wt_close_session(AppCode, AppMsg),
- case cowboy_quicer:send(Conn, SessionID, Capsule, fin) of
+ case QuicBackend:send(Conn, SessionID, fin, Capsule) of
ok ->
State = webtransport_terminate_session(State0, Session),
%% @todo Because the handler is in a separate process
@@ -1036,7 +1048,7 @@ wt_commands(State0=#state{conn=Conn}, Session=#stream{id=SessionID}, [Cmd|Tail])
%% @todo Handle errors.
end.
-webtransport_terminate_session(State=#state{conn=Conn, http3_machine=HTTP3Machine0,
+webtransport_terminate_session(State=#state{backend=QuicBackend, conn=Conn, http3_machine=HTTP3Machine0,
streams=Streams0, lingering_streams=Lingering0}, #stream{id=SessionID}) ->
%% Reset/abort the WT streams.
Streams = maps:filtermap(fun
@@ -1046,8 +1058,7 @@ webtransport_terminate_session(State=#state{conn=Conn, http3_machine=HTTP3Machin
false;
(StreamID, #stream{status={webtransport_stream, StreamSessionID}})
when StreamSessionID =:= SessionID ->
- cowboy_quicer:shutdown_stream(Conn, StreamID,
- both, cow_http3:error_to_code(wt_session_gone)),
+ QuicBackend:close_stream(Conn, StreamID, cow_http3:error_to_code(wt_session_gone)),
false;
(_, _) ->
true
@@ -1064,20 +1075,20 @@ webtransport_terminate_session(State=#state{conn=Conn, http3_machine=HTTP3Machin
lingering_streams=Lingering
}.
-stream_peer_send_shutdown(State=#state{conn=Conn}, StreamID) ->
+stream_peer_send_shutdown(State=#state{backend=QuicBackend, conn=Conn}, StreamID) ->
case stream_get(State, StreamID) of
%% Cleanly terminating the CONNECT stream is equivalent
%% to an application error code of 0 and empty message.
Stream = #stream{status={webtransport_session, _}} ->
webtransport_event(State, StreamID, {closed, 0, <<>>}),
%% Shutdown the CONNECT stream fully.
- cowboy_quicer:shutdown_stream(Conn, StreamID),
+ QuicBackend:send(Conn, StreamID, fin, <<>>),
webtransport_terminate_session(State, Stream);
_ ->
State
end.
-reset_stream(State0=#state{conn=Conn, http3_machine=HTTP3Machine0},
+reset_stream(State0=#state{backend=QuicBackend, conn=Conn, http3_machine=HTTP3Machine0},
Stream=#stream{id=StreamID}, Error) ->
Reason = case Error of
{internal_error, _, _} -> h3_internal_error;
@@ -1085,8 +1096,7 @@ reset_stream(State0=#state{conn=Conn, http3_machine=HTTP3Machine0},
end,
%% @todo Do we want to close both sides?
%% @todo Should we close the send side if the receive side was already closed?
- cowboy_quicer:shutdown_stream(Conn, StreamID,
- both, cow_http3:error_to_code(Reason)),
+ QuicBackend:close_stream(Conn, StreamID, cow_http3:error_to_code(Reason)),
State1 = case cow_http3_machine:reset_stream(StreamID, HTTP3Machine0) of
{ok, HTTP3Machine} ->
terminate_stream(State0#state{http3_machine=HTTP3Machine}, Stream, Error);
@@ -1162,9 +1172,9 @@ ignored_frame(State=#state{http3_machine=HTTP3Machine0}, #stream{id=StreamID}) -
terminate(State#state{http3_machine=HTTP3Machine}, Error)
end.
-stream_abort_receive(State=#state{conn=Conn}, Stream=#stream{id=StreamID}, Reason) ->
- cowboy_quicer:shutdown_stream(Conn, StreamID,
- receiving, cow_http3:error_to_code(Reason)),
+stream_abort_receive(State=#state{backend=QuicBackend, conn=Conn},
+ Stream=#stream{id=StreamID}, Reason) ->
+ QuicBackend:stop_sending(Conn, StreamID, cow_http3:error_to_code(Reason)),
stream_store(State, Stream#stream{status=stopping}).
%% @todo Graceful connection shutdown.
@@ -1189,14 +1199,14 @@ maybe_socket_error(State, {error, Reason}, Human) ->
-spec terminate(#state{} | undefined, _) -> no_return().
terminate(undefined, Reason) ->
exit({shutdown, Reason});
-terminate(State=#state{conn=Conn, %http3_status=Status,
+terminate(State=#state{backend=QuicBackend, conn=Conn, %http3_status=Status,
%http3_machine=HTTP3Machine,
streams=Streams, children=Children}, Reason) ->
% if
% Status =:= connected; Status =:= closing_initiated ->
%% @todo
% %% We are terminating so it's OK if we can't send the GOAWAY anymore.
-% _ = cowboy_quicer:send(Conn, ControlID, cow_http3:goaway(
+% _ = QuicBackend:send(Conn, ControlID, cow_http3:goaway(
% cow_http3_machine:get_last_streamid(HTTP3Machine))),
%% We already sent the GOAWAY frame.
% Status =:= closing ->
@@ -1205,7 +1215,7 @@ terminate(State=#state{conn=Conn, %http3_status=Status,
terminate_all_streams(State, maps:to_list(Streams), Reason),
cowboy_children:terminate(Children),
% terminate_linger(State),
- _ = cowboy_quicer:shutdown(Conn, cow_http3:error_to_code(terminate_reason(Reason))),
+ _ = QuicBackend:close(Conn, cow_http3:error_to_code(terminate_reason(Reason))),
exit({shutdown, Reason}).
terminate_reason({connection_error, Reason, _}) -> Reason;
diff --git a/src/cowboy_quic.erl b/src/cowboy_quic.erl
new file mode 100644
index 0000000..94ea074
--- /dev/null
+++ b/src/cowboy_quic.erl
@@ -0,0 +1,38 @@
+%% Copyright (c) Loïc Hoguin <[email protected]>
+%%
+%% 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(cowboy_quic).
+%% @todo -behavior(corral_protocol).
+
+-export([start_link/4]).
+-export([connection_process/5]).
+
+-spec start_link(corral:ref(), module(), corral_backend:conn(), cowboy:opts())
+ -> {ok, pid()}.
+
+start_link(Ref, QuicBackend, Conn, Opts) ->
+ Pid = proc_lib:spawn_link(?MODULE, connection_process,
+ [self(), Ref, QuicBackend, Conn, Opts]),
+ {ok, Pid}.
+
+-spec connection_process(pid(), corral:ref(), module(), corral_backend:conn(), cowboy:opts())
+ -> ok.
+
+connection_process(Parent, Ref, QuicBackend, Conn, Opts) ->
+ {ok, #{alpn := <<"h3">>}} = QuicBackend:handshake(Conn),
+ _ = case maps:get(connection_type, Opts, supervisor) of
+ worker -> ok;
+ supervisor -> process_flag(trap_exit, true)
+ end,
+ cowboy_http3:init(Parent, Ref, QuicBackend, Conn, Opts).
diff --git a/src/cowboy_quicer.erl b/src/cowboy_quicer.erl
deleted file mode 100644
index 6705e9b..0000000
--- a/src/cowboy_quicer.erl
+++ /dev/null
@@ -1,247 +0,0 @@
-%% Copyright (c) Loïc Hoguin <[email protected]>
-%%
-%% 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.
-
-%% QUIC transport using the emqx/quicer NIF.
-
--module(cowboy_quicer).
-
--ifdef(COWBOY_QUICER).
-
-%% Connection.
--export([peername/1]).
--export([sockname/1]).
--export([peercert/1]).
--export([shutdown/2]).
-
-%% Streams.
--export([start_bidi_stream/2]).
--export([start_unidi_stream/2]).
--export([setopt/4]).
--export([send/3]).
--export([send/4]).
--export([send_datagram/2]).
--export([shutdown_stream/2]).
--export([shutdown_stream/4]).
-
-%% Messages.
--export([handle/1]).
-
-%% @todo Make quicer export these types.
--type quicer_connection_handle() :: reference().
--export_type([quicer_connection_handle/0]).
-
--type quicer_app_errno() :: non_neg_integer().
-
--include_lib("quicer/include/quicer.hrl").
-
-%% Connection.
-
--spec peername(quicer_connection_handle())
- -> {ok, {inet:ip_address(), inet:port_number()}}
- | {error, any()}.
-
-peername(Conn) ->
- quicer:peername(Conn).
-
--spec sockname(quicer_connection_handle())
- -> {ok, {inet:ip_address(), inet:port_number()}}
- | {error, any()}.
-
-sockname(Conn) ->
- quicer:sockname(Conn).
-
--spec peercert(quicer_connection_handle())
- -> {ok, public_key:der_encoded()}
- | {error, any()}.
-
-peercert(Conn) ->
- quicer_nif:peercert(Conn).
-
--spec shutdown(quicer_connection_handle(), quicer_app_errno())
- -> ok | {error, any()}.
-
-shutdown(Conn, ErrorCode) ->
- quicer:shutdown_connection(Conn,
- ?QUIC_CONNECTION_SHUTDOWN_FLAG_NONE,
- ErrorCode).
-
-%% Streams.
-
--spec start_bidi_stream(quicer_connection_handle(), iodata())
- -> {ok, cow_http3:stream_id()}
- | {error, any()}.
-
-start_bidi_stream(Conn, InitialData) ->
- start_stream(Conn, InitialData, ?QUIC_STREAM_OPEN_FLAG_NONE).
-
--spec start_unidi_stream(quicer_connection_handle(), iodata())
- -> {ok, cow_http3:stream_id()}
- | {error, any()}.
-
-start_unidi_stream(Conn, InitialData) ->
- start_stream(Conn, InitialData, ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL).
-
-start_stream(Conn, InitialData, OpenFlag) ->
- case quicer:start_stream(Conn, #{
- active => true,
- open_flag => OpenFlag}) of
- {ok, StreamRef} ->
- case quicer:send(StreamRef, InitialData) of
- {ok, _} ->
- {ok, StreamID} = quicer:get_stream_id(StreamRef),
- put({quicer_stream, StreamID}, StreamRef),
- {ok, StreamID};
- Error ->
- Error
- end;
- {error, Reason1, Reason2} ->
- {error, {Reason1, Reason2}};
- Error ->
- Error
- end.
-
--spec setopt(quicer_connection_handle(), cow_http3:stream_id(), active, boolean())
- -> ok | {error, any()}.
-
-setopt(_Conn, StreamID, active, Value) ->
- StreamRef = get({quicer_stream, StreamID}),
- quicer:setopt(StreamRef, active, Value).
-
--spec send(quicer_connection_handle(), cow_http3:stream_id(), iodata())
- -> ok | {error, any()}.
-
-send(Conn, StreamID, Data) ->
- send(Conn, StreamID, Data, nofin).
-
--spec send(quicer_connection_handle(), cow_http3:stream_id(), iodata(), cow_http:fin())
- -> ok | {error, any()}.
-
-send(_Conn, StreamID, Data, IsFin) ->
- StreamRef = get({quicer_stream, StreamID}),
- Size = iolist_size(Data),
- case quicer:send(StreamRef, Data, send_flag(IsFin)) of
- {ok, Size} ->
- ok;
- {error, Reason1, Reason2} ->
- {error, {Reason1, Reason2}};
- Error ->
- Error
- end.
-
-send_flag(nofin) -> ?QUIC_SEND_FLAG_NONE;
-send_flag(fin) -> ?QUIC_SEND_FLAG_FIN.
-
--spec send_datagram(quicer_connection_handle(), iodata())
- -> ok | {error, any()}.
-
-send_datagram(Conn, Data) ->
- %% @todo Fix/ignore the Dialyzer error instead of doing this.
- DataBin = iolist_to_binary(Data),
- Size = byte_size(DataBin),
- case quicer:send_dgram(Conn, DataBin) of
- {ok, Size} ->
- ok;
- %% @todo Handle error cases.
- Error ->
- Error
- end.
-
--spec shutdown_stream(quicer_connection_handle(), cow_http3:stream_id())
- -> ok.
-
-shutdown_stream(_Conn, StreamID) ->
- StreamRef = get({quicer_stream, StreamID}),
- _ = quicer:shutdown_stream(StreamRef),
- ok.
-
--spec shutdown_stream(quicer_connection_handle(),
- cow_http3:stream_id(), both | receiving, quicer_app_errno())
- -> ok.
-
-shutdown_stream(_Conn, StreamID, Dir, ErrorCode) ->
- StreamRef = get({quicer_stream, StreamID}),
- _ = quicer:shutdown_stream(StreamRef, shutdown_flag(Dir), ErrorCode, infinity),
- ok.
-
-%% @todo Are these flags correct for what we want?
-shutdown_flag(both) -> ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT;
-shutdown_flag(receiving) -> ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT_RECEIVE.
-
-%% Messages.
-
-%% @todo Probably should have the Conn given as argument too?
--spec handle({quic, _, _, _})
- -> {data, cow_http3:stream_id(), cow_http:fin(), binary()}
- | {datagram, binary()}
- | {stream_started, cow_http3:stream_id(), unidi | bidi}
- | {stream_closed, cow_http3:stream_id(), quicer_app_errno()}
- | closed
- | {peer_send_shutdown, cow_http3:stream_id()}
- | ok
- | unknown
- | {socket_error, any()}.
-
-handle({quic, Data, StreamRef, #{flags := Flags}}) when is_binary(Data) ->
- {ok, StreamID} = quicer:get_stream_id(StreamRef),
- IsFin = case Flags band ?QUIC_RECEIVE_FLAG_FIN of
- ?QUIC_RECEIVE_FLAG_FIN -> fin;
- _ -> nofin
- end,
- {data, StreamID, IsFin, Data};
-%% @todo Match on Conn.
-handle({quic, Data, _Conn, Flags}) when is_binary(Data), is_integer(Flags) ->
- {datagram, Data};
-%% QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED.
-handle({quic, new_stream, StreamRef, #{flags := Flags}}) ->
- case quicer:setopt(StreamRef, active, true) of
- ok ->
- {ok, StreamID} = quicer:get_stream_id(StreamRef),
- put({quicer_stream, StreamID}, StreamRef),
- StreamType = case quicer:is_unidirectional(Flags) of
- true -> unidi;
- false -> bidi
- end,
- {stream_started, StreamID, StreamType};
- {error, Reason} ->
- {socket_error, Reason}
- end;
-%% QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE.
-handle({quic, stream_closed, StreamRef, #{error := ErrorCode}}) ->
- {ok, StreamID} = quicer:get_stream_id(StreamRef),
- {stream_closed, StreamID, ErrorCode};
-%% QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE.
-handle({quic, closed, Conn, _Flags}) ->
- _ = quicer:close_connection(Conn),
- closed;
-%% The following events are currently ignored either because
-%% I do not know what they do or because we do not need to
-%% take action.
-handle({quic, streams_available, _Conn, _Props}) ->
- ok;
-handle({quic, dgram_state_changed, _Conn, _Props}) ->
- ok;
-%% QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT
-handle({quic, transport_shutdown, _Conn, _Flags}) ->
- ok;
-handle({quic, peer_send_shutdown, StreamRef, undefined}) ->
- {ok, StreamID} = quicer:get_stream_id(StreamRef),
- {peer_send_shutdown, StreamID};
-handle({quic, send_shutdown_complete, _StreamRef, _IsGraceful}) ->
- ok;
-handle({quic, shutdown, _Conn, success}) ->
- ok;
-handle(_Msg) ->
- unknown.
-
--endif.
diff --git a/test/cowboy_test.erl b/test/cowboy_test.erl
index 541e8f9..fc3b6a6 100644
--- a/test/cowboy_test.erl
+++ b/test/cowboy_test.erl
@@ -37,32 +37,30 @@ 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.
+%% @todo This will probably require QuicOpts as argument.
init_http3(Ref, ProtoOpts, Config) ->
- %% @todo Quicer does not currently support non-file cert/key,
+ %% @todo Corral 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.
+ %% @todo We will want to setup cacerts as well in the client.
DataDir = filename:dirname(filename:dirname(config(data_dir, Config)))
++ "/rfc9114_SUITE_data",
- TransOpts = #{
- socket_opts => [
- {certfile, DataDir ++ "/server.pem"},
- {keyfile, DataDir ++ "/server.key"}
- ]
+ QuicOpts = #{
+ certfile => DataDir ++ "/server.pem",
+ keyfile => DataDir ++ "/server.key"
},
- {ok, Listener} = cowboy:start_quic(Ref, TransOpts, ProtoOpts),
- {ok, {_, Port}} = quicer:sockname(Listener),
+ ct:pal("wc ~0p", [supervisor:which_children(corral_sup)]),
+ {ok, Listener} = cowboy:start_quic(Ref, QuicOpts, ProtoOpts),
+ Port = corral:get_port(Ref),
%% @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].
+ [{ref, Ref}, {type, quic}, {protocol, http3}, {port, Port}, {opts, QuicOpts}|Config].
stop_group(Ref) ->
case persistent_term:get({cowboy_test_quic, Ref}, undefined) of
undefined ->
cowboy:stop_listener(Ref);
- Listener ->
- quicer:close_listener(Listener)
+ _ ->
+ corral:stop_listener(Ref)
end.
%% Common group of listeners used by most suites.
diff --git a/test/decompress_SUITE.erl b/test/decompress_SUITE.erl
index f1eb13a..d939b5d 100644
--- a/test/decompress_SUITE.erl
+++ b/test/decompress_SUITE.erl
@@ -52,6 +52,8 @@ init_per_group(Name = h2c_compress, Config) ->
init_per_group(Name = h3_compress, Config) ->
cowboy_test:init_http3(Name, init_compress_opts(Config), Config).
+end_per_group(Name, _) when Name =:= h3; Name =:= h3_compress ->
+ corral:stop_listener(Name);
end_per_group(Name, _) ->
cowboy:stop_listener(Name).
diff --git a/test/draft_h3_webtransport_SUITE.erl b/test/draft_h3_webtransport_SUITE.erl
index 05a6c17..c6382ec 100644
--- a/test/draft_h3_webtransport_SUITE.erl
+++ b/test/draft_h3_webtransport_SUITE.erl
@@ -20,7 +20,7 @@
-import(ct_helper, [doc/1]).
-import(rfc9114_SUITE, [do_wait_stream_aborted/1]).
--ifdef(COWBOY_QUICER).
+-ifdef(CORRAL).
-include_lib("quicer/include/quicer.hrl").
diff --git a/test/rfc9114_SUITE.erl b/test/rfc9114_SUITE.erl
index a03b493..cea740b 100644
--- a/test/rfc9114_SUITE.erl
+++ b/test/rfc9114_SUITE.erl
@@ -19,7 +19,7 @@
-import(ct_helper, [config/2]).
-import(ct_helper, [doc/1]).
--ifdef(COWBOY_QUICER).
+-ifdef(CORRAL).
-include_lib("quicer/include/quicer.hrl").
diff --git a/test/rfc9204_SUITE.erl b/test/rfc9204_SUITE.erl
index 942c41b..b52be74 100644
--- a/test/rfc9204_SUITE.erl
+++ b/test/rfc9204_SUITE.erl
@@ -19,7 +19,7 @@
-import(ct_helper, [config/2]).
-import(ct_helper, [doc/1]).
--ifdef(COWBOY_QUICER).
+-ifdef(CORRAL).
-include_lib("quicer/include/quicer.hrl").
diff --git a/test/rfc9220_SUITE.erl b/test/rfc9220_SUITE.erl
index 38a59b2..49b6b97 100644
--- a/test/rfc9220_SUITE.erl
+++ b/test/rfc9220_SUITE.erl
@@ -56,74 +56,82 @@ 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, #{
+ Config = cowboy_test:init_http3(?FUNCTION_NAME, #{
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.
+ try
+ %% 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
+ after
+ corral:stop_listener(?FUNCTION_NAME)
+ end.
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, #{
+ Config = cowboy_test:init_http3(?FUNCTION_NAME, #{
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.
+ try
+ %% 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
+ after
+ corral:stop_listener(?FUNCTION_NAME)
+ end.
% The Extended CONNECT Method.