From 6dd58a4ff9deedeeb6029827b936c2e81866cd54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Fri, 22 Sep 2023 16:28:00 +0200 Subject: Initial HTTP/3 implementation Since quicer, which provides the QUIC implementation, is a NIF, Gun cannot depend directly on it. In order to enable QUIC and HTTP/3, users have to set the GUN_QUICER environment variable: export GUN_QUICER=1 Gun is now tested using GitHub Actions. As a result OTP-24+ is now required. In addition, the number of OTP releases tested has been reduced; only the latest of each major version is now tested. This also updates Erlang.mk. --- src/gun_quicer.erl | 283 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 src/gun_quicer.erl (limited to 'src/gun_quicer.erl') diff --git a/src/gun_quicer.erl b/src/gun_quicer.erl new file mode 100644 index 0000000..f12d135 --- /dev/null +++ b/src/gun_quicer.erl @@ -0,0 +1,283 @@ +%% Copyright (c) 2023, Loïc Hoguin +%% +%% 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. -- cgit v1.2.3