Skip to content

Commit

Permalink
Initial HTTP/3 implementation
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
essen committed Mar 26, 2024
1 parent 3ea8395 commit 8cb9d24
Show file tree
Hide file tree
Showing 39 changed files with 5,130 additions and 238 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yaml
Expand Up @@ -4,6 +4,8 @@ name: Check Cowboy

on:
push:
branches:
- master
pull_request:
schedule:
## Every Monday at 2am.
Expand Down
23 changes: 22 additions & 1 deletion Makefile
Expand Up @@ -15,9 +15,14 @@ CT_OPTS += -ct_hooks cowboy_ct_hook [] # -boot start_sasl
LOCAL_DEPS = crypto

DEPS = cowlib ranch
dep_cowlib = git https://github.com/ninenines/cowlib 2.13.0
dep_cowlib = git https://github.com/ninenines/cowlib master
dep_ranch = git https://github.com/ninenines/ranch 1.8.0

ifeq ($(COWBOY_QUICER),1)
DEPS += quicer
dep_quicer = git https://github.com/emqx/quic main
endif

DOC_DEPS = asciideck

TEST_DEPS = $(if $(CI_ERLANG_MK),ci.erlang.mk) ct_helper gun
Expand Down Expand Up @@ -56,15 +61,31 @@ ifndef FULL
CT_SUITES := $(filter-out examples ws_autobahn,$(CT_SUITES))
endif

# Don't run HTTP/3 test suites on Windows.

ifeq ($(PLATFORM),msys2)
CT_SUITES := $(filter-out rfc9114 rfc9204 rfc9220,$(CT_SUITES))
endif

# Compile options.

ERLC_OPTS += +warn_missing_spec +warn_untyped_record # +bin_opt_info
TEST_ERLC_OPTS += +'{parse_transform, eunit_autoexport}'

ifeq ($(COWBOY_QUICER),1)
ERLC_OPTS += -D COWBOY_QUICER=1
TEST_ERLC_OPTS += -D COWBOY_QUICER=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

# Dialyze the tests.

#DIALYZER_OPTS += --src -r test
Expand Down
2 changes: 1 addition & 1 deletion ebin/cowboy.app
@@ -1,7 +1,7 @@
{application, 'cowboy', [
{description, "Small, fast, modern HTTP server."},
{vsn, "2.12.0"},
{modules, ['cowboy','cowboy_app','cowboy_bstr','cowboy_children','cowboy_clear','cowboy_clock','cowboy_compress_h','cowboy_constraints','cowboy_decompress_h','cowboy_handler','cowboy_http','cowboy_http2','cowboy_loop','cowboy_metrics_h','cowboy_middleware','cowboy_req','cowboy_rest','cowboy_router','cowboy_static','cowboy_stream','cowboy_stream_h','cowboy_sub_protocol','cowboy_sup','cowboy_tls','cowboy_tracer_h','cowboy_websocket']},
{modules, ['cowboy','cowboy_app','cowboy_bstr','cowboy_children','cowboy_clear','cowboy_clock','cowboy_compress_h','cowboy_constraints','cowboy_decompress_h','cowboy_handler','cowboy_http','cowboy_http2','cowboy_http3','cowboy_loop','cowboy_metrics_h','cowboy_middleware','cowboy_quicer','cowboy_req','cowboy_rest','cowboy_router','cowboy_static','cowboy_stream','cowboy_stream_h','cowboy_sub_protocol','cowboy_sup','cowboy_tls','cowboy_tracer_h','cowboy_websocket']},
{registered, [cowboy_sup,cowboy_clock]},
{applications, [kernel,stdlib,crypto,cowlib,ranch]},
{optional_applications, []},
Expand Down
4 changes: 2 additions & 2 deletions erlang.mk
Expand Up @@ -17,7 +17,7 @@
ERLANG_MK_FILENAME := $(realpath $(lastword $(MAKEFILE_LIST)))
export ERLANG_MK_FILENAME

ERLANG_MK_VERSION = 61f58ff
ERLANG_MK_VERSION = 16d60fa
ERLANG_MK_WITHOUT =

# Make 3.81 and 3.82 are deprecated.
Expand Down Expand Up @@ -3565,7 +3565,7 @@ REBAR_DEPS_DIR = $(DEPS_DIR)
export REBAR_DEPS_DIR

REBAR3_GIT ?= https://github.com/erlang/rebar3
REBAR3_COMMIT ?= 3f563feaf1091a1980241adefa83a32dd2eebf7c # 3.20.0
REBAR3_COMMIT ?= 06aaecd51b0ce828b66bb65a74d3c1fd7833a4ba # 3.22.1 + OTP-27 fixes

CACHE_DEPS ?= 0

Expand Down
2 changes: 1 addition & 1 deletion rebar.config
@@ -1,4 +1,4 @@
{deps, [
{cowlib,".*",{git,"https://github.com/ninenines/cowlib","2.13.0"}},{ranch,".*",{git,"https://github.com/ninenines/ranch","1.8.0"}}
{cowlib,".*",{git,"https://github.com/ninenines/cowlib","master"}},{ranch,".*",{git,"https://github.com/ninenines/ranch","1.8.0"}}
]}.
{erl_opts, [debug_info,warn_export_vars,warn_shadow_vars,warn_obsolete_guard,warn_missing_spec,warn_untyped_record]}.
83 changes: 83 additions & 0 deletions src/cowboy.erl
Expand Up @@ -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]).
Expand All @@ -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]).

Expand All @@ -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),
Expand All @@ -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, []),
Expand All @@ -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, #{}),
Expand All @@ -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),
Expand Down
4 changes: 3 additions & 1 deletion src/cowboy_http.erl
Expand Up @@ -12,6 +12,8 @@
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

%% @todo Worth renaming to cowboy_http1.
%% @todo Change use of cow_http to cow_http1 where appropriate.
-module(cowboy_http).

-export([init/6]).
Expand Down Expand Up @@ -1531,7 +1533,7 @@ maybe_socket_error(_, Result = {ok, _}, _) ->
maybe_socket_error(State, {error, Reason}, Human) ->
terminate(State, {socket_error, Reason, Human}).

-spec terminate(_, _) -> no_return().
-spec terminate(#state{} | undefined, _) -> no_return().
terminate(undefined, Reason) ->
exit({shutdown, Reason});
terminate(State=#state{streams=Streams, children=Children}, Reason) ->
Expand Down
2 changes: 1 addition & 1 deletion src/cowboy_http2.erl
Expand Up @@ -1139,7 +1139,7 @@ maybe_socket_error(_, Result = {ok, _}, _) ->
maybe_socket_error(State, {error, Reason}, Human) ->
terminate(State, {socket_error, Reason, Human}).

-spec terminate(#state{}, _) -> no_return().
-spec terminate(#state{} | undefined, _) -> no_return().
terminate(undefined, Reason) ->
exit({shutdown, Reason});
terminate(State=#state{socket=Socket, transport=Transport, http2_status=Status,
Expand Down

0 comments on commit 8cb9d24

Please sign in to comment.