aboutsummaryrefslogtreecommitdiffstats
path: root/src/cowboy_quicer.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/cowboy_quicer.erl')
-rw-r--r--src/cowboy_quicer.erl231
1 files changed, 231 insertions, 0 deletions
diff --git a/src/cowboy_quicer.erl b/src/cowboy_quicer.erl
new file mode 100644
index 0000000..d9bbe1f
--- /dev/null
+++ b/src/cowboy_quicer.erl
@@ -0,0 +1,231 @@
+%% 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.
+
+%% QUIC transport using the emqx/quicer NIF.
+
+-module(cowboy_quicer).
+
+%% Connection.
+-export([peername/1]).
+-export([sockname/1]).
+-export([peercert/1]).
+-export([shutdown/2]).
+
+%% Streams.
+-export([start_unidi_stream/2]).
+-export([send/3]).
+-export([send/4]).
+-export([shutdown_stream/4]).
+
+%% Messages.
+-export([handle/1]).
+
+-ifndef(COWBOY_QUICER).
+
+-spec peername(_) -> no_return().
+peername(_) -> no_quicer().
+
+-spec sockname(_) -> no_return().
+sockname(_) -> no_quicer().
+
+-spec peercert(_) -> no_return().
+peercert(_) -> no_quicer().
+
+-spec shutdown(_, _) -> no_return().
+shutdown(_, _) -> 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().
+
+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 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_unidi_stream(quicer_connection_handle(), iodata())
+ -> {ok, cow_http3:stream_id()}
+ | {error, any()}.
+
+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, 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 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.
+
+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()}
+ | {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, 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.