Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 67 additions & 11 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ jobs:
if: ${{ !cancelled() }}
uses: ninenines/ci.erlang.mk/.github/workflows/ci.yaml@master

dialyzer-no-quicer:
name: Check / Dialyzer (without COWBOY_QUICER)
examples:
name: Check examples
runs-on: ubuntu-latest
steps:

Expand All @@ -34,30 +34,86 @@ jobs:
with:
otp-version: '> 0'

- name: Run Dialyzer (without COWBOY_QUICER)
run: make dialyze COWBOY_QUICER=0
- name: Run ct-examples
run: make ct-examples

examples:
name: Check examples
- name: Upload logs
uses: actions/upload-artifact@v4
if: always()
with:
name: Common Test logs (examples)
path: |
logs/
!logs/**/log_private

http3-erlang-quic:
name: Check HTTP/3 with erlang_quic
runs-on: ubuntu-latest
steps:

- name: Checkout repository
uses: actions/checkout@v4

- name: Install latest Erlang/OTP
- name: Install Erlang/OTP 27
uses: erlef/setup-beam@v1
with:
otp-version: '> 0'
otp-version: '27'

- name: Run ct-examples
run: make ct-examples
- name: Build
run: make

- name: Run erlang_quic HTTP/3 tests
run: make ct-h3 ct-rfc9114_quic ct-rfc9220_quic ct-webtransport_quic

- name: Upload logs
uses: actions/upload-artifact@v4
if: always()
with:
name: Common Test logs (examples)
name: Common Test logs (erlang_quic)
path: |
logs/
!logs/**/log_private

http3-quicer:
name: Check HTTP/3 with quicer
runs-on: ubuntu-latest
# Allow failure - quicer adapter needs updates for latest emqx/quic API
continue-on-error: true
steps:

- name: Checkout repository
uses: actions/checkout@v4

- name: Install Erlang/OTP 27
uses: erlef/setup-beam@v1
with:
otp-version: '27'

- name: Install MsQuic dependencies
run: |
sudo apt-get update
sudo apt-get install -y cmake build-essential

- name: Configure git for GitHub
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config --global url."https://github.com/".insteadOf git://github.com/
git config --global url."https://github.com/".insteadOf git@github.com:
git config --global credential.helper store
echo "https://x-access-token:${GITHUB_TOKEN}@github.com" > ~/.git-credentials

- name: Build with quicer
run: make COWBOY_QUICER=1

- name: Run quicer HTTP/3 tests
run: make COWBOY_QUICER=1 ct-rfc9114 ct-rfc9204 ct-rfc9220

- name: Upload logs
uses: actions/upload-artifact@v4
if: always()
with:
name: Common Test logs (quicer)
path: |
logs/
!logs/**/log_private
16 changes: 9 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,16 @@ DEPS = cowlib ranch
dep_cowlib = git https://github.com/ninenines/cowlib 2.16.0
dep_ranch = git https://github.com/ninenines/ranch 1.8.1

# Conditional QUIC backend.
# Use COWBOY_QUICER=1 for quicer (emqx/quic NIF), default is erlang_quic.
ifeq ($(COWBOY_QUICER),1)
DEPS += quicer
dep_quicer = git https://github.com/emqx/quic main
ERLC_OPTS += -D COWBOY_QUICER=1
TEST_ERLC_OPTS += -D COWBOY_QUICER=1
else
DEPS += quic
dep_quic = git https://github.com/benoitc/erlang_quic 0.10.2
endif

DOC_DEPS = asciideck
Expand All @@ -35,8 +42,8 @@ dep_gun = git https://github.com/ninenines/gun master
dep_ci.erlang.mk = git https://github.com/ninenines/ci.erlang.mk master
DEP_EARLY_PLUGINS = ci.erlang.mk

AUTO_CI_OTP ?= OTP-LATEST-24+
AUTO_CI_WINDOWS ?= OTP-LATEST-24+
AUTO_CI_OTP ?= OTP-LATEST-26+
AUTO_CI_WINDOWS ?= OTP-LATEST-26+

# Hex configuration.

Expand Down Expand Up @@ -76,11 +83,6 @@ endif
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
Expand Down
6 changes: 3 additions & 3 deletions ebin/cowboy.app
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{application, 'cowboy', [
{description, "Small, fast, modern HTTP server."},
{vsn, "2.14.2"},
{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','cowboy_webtransport']},
{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_quic','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','cowboy_webtransport']},
{registered, [cowboy_sup,cowboy_clock]},
{applications, [kernel,stdlib,crypto,cowlib,ranch]},
{applications, [kernel,stdlib,crypto,cowlib,ranch,quic]},
{optional_applications, []},
{mod, {cowboy_app, []}},
{mod, {'cowboy_app', []}},
{env, []}
]}.
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",{tag,"2.16.0"}}},{ranch,".*",{git,"https://github.com/ninenines/ranch",{tag,"1.8.1"}}}
{cowlib,".*",{git,"https://github.com/ninenines/cowlib",{tag,"2.16.0"}}},{ranch,".*",{git,"https://github.com/ninenines/ranch",{tag,"1.8.1"}}},{quic,".*",{git,"https://github.com/benoitc/erlang_quic",{branch,"main"}}}
]}.
{erl_opts, [debug_info,warn_export_vars,warn_shadow_vars,warn_obsolete_guard,warn_missing_spec,warn_untyped_record]}.
88 changes: 85 additions & 3 deletions src/cowboy.erl
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
-export([get_env/3]).
-export([set_env/3]).

-ifdef(COWBOY_QUICER).
-export([start_quic/3]).

-ifdef(COWBOY_QUICER).
%% Don't warn about the bad quicer specs.
-dialyzer([{nowarn_function, start_quic/3}]).
-endif.
Expand Down Expand Up @@ -76,8 +76,6 @@ start_tls(Ref, TransOpts0, ProtoOpts0) ->
},
ranch:start_listener(Ref, ranch_ssl, TransOpts, cowboy_tls, ProtoOpts).

-ifdef(COWBOY_QUICER).

%% @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.
Expand All @@ -87,6 +85,8 @@ start_tls(Ref, TransOpts0, ProtoOpts0) ->
-spec start_quic(ranch:ref(), #{socket_opts => [{atom(), _}]}, cowboy_http3:opts())
-> {ok, pid()}.

-ifdef(COWBOY_QUICER).
%% quicer (emqx/quicer NIF) implementation.
%% @todo Implement dynamic_buffer for HTTP/3 if/when it applies.
start_quic(Ref, TransOpts, ProtoOpts) ->
{ok, _} = application:ensure_all_started(quicer),
Expand Down Expand Up @@ -156,6 +156,88 @@ port_0() ->
end,
Port.

-else.
%% erlang_quic (pure Erlang) implementation.
start_quic(Ref, TransOpts, ProtoOpts) ->
{ok, _} = application:ensure_all_started(quic),
Parent = self(),
SocketOpts0 = maps:get(socket_opts, TransOpts, []),
{Port, SocketOpts2} = case lists:keytake(port, 1, SocketOpts0) of
{value, {port, Port0}, SocketOpts1} ->
{Port0, SocketOpts1};
false ->
{0, SocketOpts0}
end,
%% Extract certificate configuration.
{Cert, CertChain, Key, SocketOpts} = extract_cert_opts(SocketOpts2),
%% Build server options.
ServerOpts = maps:merge(maps:from_list(SocketOpts), #{
cert => Cert,
cert_chain => CertChain,
key => Key,
alpn => [<<"h3">>],
connection_handler => fun(_ConnPid, ConnRef) ->
Pid = spawn(fun() ->
receive go -> ok end,
%% Wait for handshake to complete.
receive
{quic, ConnRef, {connected, _Info}} ->
ok
after 30000 ->
exit({shutdown, handshake_timeout})
end,
process_flag(trap_exit, true),
try cowboy_http3:init(Parent, Ref, ConnRef, ProtoOpts)
catch
exit:{shutdown,_} -> ok;
C:E:S ->
log(error, "CRASH ~p:~p:~p", [C,E,S], ProtoOpts)
end
end),
%% Ownership will be transferred by the listener after this returns.
Pid ! go,
{ok, Pid}
end
}),
quic:start_server(Ref, Port, ServerOpts).

%% Extract certificate options from socket opts.
extract_cert_opts(SocketOpts) ->
{Certfile, SocketOpts1} = case lists:keytake(certfile, 1, SocketOpts) of
{value, {certfile, CF}, SO1} -> {CF, SO1};
false -> {undefined, SocketOpts}
end,
{Keyfile, SocketOpts2} = case lists:keytake(keyfile, 1, SocketOpts1) of
{value, {keyfile, KF}, SO2} -> {KF, SO2};
false -> {undefined, SocketOpts1}
end,
%% Read certificate and key from files.
{Cert, CertChain} = case Certfile of
undefined -> {undefined, []};
_ -> read_cert_file(Certfile)
end,
Key = case Keyfile of
undefined -> undefined;
_ -> read_key_file(Keyfile)
end,
{Cert, CertChain, Key, SocketOpts2}.

%% Read certificate file (PEM) and convert to DER.
read_cert_file(Filename) ->
{ok, PemBin} = file:read_file(Filename),
PemEntries = public_key:pem_decode(PemBin),
Certs = [Der || {'Certificate', Der, not_encrypted} <- PemEntries],
case Certs of
[Cert|Chain] -> {Cert, Chain};
[] -> {undefined, []}
end.

%% Read key file (PEM) and return the key.
read_key_file(Filename) ->
{ok, PemBin} = file:read_file(Filename),
[PemEntry|_] = public_key:pem_decode(PemBin),
public_key:pem_entry_decode(PemEntry).

-endif.

ensure_alpn(TransOpts) ->
Expand Down
Loading