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.

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 48dd455
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit 48dd455

Please sign in to comment.