%% Copyright (c) 2023, 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.