From 8cb9d242b0a665cada6de8b9a9dfa329e0c06ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Tue, 31 Jan 2023 11:07:31 +0100 Subject: Initial HTTP/3 implementation This includes Websocket over HTTP/3. Since quicer, which provides the QUIC implementation, is a NIF, Cowboy cannot depend directly on it. In order to enable QUIC and HTTP/3, users have to set the COWBOY_QUICER environment variable: export COWBOY_QUICER=1 In order to run the test suites, the same must be done for Gun: export GUN_QUICER=1 HTTP/3 support is currently not available on Windows due to compilation issues of quicer which have yet to be looked at or resolved. HTTP/3 support is also unavailable on the upcoming OTP-27 due to compilation errors in quicer dependencies. Once resolved HTTP/3 should work on OTP-27. Because of how QUIC currently works, it's possible that streams that get reset after sending a response do not receive that response. The test suite was modified to accomodate for that. A future extension to QUIC will allow us to gracefully reset streams. This also updates Erlang.mk. --- src/cowboy_quicer.erl | 231 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 src/cowboy_quicer.erl (limited to 'src/cowboy_quicer.erl') 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 +%% +%% 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. -- cgit v1.2.3