diff options
| author | Loïc Hoguin <[email protected]> | 2026-05-06 16:09:36 +0200 |
|---|---|---|
| committer | Loïc Hoguin <[email protected]> | 2026-05-06 16:09:36 +0200 |
| commit | 4ef0813e7c4f0fbbf7b778a1c852d65f30688d96 (patch) | |
| tree | 5a1430b9030e060115b7a4c51cbe58d9edfac3c3 | |
| parent | 514bcb2e90ddc3e1fc6ca47bc8c34d81daa2ff4f (diff) | |
| download | gun-corral.tar.gz gun-corral.tar.bz2 gun-corral.zip | |
WIPcorral
| -rw-r--r-- | Makefile | 19 | ||||
| -rw-r--r-- | ebin/gun.app | 4 | ||||
| -rw-r--r-- | src/gun.erl | 8 | ||||
| -rw-r--r-- | src/gun_http3.erl | 50 | ||||
| -rw-r--r-- | src/gun_quic.erl | 245 | ||||
| -rw-r--r-- | src/gun_quicer.erl | 283 |
6 files changed, 283 insertions, 326 deletions
@@ -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. |
