Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP/3 #319

Merged
merged 1 commit into from
Mar 26, 2024
Merged
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
36 changes: 36 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
## Use workflows from ninenines/ci.erlang.mk to test Gun.

name: Check Gun

on:
push:
branches:
- master
pull_request:
schedule:
## Every Monday at 2am.
- cron: 0 2 * * 1

env:
CI_ERLANG_MK: 1

jobs:
cleanup-master:
name: Cleanup master build
runs-on: ubuntu-latest
steps:

- name: Cleanup master build if necessary
if: ${{ github.event_name == 'schedule' }}
run: |
gh extension install actions/gh-actions-cache
gh actions-cache delete Linux-X64-Erlang-master -R $REPO --confirm || true
gh actions-cache delete macOS-X64-Erlang-master -R $REPO --confirm || true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}

check:
name: Gun
needs: cleanup-master
uses: ninenines/ci.erlang.mk/.github/workflows/ci.yaml@master
25 changes: 19 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ 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.13.0
dep_cowlib = git https://github.com/ninenines/cowlib master

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

DOC_DEPS = asciideck

Expand All @@ -29,10 +34,8 @@ dep_ranch_commit = 2.0.0
dep_ci.erlang.mk = git https://github.com/ninenines/ci.erlang.mk master
DEP_EARLY_PLUGINS = ci.erlang.mk

AUTO_CI_OTP ?= OTP-22+
#AUTO_CI_HIPE ?= OTP-LATEST
# AUTO_CI_ERLLVM ?= OTP-LATEST
AUTO_CI_WINDOWS ?= OTP-22+
AUTO_CI_OTP ?= OTP-LATEST-24+
AUTO_CI_WINDOWS ?= OTP-LATEST-24+

# Hex configuration.

Expand All @@ -58,14 +61,24 @@ ifndef FULL
CT_SUITES := $(filter-out ws_autobahn,$(CT_SUITES))
endif

# Enable eunit.
# Compile options.

TEST_ERLC_OPTS += +'{parse_transform, eunit_autoexport}'

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

# h2specd setup.

GOPATH := $(ERLANG_MK_TMP)/gopath
Expand Down
2 changes: 1 addition & 1 deletion ebin/gun.app
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{application, 'gun', [
{description, "HTTP/1.1, HTTP/2 and Websocket client for Erlang/OTP."},
{vsn, "2.1.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_pool','gun_pool_events_h','gun_pools_sup','gun_protocols','gun_public_suffix','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_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']},
{registered, [gun_sup]},
{applications, [kernel,stdlib,public_key,ssl,cowlib]},
{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"}}
{cowlib,".*",{git,"https://github.com/ninenines/cowlib","master"}}
]}.
{erl_opts, [debug_info,warn_export_vars,warn_shadow_vars,warn_obsolete_guard]}.
84 changes: 72 additions & 12 deletions src/gun.erl
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,9 @@
| {close, ws_close_code(), iodata()}.
-export_type([ws_frame/0]).

-type protocol() :: http | http2 | raw | socks
| {http, http_opts()} | {http2, http2_opts()} | {raw, raw_opts()} | {socks, socks_opts()}.
-type protocol() :: http | http2 | http3 | raw | socks
| {http, http_opts()} | {http2, http2_opts()} | {http3, http3_opts()}
| {raw, raw_opts()} | {socks, socks_opts()}.
-export_type([protocol/0]).

-type protocols() :: [protocol()].
Expand All @@ -141,6 +142,7 @@
event_handler => {module(), any()},
http_opts => http_opts(),
http2_opts => http2_opts(),
http3_opts => http3_opts(),
protocols => protocols(),
raw_opts => raw_opts(),
retry => non_neg_integer(),
Expand All @@ -153,7 +155,7 @@
tls_handshake_timeout => timeout(),
tls_opts => [ssl:tls_client_option()],
trace => boolean(),
transport => tcp | tls | ssl,
transport => tcp | tls | ssl | quic,
ws_opts => ws_opts()
}.
-export_type([opts/0]).
Expand Down Expand Up @@ -252,6 +254,11 @@
}.
-export_type([http2_opts/0]).

%% @todo
-type http3_opts() :: #{
}.
-export_type([http3_opts/0]).

-type socks_opts() :: #{
version => 5,
auth => [{username_password, binary(), binary()} | none],
Expand Down Expand Up @@ -391,6 +398,11 @@ check_options([{http2_opts, ProtoOpts}|Opts]) when is_map(ProtoOpts) ->
Error ->
Error
end;
check_options([{http3_opts, ProtoOpts}|Opts]) when is_map(ProtoOpts) ->
case gun_http3:check_options(ProtoOpts) of
ok ->
check_options(Opts)
end;
check_options([Opt = {protocols, L}|Opts]) when is_list(L) ->
case check_protocols_opt(L) of
ok -> check_options(Opts);
Expand Down Expand Up @@ -428,7 +440,7 @@ check_options([{tls_opts, L}|Opts]) when is_list(L) ->
check_options(Opts);
check_options([{trace, B}|Opts]) when is_boolean(B) ->
check_options(Opts);
check_options([{transport, T}|Opts]) when T =:= tcp; T =:= tls ->
check_options([{transport, T}|Opts]) when T =:= tcp; T =:= tls; T =:= quic ->
check_options(Opts);
check_options([{ws_opts, ProtoOpts}|Opts]) when is_map(ProtoOpts) ->
case gun_ws:check_options(ProtoOpts) of
Expand All @@ -442,9 +454,9 @@ check_options([Opt|_]) ->

check_protocols_opt(Protocols) ->
%% Protocols must not appear more than once, and they
%% must be one of http, http2 or socks.
%% must be one of http, http2, http3, raw or socks.
ProtoNames0 = lists:usort([case P0 of {P, _} -> P; P -> P end || P0 <- Protocols]),
ProtoNames = [P || P <- ProtoNames0, lists:member(P, [http, http2, raw, socks])],
ProtoNames = [P || P <- ProtoNames0, lists:member(P, [http, http2, http3, raw, socks])],
case length(Protocols) =:= length(ProtoNames) of
false -> error;
true ->
Expand All @@ -453,6 +465,7 @@ check_protocols_opt(Protocols) ->
TupleCheck = [case P of
{http, Opts} -> gun_http:check_options(Opts);
{http2, Opts} -> gun_http2:check_options(Opts);
{http3, Opts} -> gun_http3:check_options(Opts);
{raw, Opts} -> gun_raw:check_options(Opts);
{socks, Opts} -> gun_socks:check_options(Opts)
end || P <- Protocols, is_tuple(P)],
Expand All @@ -468,6 +481,7 @@ consider_tracing(ServerPid, #{trace := true}) ->
_ = dbg:tpl(gun, [{'_', [], [{return_trace}]}]),
_ = dbg:tpl(gun_http, [{'_', [], [{return_trace}]}]),
_ = dbg:tpl(gun_http2, [{'_', [], [{return_trace}]}]),
_ = dbg:tpl(gun_http3, [{'_', [], [{return_trace}]}]),
_ = dbg:tpl(gun_raw, [{'_', [], [{return_trace}]}]),
_ = dbg:tpl(gun_socks, [{'_', [], [{return_trace}]}]),
_ = dbg:tpl(gun_ws, [{'_', [], [{return_trace}]}]),
Expand Down Expand Up @@ -495,6 +509,7 @@ info(ServerPid) ->
Info0 = #{
owner => Owner,
socket => Socket,
%% @todo This is no longer correct for https because of QUIC.
transport => case OriginScheme of
<<"http">> -> tcp;
<<"https">> -> tls
Expand Down Expand Up @@ -818,7 +833,7 @@ await_body(ServerPid, StreamRef, Timeout, MRef, Acc) ->
end.

-spec await_up(pid())
-> {ok, http | http2 | raw | socks}
-> {ok, http | http2 | http3 | raw | socks}
| {error, {down, any()} | timeout}.
await_up(ServerPid) ->
MRef = monitor(process, ServerPid),
Expand All @@ -827,7 +842,7 @@ await_up(ServerPid) ->
Res.

-spec await_up(pid(), reference() | timeout())
-> {ok, http | http2 | raw | socks}
-> {ok, http | http2 | http3 | raw | socks}
| {error, {down, any()} | timeout}.
await_up(ServerPid, MRef) when is_reference(MRef) ->
await_up(ServerPid, 5000, MRef);
Expand All @@ -838,7 +853,7 @@ await_up(ServerPid, Timeout) ->
Res.

-spec await_up(pid(), timeout(), reference())
-> {ok, http | http2 | raw | socks}
-> {ok, http | http2 | http3 | raw | socks}
| {error, {down, any()} | timeout}.
await_up(ServerPid, Timeout, MRef) ->
receive
Expand Down Expand Up @@ -974,7 +989,8 @@ init({Owner, Host, Port, Opts}) ->
%% This is corrected in the gun:info/1 and gun:stream_info/2 functions where applicable.
{OriginScheme, Transport} = case OriginTransport of
tcp -> {<<"http">>, gun_tcp};
tls -> {<<"https">>, gun_tls}
tls -> {<<"https">>, gun_tls};
quic -> {<<"https">>, gun_quicer}
end,
OwnerRef = monitor(process, Owner),
{EvHandler, EvHandlerState0} = maps:get(event_handler, Opts,
Expand Down Expand Up @@ -1061,6 +1077,38 @@ domain_lookup({call, From}, {stream_info, _}, _) ->
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}) ->
%% @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),
ConnectEvent = #{
lookup_info => LookupInfo,
timeout => ConnectTimeout
},
EvHandlerState1 = EvHandler:connect_start(ConnectEvent, EvHandlerState0),
case gun_quicer:connect(LookupInfo, ConnectTimeout) of
{ok, Socket} ->
%% @todo We should double check the ALPN result.
[Protocol] = maps:get(protocols, Opts, [http3]),
ProtocolName = case Protocol of
{P, _} -> P;
P -> P
end,
EvHandlerState = EvHandler:connect_end(ConnectEvent#{
socket => Socket,
protocol => ProtocolName
}, EvHandlerState1),
{next_state, connected_protocol_init,
State#state{event_handler_state=EvHandlerState},
{next_event, internal, {connected, Retries, Socket, Protocol}}};
{error, Reason} ->
EvHandlerState = EvHandler:connect_end(ConnectEvent#{
error => Reason
}, EvHandlerState1),
{next_state, not_connected, State#state{event_handler_state=EvHandlerState},
{next_event, internal, {retries, Retries, Reason}}}
end;
connecting(_, {retries, Retries, LookupInfo}, State=#state{opts=Opts,
transport=Transport, event_handler=EvHandler, event_handler_state=EvHandlerState0}) ->
ConnectTimeout = maps:get(connect_timeout, Opts, infinity),
Expand Down Expand Up @@ -1100,6 +1148,7 @@ connecting(_, {retries, Retries, LookupInfo}, State=#state{opts=Opts,
initial_tls_handshake(_, {retries, Retries, Socket}, State0=#state{opts=Opts, origin_host=OriginHost}) ->
Protocols = maps:get(protocols, Opts, [http2, http]),
HandshakeEvent = #{
%% @todo This results in ensure_tls_opts being called twice.
tls_opts => ensure_tls_opts(Protocols, maps:get(tls_opts, Opts, []), OriginHost),
timeout => maps:get(tls_handshake_timeout, Opts, infinity)
},
Expand Down Expand Up @@ -1453,13 +1502,22 @@ handle_common_connected(Type, Event, StateName, StateData) ->
handle_common_connected_no_input(Type, Event, StateName, StateData).

%% Socket events.
handle_common_connected_no_input(info, Msg, _, State=#state{
protocol=Protocol=gun_http3, protocol_state=ProtoState, cookie_store=CookieStore0,
event_handler=EvHandler, event_handler_state=EvHandlerState0})
when element(1, Msg) =:= quic ->
% ct:pal("~p", [Msg]),
{Commands, CookieStore, EvHandlerState} = Protocol:handle(Msg,
ProtoState, CookieStore0, EvHandler, EvHandlerState0),
maybe_active(commands(Commands, State#state{cookie_store=CookieStore,
event_handler_state=EvHandlerState}));
handle_common_connected_no_input(info, {OK, Socket, Data}, _,
State0=#state{socket=Socket, messages={OK, _, _},
State=#state{socket=Socket, messages={OK, _, _},
protocol=Protocol, protocol_state=ProtoState, cookie_store=CookieStore0,
event_handler=EvHandler, event_handler_state=EvHandlerState0}) ->
{Commands, CookieStore, EvHandlerState} = Protocol:handle(Data,
ProtoState, CookieStore0, EvHandler, EvHandlerState0),
maybe_active(commands(Commands, State0#state{cookie_store=CookieStore,
maybe_active(commands(Commands, State#state{cookie_store=CookieStore,
event_handler_state=EvHandlerState}));
handle_common_connected_no_input(info, {Closed, Socket}, _,
State=#state{socket=Socket, messages={_, Closed, _}}) ->
Expand Down Expand Up @@ -1575,6 +1633,8 @@ maybe_active(Other) ->

active(State=#state{active=false}) ->
{ok, State};
active(State=#state{transport=gun_quicer}) ->
{ok, State};
active(State=#state{socket=Socket, transport=Transport}) ->
case Transport:setopts(Socket, [{active, once}]) of
ok ->
Expand Down
2 changes: 1 addition & 1 deletion src/gun_http.erl
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
socket :: inet:socket() | ssl:sslsocket(),
transport :: module(),
opts = #{} :: gun:http_opts(),
version = 'HTTP/1.1' :: cow_http:version(),
version = 'HTTP/1.1' :: cow_http1:version(),
connection = keepalive :: keepalive | close,
buffer = <<>> :: binary(),

Expand Down
4 changes: 3 additions & 1 deletion src/gun_http2.erl
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
%% by the client or by the server through PUSH_PROMISE frames.
%%
%% Streams can be found by ID or by Ref. The most common should be
%% the idea, that's why the main map has the ID as key. Then we also
%% the ID, that's why the main map has the ID as key. Then we also
%% have a Ref->ID index for faster lookup when we only have the Ref.
streams = #{} :: #{cow_http2:streamid() => #stream{}},
stream_refs = #{} :: #{reference() => cow_http2:streamid()},
Expand Down Expand Up @@ -1074,6 +1074,8 @@ prepare_headers(State=#http2_state{transport=Transport},
end,
%% @todo We also must remove any header found in the connection header.
%% @todo Much of this is duplicated in cow_http2_machine; sort things out.
%% I think we want to do this before triggering events, not when
%% building HeaderBlock.
Headers1 =
lists:keydelete(<<"host">>, 1,
lists:keydelete(<<"connection">>, 1,
Expand Down
Loading
Loading