aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2026-05-06 16:09:36 +0200
committerLoïc Hoguin <[email protected]>2026-05-06 16:09:36 +0200
commit4ef0813e7c4f0fbbf7b778a1c852d65f30688d96 (patch)
tree5a1430b9030e060115b7a4c51cbe58d9edfac3c3
parent514bcb2e90ddc3e1fc6ca47bc8c34d81daa2ff4f (diff)
downloadgun-corral.tar.gz
gun-corral.tar.bz2
gun-corral.zip
-rw-r--r--Makefile19
-rw-r--r--ebin/gun.app4
-rw-r--r--src/gun.erl8
-rw-r--r--src/gun_http3.erl50
-rw-r--r--src/gun_quic.erl245
-rw-r--r--src/gun_quicer.erl283
6 files changed, 283 insertions, 326 deletions
diff --git a/Makefile b/Makefile
index e69fbda..b89785d 100644
--- a/Makefile
+++ b/Makefile
@@ -15,11 +15,11 @@ CT_OPTS += -ct_hooks gun_ct_hook [] # -boot start_sasl
LOCAL_DEPS = public_key ssl
DEPS = cowlib
-dep_cowlib = git https://github.com/ninenines/cowlib 2.15.0
+dep_cowlib = git https://github.com/ninenines/cowlib corral
-ifeq ($(GUN_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
@@ -68,20 +68,15 @@ endif
TEST_ERLC_OPTS += +'{parse_transform, eunit_autoexport}'
-ifeq ($(GUN_QUICER),1)
-ERLC_OPTS += -D GUN_QUICER=1
-TEST_ERLC_OPTS += -D GUN_QUICER=1
+ifdef CORRAL_DEPS
+ERLC_OPTS += -D CORRAL=1
+TEST_ERLC_OPTS += -D CORRAL=1
endif
# Generate rebar.config on build.
app:: rebar.config
-# Fix quicer compilation for HTTP/3.
-
-autopatch-quicer::
- $(verbose) printf "%s\n" "all: ;" > $(DEPS_DIR)/quicer/c_src/Makefile.erlang.mk
-
# h2specd setup.
GOPATH := $(ERLANG_MK_TMP)/gopath
diff --git a/ebin/gun.app b/ebin/gun.app
index d52fa95..4a4f1ee 100644
--- a/ebin/gun.app
+++ b/ebin/gun.app
@@ -1,10 +1,10 @@
{application, 'gun', [
{description, "HTTP/1.1, HTTP/2 and Websocket client for Erlang/OTP."},
{vsn, "2.2.0"},
- {modules, ['gun','gun_app','gun_conns_sup','gun_content_handler','gun_cookies','gun_cookies_list','gun_data_h','gun_default_event_h','gun_event','gun_http','gun_http2','gun_http3','gun_pool','gun_pool_events_h','gun_pools_sup','gun_protocols','gun_public_suffix','gun_quicer','gun_raw','gun_socks','gun_sse_h','gun_sup','gun_tcp','gun_tcp_proxy','gun_tls','gun_tls_proxy','gun_tls_proxy_cb','gun_tls_proxy_http2_connect','gun_tunnel','gun_ws','gun_ws_h','gun_ws_protocol']},
+ {modules, ['gun','gun_app','gun_conns_sup','gun_content_handler','gun_cookies','gun_cookies_list','gun_data_h','gun_default_event_h','gun_event','gun_http','gun_http2','gun_http3','gun_pool','gun_pool_events_h','gun_pools_sup','gun_protocols','gun_public_suffix','gun_quic','gun_raw','gun_socks','gun_sse_h','gun_sup','gun_tcp','gun_tcp_proxy','gun_tls','gun_tls_proxy','gun_tls_proxy_cb','gun_tls_proxy_http2_connect','gun_tunnel','gun_ws','gun_ws_h','gun_ws_protocol']},
{registered, [gun_sup]},
{applications, [kernel,stdlib,public_key,ssl,cowlib]},
{optional_applications, []},
- {mod, {gun_app, []}},
+ {mod, {'gun_app', []}},
{env, []}
]}. \ No newline at end of file
diff --git a/src/gun.erl b/src/gun.erl
index e23da3a..25f9913 100644
--- a/src/gun.erl
+++ b/src/gun.erl
@@ -1033,7 +1033,7 @@ init({Owner, Host, Port, Opts}) ->
{OriginScheme, Transport} = case OriginTransport of
tcp -> {<<"http">>, gun_tcp};
tls -> {<<"https">>, gun_tls};
- quic -> {<<"https">>, gun_quicer}
+ quic -> {<<"https">>, gun_quic}
end,
OwnerRef = monitor(process, Owner),
{EvHandler, EvHandlerState0} = maps:get(event_handler, Opts,
@@ -1121,7 +1121,7 @@ domain_lookup(Type, Event, State) ->
handle_common(Type, Event, ?FUNCTION_NAME, State).
connecting(_, {retries, Retries, LookupInfo}, State=#state{opts=Opts,
- transport=gun_quicer, event_handler=EvHandler, event_handler_state=EvHandlerState0}) ->
+ transport=gun_quic, event_handler=EvHandler, event_handler_state=EvHandlerState0}) ->
%% @todo We are doing the TLS handshake at the same time,
%% we cannot separate it from the connection. Fire events.
ConnectTimeout = maps:get(connect_timeout, Opts, infinity),
@@ -1130,7 +1130,7 @@ connecting(_, {retries, Retries, LookupInfo}, State=#state{opts=Opts,
timeout => ConnectTimeout
},
EvHandlerState1 = EvHandler:connect_start(ConnectEvent, EvHandlerState0),
- case gun_quicer:connect(LookupInfo, ConnectTimeout) of
+ case gun_quic:connect(LookupInfo, ConnectTimeout) of
{ok, Socket} ->
%% @todo We should double check the ALPN result.
[Protocol] = maps:get(protocols, Opts, [http3]),
@@ -1702,7 +1702,7 @@ maybe_active(Other) ->
active(State=#state{active=false}) ->
{ok, State};
-active(State=#state{transport=gun_quicer}) ->
+active(State=#state{transport=gun_quic}) ->
{ok, State};
active(State=#state{socket=Socket, transport=Transport}) ->
case Transport:setopts(Socket, [{active, once}]) of
diff --git a/src/gun_http3.erl b/src/gun_http3.erl
index 26aa072..48f1ffe 100644
--- a/src/gun_http3.erl
+++ b/src/gun_http3.erl
@@ -69,7 +69,7 @@
-record(http3_state, {
reply_to :: gun:reply_to(),
- conn :: gun_quicer:quicer_connection_handle(),
+ conn :: gun_quic:conn(),
transport :: module(),
opts = #{} :: gun:http2_opts(),
content_handlers :: gun_content_handler:opt(),
@@ -105,9 +105,9 @@ init(ReplyTo, Conn, Transport, Opts) ->
Handlers = maps:get(content_handlers, Opts, [gun_data_h]),
{ok, SettingsBin, HTTP3Machine0} = cow_http3_machine:init(client, Opts),
%% @todo We may get a TLS 1.3 error/alert here in mTLS scenarios.
- {ok, ControlID} = Transport:start_unidi_stream(Conn, [<<0>>, SettingsBin]),
- {ok, EncoderID} = Transport:start_unidi_stream(Conn, [<<2>>]),
- {ok, DecoderID} = Transport:start_unidi_stream(Conn, [<<3>>]),
+ {ok, ControlID} = Transport:open_unidi_stream(Conn, [<<0>>, SettingsBin]),
+ {ok, EncoderID} = Transport:open_unidi_stream(Conn, [<<2>>]),
+ {ok, DecoderID} = Transport:open_unidi_stream(Conn, [<<3>>]),
%% Set the control, encoder and decoder streams in the machine.
HTTP3Machine = cow_http3_machine:init_unidi_local_streams(
ControlID, EncoderID, DecoderID, HTTP3Machine0),
@@ -123,27 +123,27 @@ switch_transport(_Transport, _Conn, _State) ->
handle(Msg, State0=#http3_state{transport=Transport},
CookieStore, EvHandler, EvHandlerState) ->
- case Transport:handle(Msg) of
- {data, StreamID, IsFin, Data} ->
+ case Transport:make_event(Msg) of
+ {stream_data, StreamID, IsFin, Data} ->
parse(Data, State0, StreamID, IsFin,
CookieStore, EvHandler, EvHandlerState);
- {stream_started, StreamID, StreamType} ->
+ {stream_opened, StreamID, StreamType} ->
State = stream_new_remote(State0, StreamID, StreamType),
{{state, State}, CookieStore, EvHandlerState};
- {stream_closed, _StreamID, _ErrorCode} ->
- %% @todo Clean up the stream.
- {{state, State0}, CookieStore, EvHandlerState};
- {stream_peer_send_aborted, StreamID, ErrorCode} ->
+ {stream_reset, StreamID, ErrorCode} ->
Reason = cow_http3:code_to_error(ErrorCode),
{StateOrError, EvHandlerStateRet} = stream_aborted(
State0, StreamID, Reason, EvHandler, EvHandlerState),
{StateOrError, CookieStore, EvHandlerStateRet};
- closed ->
+ {stream_stop_sending, _StreamID, _AppErrno} ->
+ %% @todo Clean up the stream.
+ {{state, State0}, CookieStore, EvHandlerState};
+ {conn_closed, _, _} ->
%% @todo Terminate the connection.
{{state, State0}, CookieStore, EvHandlerState};
- ok ->
+ no_event ->
{{state, State0}, CookieStore, EvHandlerState};
- unknown ->
+ unknown_msg ->
%% @todo Log a warning.
{{state, State0}, CookieStore, EvHandlerState}
end.
@@ -281,7 +281,7 @@ parse_unidirectional_stream_header(Data, State0=#http3_state{http3_machine=HTTP3
%% Unknown stream types must be ignored. We choose to abort the
%% stream instead of reading and discarding the incoming data.
{undefined, _} ->
- {{state, (stream_abort_receive(State0, Stream0, h3_stream_creation_error))},
+ {{state, (stream_stop_sending_local(State0, Stream0, h3_stream_creation_error))},
CookieStore, EvHandlerState}
end.
@@ -455,9 +455,9 @@ reset_stream(_State, _StreamID, _Error) ->
ignored_frame(_State, _Stream) ->
error(todo).
-stream_abort_receive(State=#http3_state{conn=Conn, transport=Transport},
+stream_stop_sending_local(State=#http3_state{conn=Conn, transport=Transport},
Stream=#stream{id=StreamID}, Reason) ->
- Transport:shutdown_stream(Conn, StreamID, receiving, cow_http3:error_to_code(Reason)),
+ Transport:stop_sending(Conn, StreamID, cow_http3:error_to_code(Reason)),
stream_update(State, Stream#stream{status=discard}).
-spec connection_error(_, _) -> no_return().
@@ -497,8 +497,8 @@ headers(State0=#http3_state{conn=Conn, transport=Transport,
http3_machine=HTTP3Machine0}, StreamRef, ReplyTo, Method, Host, Port,
Path, Headers0, _InitialFlow0, CookieStore0, EvHandler, EvHandlerState0)
when is_reference(StreamRef) ->
- {ok, StreamID} = Transport:start_bidi_stream(Conn),
- HTTP3Machine1 = cow_http3_machine:init_bidi_stream(StreamID,
+ {ok, StreamID} = Transport:open_bidi_stream(Conn),
+ HTTP3Machine1 = cow_http3_machine:new_bidi_stream(StreamID,
iolist_to_binary(Method), HTTP3Machine0),
{ok, PseudoHeaders, Headers, CookieStore} = prepare_headers(
Method, Host, Port, Path, Headers0, CookieStore0),
@@ -533,8 +533,8 @@ request(State0=#http3_state{conn=Conn, transport=Transport,
Headers1 = lists:keystore(<<"content-length">>, 1, Headers0,
{<<"content-length">>, integer_to_binary(iolist_size(Body))}),
%% @todo InitialFlow = initial_flow(InitialFlow0, Opts),
- {ok, StreamID} = Transport:start_bidi_stream(Conn),
- HTTP3Machine1 = cow_http3_machine:init_bidi_stream(StreamID,
+ {ok, StreamID} = Transport:open_bidi_stream(Conn),
+ HTTP3Machine1 = cow_http3_machine:new_bidi_stream(StreamID,
iolist_to_binary(Method), HTTP3Machine0),
{ok, PseudoHeaders, Headers, CookieStore} = prepare_headers(
Method, Host, Port, Path, Headers1, CookieStore0),
@@ -554,14 +554,14 @@ request(State0=#http3_state{conn=Conn, transport=Transport,
StreamID, HTTP3Machine1, fin, PseudoHeaders, Headers),
State1 = send_instructions(State0#http3_state{http3_machine=HTTP3Machine}, Instrs),
%% @todo Handle send errors.
- ok = Transport:send(Conn, StreamID, [
+ ok = Transport:send(Conn, StreamID, fin, [
cow_http3:headers(HeaderBlock),
%% Only send a DATA frame if we have a body.
case iolist_size(Body) of
0 -> <<>>;
_ -> cow_http3:data(Body)
end
- ], fin),
+ ]),
EvHandlerState = EvHandler:request_headers(RequestEvent, EvHandlerState1),
Stream = #stream{id=StreamID, ref=StreamRef, reply_to=ReplyTo,
status=normal, authority=Authority, path=Path},
@@ -619,7 +619,7 @@ data(State=#http3_state{conn=Conn, transport=Transport}, StreamRef, _ReplyTo, Is
_EvHandler, EvHandlerState) when is_reference(StreamRef) ->
case stream_get_by_ref(State, StreamRef) of
#stream{id=StreamID} -> %, tunnel=Tunnel} ->
- ok = Transport:send(Conn, StreamID, cow_http3:data(Data), IsFin),
+ ok = Transport:send(Conn, StreamID, IsFin, cow_http3:data(Data)),
{[], EvHandlerState} %% @todo Probably wrong, need to update/keep states?
%% @todo
% case cow_http2_machine:get_stream_local_state(StreamID, HTTP2Machine) of
@@ -701,7 +701,7 @@ stream_new_remote(State=#http3_state{http3_machine=HTTP3Machine0,
%% All remote streams to the client are expected to be unidirectional.
%% @todo Handle errors instead of crashing.
unidi = StreamType,
- HTTP3Machine = cow_http3_machine:init_unidi_stream(StreamID,
+ HTTP3Machine = cow_http3_machine:new_unidi_stream(StreamID,
unidi_remote, HTTP3Machine0),
StreamRef = make_ref(),
Stream = #stream{id=StreamID, ref=StreamRef, status=header},
diff --git a/src/gun_quic.erl b/src/gun_quic.erl
new file mode 100644
index 0000000..7ac18e0
--- /dev/null
+++ b/src/gun_quic.erl
@@ -0,0 +1,245 @@
+%% 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(gun_quic).
+
+-export([name/0]).
+-export([messages/0]).
+-export([connect/2]).
+-export([sockname/1]).
+-export([close/1]).
+
+-export([open_bidi_stream/1]).
+-export([open_unidi_stream/2]).
+-export([send/3]).
+-export([send/4]).
+-export([stop_sending/3]).
+-export([make_event/1]).
+
+-opaque conn() :: any().
+-export_type([conn/0]).
+
+-ifndef(CORRAL).
+
+-spec name() -> no_return().
+name() -> no_quic().
+
+-spec messages() -> no_return().
+messages() -> no_quic().
+
+-spec connect(_, _) -> no_return().
+connect(_, _) -> no_quic().
+
+-spec sockname(_) -> no_return().
+sockname(_) -> no_quic().
+
+-spec close(_) -> no_return().
+close(_) -> no_quic().
+
+-spec open_bidi_stream(_) -> no_return().
+open_bidi_stream(_) -> no_quic().
+
+-spec open_unidi_stream(_, _) -> no_return().
+open_unidi_stream(_, _) -> no_quic().
+
+-spec send(_, _, _) -> no_return().
+send(_, _, _) -> no_quic().
+
+-spec send(_, _, _, _) -> no_return().
+send(_, _, _, _) -> no_quic().
+
+-spec shutdown_stream(_, _, _, _) -> no_return().
+shutdown_stream(_, _, _, _) -> no_quic().
+
+-spec handle(_) -> no_return().
+handle(_) -> no_quic().
+
+-spec no_quic() -> no_return().
+no_quic() ->
+ error({no_quic,
+ "Gun must be compiled with environment variable CORRAL_DEPS "
+ "(with a value that includes 'quicer') "
+ "or with compilation flag -D CORRAL=1 in order to enable "
+ "QUIC support"}).
+
+-else.
+
+-spec name() -> quic.
+
+name() -> quic.
+
+-spec messages() -> {quic, quic, quic}.
+
+%% Quic messages aren't compatible with gen_tcp/ssl; unused.
+messages() -> {quic, quic, quic}.
+
+connect(#{ip_addresses := IPs, port := Port, tcp_opts := _Opts}, Timeout) ->
+ Timer = inet:start_timer(Timeout),
+ %% @todo We must not disable security by default.
+ QuicOpts = #{
+ alpn => [<<"h3">>],
+ max_streams_unidi => 3,
+ verify => none
+ }, %% @todo We need quic_opts not tcp_opts.
+ Res = try
+ try_connect(IPs, Port, QuicOpts, Timer, {error, einval})
+ after
+ _ = inet:stop_timer(Timer)
+ end,
+ case Res of
+ {ok, Conn} -> {ok, Conn};
+ Error -> maybe_exit(Error)
+ end.
+
+-dialyzer({nowarn_function, try_connect/5}).
+
+try_connect([IP|IPs], Port, Opts, Timer, _) ->
+ Timeout = inet:timeout(Timer),
+ case corral_quicer:connect(IP, Port, Opts#{connect_timeout => Timeout}) of
+ {ok, Conn} ->
+ {ok, Conn};
+ {error, econnrefused} ->
+ timer:sleep(1),
+ try_connect([IP|IPs], Port, Opts, Timer, {error, econnrefused});
+ {error, Reason} ->
+ try_connect(IPs, Port, Opts, Timer, {error, Reason})
+ end;
+try_connect([], _, _, _, Error) ->
+ Error.
+
+-dialyzer({nowarn_function, maybe_exit/1}).
+
+maybe_exit({error, einval}) -> exit(badarg);
+maybe_exit({error, eaddrnotavail}) -> exit(badarg); %% @todo Probably dead code right now.
+maybe_exit(Error) -> Error.
+
+-spec sockname(conn())
+ -> {ok, {inet:ip_address(), inet:port_number()}}
+ | {error, any()}.
+
+sockname(Conn) ->
+ corral_quicer:sockname(Conn).
+
+-spec close(conn()) -> ok.
+
+close(Conn) ->
+ corral_quicer:close(Conn).
+
+-spec open_bidi_stream(conn())
+ -> {ok, cow_http3:stream_id()}
+ | {error, any()}.
+
+%% We cannot send data immediately because we need the
+%% StreamID in order to compress the headers.
+open_bidi_stream(Conn) ->
+ corral_quicer:open_bidi_stream(Conn, <<>>).
+
+-spec open_unidi_stream(conn(), iodata())
+ -> {ok, cow_http3:stream_id()}
+ | {error, any()}.
+
+open_unidi_stream(Conn, InitialData) ->
+ corral_quicer:open_unidi_stream(Conn, InitialData).
+
+-spec send(conn(), cow_http3:stream_id(), iodata())
+ -> ok | {error, any()}.
+
+send(Conn, StreamID, Data) ->
+ corral_quicer:send(Conn, StreamID, Data).
+
+-spec send(conn(), cow_http3:stream_id(), iodata(), cow_http:fin())
+ -> ok | {error, any()}.
+
+send(Conn, StreamID, IsFin, Data) ->
+ corral_quicer:send(Conn, StreamID, IsFin, Data).
+
+-spec stop_sending(conn(), cow_http3:stream_id(), corral_backend:app_errno())
+ -> ok | {error, any()}.
+
+stop_sending(Conn, StreamID, AppErrno) ->
+ corral_quicer:stop_sending(Conn, StreamID, AppErrno).
+
+-spec make_event(tuple()) -> corral_backend:event().
+
+make_event(Msg) ->
+ corral_quicer:make_event(Msg).
+
+
+
+
+
+
+%% @todo Probably should have the Conn given as argument too?
+%-spec handle({quic, _, _, _})
+% -> {data, cow_http3:stream_id(), cow_http:fin(), binary()}
+% | {stream_started, cow_http3:stream_id(), unidi | bidi}
+% | {stream_closed, cow_http3:stream_id(), corral_backend:app_errno()}
+% | closed
+% | ok
+% | unknown
+% | {socket_error, any()}.
+%
+%handle({quic, peer_send_aborted, QStreamRef, ErrorCode}) ->
+% {ok, StreamID} = quicer:get_stream_id(QStreamRef),
+% {stream_peer_send_aborted, StreamID, ErrorCode};
+%%% Clauses past this point copied from cowboy_quicer.
+%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};
+%%% 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;
+%handle({quic, send_shutdown_complete, _StreamRef, _IsGraceful}) ->
+% ok;
+%handle({quic, shutdown, _Conn, success}) ->
+% ok;
+%handle(_Msg) ->
+% unknown.
+
+-endif.
diff --git a/src/gun_quicer.erl b/src/gun_quicer.erl
deleted file mode 100644
index 3782803..0000000
--- a/src/gun_quicer.erl
+++ /dev/null
@@ -1,283 +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.
-
--module(gun_quicer).
-
--export([name/0]).
--export([messages/0]).
--export([connect/2]).
--export([sockname/1]).
--export([close/1]).
-
--export([start_bidi_stream/1]).
--export([start_unidi_stream/2]).
--export([send/3]).
--export([send/4]).
--export([shutdown_stream/4]).
--export([handle/1]).
-
-%% @todo Make quicer export this type.
--type quicer_connection_handle() :: reference().
--export_type([quicer_connection_handle/0]).
-
--ifndef(GUN_QUICER).
-
--spec name() -> no_return().
-name() -> no_quicer().
-
--spec messages() -> no_return().
-messages() -> no_quicer().
-
--spec connect(_, _) -> no_return().
-connect(_, _) -> no_quicer().
-
--spec sockname(_) -> no_return().
-sockname(_) -> no_quicer().
-
--spec close(_) -> no_return().
-close(_) -> no_quicer().
-
--spec start_bidi_stream(_) -> no_return().
-start_bidi_stream(_) -> no_quicer().
-
--spec start_unidi_stream(_, _) -> no_return().
-start_unidi_stream(_, _) -> no_quicer().
-
--spec send(_, _, _) -> no_return().
-send(_, _, _) -> no_quicer().
-
--spec send(_, _, _, _) -> no_return().
-send(_, _, _, _) -> no_quicer().
-
--spec shutdown_stream(_, _, _, _) -> no_return().
-shutdown_stream(_, _, _, _) -> no_quicer().
-
--spec handle(_) -> no_return().
-handle(_) -> no_quicer().
-
--spec no_quicer() -> no_return().
-no_quicer() ->
- error({no_quicer,
- "Cowboy must be compiled with environment variable COWBOY_QUICER=1 "
- "or with compilation flag -D COWBOY_QUICER=1 in order to enable "
- "QUIC support using the emqx/quic NIF"}).
-
--else.
-
-%% @todo Make quicer export this type.
--type quicer_app_errno() :: non_neg_integer().
-
--include_lib("quicer/include/quicer.hrl").
-
--spec name() -> quic.
-
-name() -> quic.
-
--spec messages() -> {quic, quic, quic}.
-
-%% Quicer messages aren't compatible with gen_tcp/ssl.
-messages() -> {quic, quic, quic}.
-
-connect(#{ip_addresses := IPs, port := Port, tcp_opts := _Opts}, Timeout) ->
- Timer = inet:start_timer(Timeout),
- %% @todo We must not disable security by default.
- QuicOpts = #{
- alpn => ["h3"],
- peer_unidi_stream_count => 3,
- verify => none
- }, %% @todo We need quic_opts not tcp_opts.
- Res = try
- try_connect(IPs, Port, QuicOpts, Timer, {error, einval})
- after
- _ = inet:stop_timer(Timer)
- end,
- case Res of
- {ok, Conn} -> {ok, Conn};
- Error -> maybe_exit(Error)
- end.
-
--dialyzer({nowarn_function, try_connect/5}).
-
-try_connect([IP|IPs], Port, Opts, Timer, _) ->
- Timeout = inet:timeout(Timer),
- case quicer:connect(IP, Port, Opts, Timeout) of
- {ok, Conn} ->
- {ok, Conn};
- {error, Reason} ->
- {error, Reason};
- {error, transport_down, #{error := 2, status := connection_refused}} ->
- timer:sleep(1),
- try_connect([IP|IPs], Port, Opts, Timer, {error, einval});
- {error, Reason, Flags} ->
- try_connect(IPs, Port, Opts, Timer, {error, {Reason, Flags}})
- end;
-try_connect([], _, _, _, Error) ->
- Error.
-
--dialyzer({nowarn_function, maybe_exit/1}).
-
-maybe_exit({error, einval}) -> exit(badarg);
-maybe_exit({error, eaddrnotavail}) -> exit(badarg);
-maybe_exit(Error) -> Error.
-
--spec sockname(quicer_connection_handle())
- -> {ok, {inet:ip_address(), inet:port_number()}}
- | {error, any()}.
-
-sockname(Conn) ->
- quicer:sockname(Conn).
-
--spec close(quicer_connection_handle()) -> ok.
-
-close(Conn) ->
- quicer:close_connection(Conn).
-
--spec start_bidi_stream(quicer_connection_handle())
- -> {ok, cow_http3:stream_id()}
- | {error, any()}.
-
-%% We cannot send data immediately because we need the
-%% StreamID in order to compress the headers.
-start_bidi_stream(Conn) ->
- case quicer:start_stream(Conn, #{active => true}) of
- {ok, StreamRef} ->
- {ok, StreamID} = quicer:get_stream_id(StreamRef),
- put({quicer_stream, StreamID}, StreamRef),
- {ok, StreamID};
- {error, Reason1, Reason2} ->
- {error, {Reason1, Reason2}};
- Error ->
- Error
- end.
-
--spec start_unidi_stream(quicer_connection_handle(), iodata())
- -> {ok, cow_http3:stream_id()}
- | {error, any()}.
-
-%% Function copied from cowboy_quicer.
-start_unidi_stream(Conn, HeaderData) ->
- case quicer:start_stream(Conn, #{
- active => true,
- open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}) of
- {ok, StreamRef} ->
- case quicer:send(StreamRef, HeaderData) 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 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, nofin) ->
- Len = iolist_size(Data),
- StreamRef = get({quicer_stream, StreamID}),
- {ok, Len} = quicer:send(StreamRef, Data),
- ok;
-send(_Conn, StreamID, Data, fin) ->
- Len = iolist_size(Data),
- StreamRef = get({quicer_stream, StreamID}),
- {ok, Len} = quicer:send(StreamRef, Data, ?QUIC_SEND_FLAG_FIN),
- ok.
-
--spec shutdown_stream(quicer_connection_handle(),
- cow_http3:stream_id(), both | receiving, quicer_app_errno())
- -> ok.
-
-%% Function copied from cowboy_quicer.
-shutdown_stream(_Conn, StreamID, Dir, ErrorCode) ->
- StreamRef = get({quicer_stream, StreamID}),
- _ = quicer:shutdown_stream(StreamRef, shutdown_flag(Dir), ErrorCode, infinity),
- ok.
-
-shutdown_flag(both) -> ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT;
-shutdown_flag(receiving) -> ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT_RECEIVE.
-
-%% @todo Probably should have the Conn given as argument too?
--spec handle({quic, _, _, _})
- -> {data, cow_http3:stream_id(), cow_http:fin(), binary()}
- | {stream_started, cow_http3:stream_id(), unidi | bidi}
- | {stream_closed, cow_http3:stream_id(), quicer_app_errno()}
- | closed
- | ok
- | unknown
- | {socket_error, any()}.
-
-handle({quic, peer_send_aborted, QStreamRef, ErrorCode}) ->
- {ok, StreamID} = quicer:get_stream_id(QStreamRef),
- {stream_peer_send_aborted, StreamID, ErrorCode};
-%% Clauses past this point copied from cowboy_quicer.
-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};
-%% 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;
-handle({quic, send_shutdown_complete, _StreamRef, _IsGraceful}) ->
- ok;
-handle({quic, shutdown, _Conn, success}) ->
- ok;
-handle(_Msg) ->
- unknown.
-
--endif.