aboutsummaryrefslogtreecommitdiffstats
path: root/src/cowboy.erl
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2023-01-31 11:07:31 +0100
committerLoïc Hoguin <[email protected]>2024-03-26 15:53:48 +0100
commit8cb9d242b0a665cada6de8b9a9dfa329e0c06ee9 (patch)
treeae2c323c3825da367e54704ea0b9ad80096059c3 /src/cowboy.erl
parent3ea8395eb8f53a57acb5d3c00b99c70296e7cdbd (diff)
downloadcowboy-8cb9d242b0a665cada6de8b9a9dfa329e0c06ee9.tar.gz
cowboy-8cb9d242b0a665cada6de8b9a9dfa329e0c06ee9.tar.bz2
cowboy-8cb9d242b0a665cada6de8b9a9dfa329e0c06ee9.zip
Initial HTTP/3 implementationhttp3
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.
Diffstat (limited to 'src/cowboy.erl')
-rw-r--r--src/cowboy.erl83
1 files changed, 83 insertions, 0 deletions
diff --git a/src/cowboy.erl b/src/cowboy.erl
index ad45919..e5ed831 100644
--- a/src/cowboy.erl
+++ b/src/cowboy.erl
@@ -16,6 +16,7 @@
-export([start_clear/3]).
-export([start_tls/3]).
+-export([start_quic/3]).
-export([stop_listener/1]).
-export([get_env/2]).
-export([get_env/3]).
@@ -25,6 +26,9 @@
-export([log/2]).
-export([log/4]).
+%% Don't warn about the bad quicer specs.
+-dialyzer([{nowarn_function, start_quic/3}]).
+
-type opts() :: cowboy_http:opts() | cowboy_http2:opts().
-export_type([opts/0]).
@@ -44,6 +48,7 @@
-spec start_clear(ranch:ref(), ranch:opts(), opts())
-> {ok, pid()} | {error, any()}.
+
start_clear(Ref, TransOpts0, ProtoOpts0) ->
TransOpts1 = ranch:normalize_opts(TransOpts0),
{TransOpts, ConnectionType} = ensure_connection_type(TransOpts1),
@@ -52,6 +57,7 @@ start_clear(Ref, TransOpts0, ProtoOpts0) ->
-spec start_tls(ranch:ref(), ranch:opts(), opts())
-> {ok, pid()} | {error, any()}.
+
start_tls(Ref, TransOpts0, ProtoOpts0) ->
TransOpts1 = ranch:normalize_opts(TransOpts0),
SocketOpts = maps:get(socket_opts, TransOpts1, []),
@@ -62,28 +68,103 @@ start_tls(Ref, TransOpts0, ProtoOpts0) ->
ProtoOpts = ProtoOpts0#{connection_type => ConnectionType},
ranch:start_listener(Ref, ranch_ssl, TransOpts, cowboy_tls, ProtoOpts).
+%% @todo Experimental function to start a barebone QUIC listener.
+%% This will need to be reworked to be closer to Ranch
+%% listeners and provide equivalent features.
+%%
+%% @todo Better type for transport options. Might require fixing quicer types.
+
+-spec start_quic(ranch:ref(), #{socket_opts => [{atom(), _}]}, cowboy_http3:opts())
+ -> {ok, pid()}.
+
+start_quic(Ref, TransOpts, ProtoOpts) ->
+ {ok, _} = application:ensure_all_started(quicer),
+ Parent = self(),
+ SocketOpts0 = maps:get(socket_opts, TransOpts, []),
+ {Port, SocketOpts2} = case lists:keytake(port, 1, SocketOpts0) of
+ {value, {port, Port0}, SocketOpts1} ->
+ {Port0, SocketOpts1};
+ false ->
+ {port_0(), SocketOpts0}
+ end,
+ SocketOpts = [
+ {alpn, ["h3"]}, %% @todo Why not binary?
+ {peer_unidi_stream_count, 3}, %% We only need control and QPACK enc/dec.
+ {peer_bidi_stream_count, 100}
+ |SocketOpts2],
+ _ListenerPid = spawn(fun() ->
+ {ok, Listener} = quicer:listen(Port, SocketOpts),
+ Parent ! {ok, Listener},
+ _AcceptorPid = [spawn(fun AcceptLoop() ->
+ {ok, Conn} = quicer:accept(Listener, []),
+ Pid = spawn(fun() ->
+ receive go -> ok end,
+ %% We have to do the handshake after handing control of
+ %% the connection otherwise streams may come in before
+ %% the controlling process is changed and messages will
+ %% not be sent to the correct process.
+ {ok, Conn} = quicer:handshake(Conn),
+ process_flag(trap_exit, true), %% @todo Only if supervisor though.
+ try cowboy_http3:init(Parent, Ref, Conn, ProtoOpts)
+ catch
+ exit:{shutdown,_} -> ok;
+ C:E:S -> log(error, "CRASH ~p:~p:~p", [C,E,S], ProtoOpts)
+ end
+ end),
+ ok = quicer:controlling_process(Conn, Pid),
+ Pid ! go,
+ AcceptLoop()
+ end) || _ <- lists:seq(1, 20)],
+ %% Listener process must not terminate.
+ receive after infinity -> ok end
+ end),
+ receive
+ {ok, Listener} ->
+ {ok, Listener}
+ end.
+
+%% Select a random UDP port using gen_udp because quicer
+%% does not provide equivalent functionality. Taken from
+%% quicer test suites.
+port_0() ->
+ {ok, Socket} = gen_udp:open(0, [{reuseaddr, true}]),
+ {ok, {_, Port}} = inet:sockname(Socket),
+ gen_udp:close(Socket),
+ case os:type() of
+ {unix, darwin} ->
+ %% Apparently macOS doesn't free the port immediately.
+ timer:sleep(500);
+ _ ->
+ ok
+ end,
+ Port.
+
ensure_connection_type(TransOpts=#{connection_type := ConnectionType}) ->
{TransOpts, ConnectionType};
ensure_connection_type(TransOpts) ->
{TransOpts#{connection_type => supervisor}, supervisor}.
-spec stop_listener(ranch:ref()) -> ok | {error, not_found}.
+
stop_listener(Ref) ->
ranch:stop_listener(Ref).
-spec get_env(ranch:ref(), atom()) -> ok.
+
get_env(Ref, Name) ->
Opts = ranch:get_protocol_options(Ref),
Env = maps:get(env, Opts, #{}),
maps:get(Name, Env).
-spec get_env(ranch:ref(), atom(), any()) -> ok.
+
get_env(Ref, Name, Default) ->
Opts = ranch:get_protocol_options(Ref),
Env = maps:get(env, Opts, #{}),
maps:get(Name, Env, Default).
-spec set_env(ranch:ref(), atom(), any()) -> ok.
+
set_env(Ref, Name, Value) ->
Opts = ranch:get_protocol_options(Ref),
Env = maps:get(env, Opts, #{}),
@@ -93,10 +174,12 @@ set_env(Ref, Name, Value) ->
%% Internal.
-spec log({log, logger:level(), io:format(), list()}, opts()) -> ok.
+
log({log, Level, Format, Args}, Opts) ->
log(Level, Format, Args, Opts).
-spec log(logger:level(), io:format(), list(), opts()) -> ok.
+
log(Level, Format, Args, #{logger := Logger})
when Logger =/= error_logger ->
_ = Logger:Level(Format, Args),