aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/cowboy.erl100
-rw-r--r--src/cowboy_app.erl2
-rw-r--r--src/cowboy_bstr.erl2
-rw-r--r--src/cowboy_children.erl2
-rw-r--r--src/cowboy_clear.erl18
-rw-r--r--src/cowboy_clock.erl2
-rw-r--r--src/cowboy_compress_h.erl42
-rw-r--r--src/cowboy_constraints.erl2
-rw-r--r--src/cowboy_decompress_h.erl257
-rw-r--r--src/cowboy_handler.erl2
-rw-r--r--src/cowboy_http.erl300
-rw-r--r--src/cowboy_http2.erl295
-rw-r--r--src/cowboy_http3.erl973
-rw-r--r--src/cowboy_loop.erl45
-rw-r--r--src/cowboy_metrics_h.erl2
-rw-r--r--src/cowboy_middleware.erl2
-rw-r--r--src/cowboy_quicer.erl231
-rw-r--r--src/cowboy_req.erl70
-rw-r--r--src/cowboy_rest.erl11
-rw-r--r--src/cowboy_router.erl2
-rw-r--r--src/cowboy_static.erl2
-rw-r--r--src/cowboy_stream.erl2
-rw-r--r--src/cowboy_stream_h.erl7
-rw-r--r--src/cowboy_sub_protocol.erl2
-rw-r--r--src/cowboy_sup.erl2
-rw-r--r--src/cowboy_tls.erl18
-rw-r--r--src/cowboy_tracer_h.erl2
-rw-r--r--src/cowboy_websocket.erl20
28 files changed, 2136 insertions, 279 deletions
diff --git a/src/cowboy.erl b/src/cowboy.erl
index c4be25b..e5ed831 100644
--- a/src/cowboy.erl
+++ b/src/cowboy.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -16,13 +16,19 @@
-export([start_clear/3]).
-export([start_tls/3]).
+-export([start_quic/3]).
-export([stop_listener/1]).
+-export([get_env/2]).
+-export([get_env/3]).
-export([set_env/3]).
%% Internal.
-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]).
@@ -42,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),
@@ -50,27 +57,114 @@ 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, []),
TransOpts2 = TransOpts1#{socket_opts => [
- {next_protocols_advertised, [<<"h2">>, <<"http/1.1">>]},
{alpn_preferred_protocols, [<<"h2">>, <<"http/1.1">>]}
|SocketOpts]},
{TransOpts, ConnectionType} = ensure_connection_type(TransOpts2),
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, #{}),
@@ -80,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),
diff --git a/src/cowboy_app.erl b/src/cowboy_app.erl
index 74cba41..95ae564 100644
--- a/src/cowboy_app.erl
+++ b/src/cowboy_app.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_bstr.erl b/src/cowboy_bstr.erl
index d8041e4..f23167d 100644
--- a/src/cowboy_bstr.erl
+++ b/src/cowboy_bstr.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_children.erl b/src/cowboy_children.erl
index 05d39fb..305c989 100644
--- a/src/cowboy_children.erl
+++ b/src/cowboy_children.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_clear.erl b/src/cowboy_clear.erl
index 4f3a234..eaeab74 100644
--- a/src/cowboy_clear.erl
+++ b/src/cowboy_clear.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2016-2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2016-2024, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -33,13 +33,7 @@ start_link(Ref, Transport, Opts) ->
-spec connection_process(pid(), ranch:ref(), module(), cowboy:opts()) -> ok.
connection_process(Parent, Ref, Transport, Opts) ->
- ProxyInfo = case maps:get(proxy_header, Opts, false) of
- true ->
- {ok, ProxyInfo0} = ranch:recv_proxy_header(Ref, 1000),
- ProxyInfo0;
- false ->
- undefined
- end,
+ ProxyInfo = get_proxy_info(Ref, Opts),
{ok, Socket} = ranch:handshake(Ref),
%% Use cowboy_http2 directly only when 'http' is missing.
%% Otherwise switch to cowboy_http2 from cowboy_http.
@@ -58,3 +52,11 @@ init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, Protocol) ->
supervisor -> process_flag(trap_exit, true)
end,
Protocol:init(Parent, Ref, Socket, Transport, ProxyInfo, Opts).
+
+get_proxy_info(Ref, #{proxy_header := true}) ->
+ case ranch:recv_proxy_header(Ref, 1000) of
+ {ok, ProxyInfo} -> ProxyInfo;
+ {error, closed} -> exit({shutdown, closed})
+ end;
+get_proxy_info(_, _) ->
+ undefined.
diff --git a/src/cowboy_clock.erl b/src/cowboy_clock.erl
index 28f8a1b..e2cdf62 100644
--- a/src/cowboy_clock.erl
+++ b/src/cowboy_clock.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_compress_h.erl b/src/cowboy_compress_h.erl
index 374cb6a..338ea9f 100644
--- a/src/cowboy_compress_h.erl
+++ b/src/cowboy_compress_h.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -96,11 +96,14 @@ check_req(Req) ->
%% Do not compress responses that contain the content-encoding header.
check_resp_headers(#{<<"content-encoding">> := _}, State) ->
State#state{compress=undefined};
+%% Do not compress responses that contain the etag header.
+check_resp_headers(#{<<"etag">> := _}, State) ->
+ State#state{compress=undefined};
check_resp_headers(_, State) ->
State.
fold(Commands, State=#state{compress=undefined}) ->
- {Commands, State};
+ fold_vary_only(Commands, State, []);
fold(Commands, State) ->
fold(Commands, State, []).
@@ -108,32 +111,32 @@ fold([], State, Acc) ->
{lists:reverse(Acc), State};
%% We do not compress full sendfile bodies.
fold([Response={response, _, _, {sendfile, _, _, _}}|Tail], State, Acc) ->
- fold(Tail, State, [Response|Acc]);
+ fold(Tail, State, [vary_response(Response)|Acc]);
%% We compress full responses directly, unless they are lower than
%% the configured threshold or we find we are not able to by looking at the headers.
fold([Response0={response, _, Headers, Body}|Tail],
State0=#state{threshold=CompressThreshold}, Acc) ->
case check_resp_headers(Headers, State0) of
State=#state{compress=undefined} ->
- fold(Tail, State, [Response0|Acc]);
+ fold(Tail, State, [vary_response(Response0)|Acc]);
State1 ->
BodyLength = iolist_size(Body),
if
BodyLength =< CompressThreshold ->
- fold(Tail, State1, [Response0|Acc]);
+ fold(Tail, State1, [vary_response(Response0)|Acc]);
true ->
{Response, State} = gzip_response(Response0, State1),
- fold(Tail, State, [Response|Acc])
+ fold(Tail, State, [vary_response(Response)|Acc])
end
end;
%% Check headers and initiate compression...
fold([Response0={headers, _, Headers}|Tail], State0, Acc) ->
case check_resp_headers(Headers, State0) of
State=#state{compress=undefined} ->
- fold(Tail, State, [Response0|Acc]);
+ fold(Tail, State, [vary_headers(Response0)|Acc]);
State1 ->
{Response, State} = gzip_headers(Response0, State1),
- fold(Tail, State, [Response|Acc])
+ fold(Tail, State, [vary_headers(Response)|Acc])
end;
%% then compress each data commands individually.
fold([Data0={data, _, _}|Tail], State0=#state{compress=gzip}, Acc) ->
@@ -161,6 +164,15 @@ fold([SetOptions={set_options, Opts}|Tail], State=#state{
fold([Command|Tail], State, Acc) ->
fold(Tail, State, [Command|Acc]).
+fold_vary_only([], State, Acc) ->
+ {lists:reverse(Acc), State};
+fold_vary_only([Response={response, _, _, _}|Tail], State, Acc) ->
+ fold_vary_only(Tail, State, [vary_response(Response)|Acc]);
+fold_vary_only([Response={headers, _, _}|Tail], State, Acc) ->
+ fold_vary_only(Tail, State, [vary_headers(Response)|Acc]);
+fold_vary_only([Command|Tail], State, Acc) ->
+ fold_vary_only(Tail, State, [Command|Acc]).
+
buffering_to_zflush(true) -> none;
buffering_to_zflush(false) -> sync.
@@ -180,10 +192,10 @@ gzip_response({response, Status, Headers, Body}, State) ->
after
zlib:close(Z)
end,
- {{response, Status, vary(Headers#{
+ {{response, Status, Headers#{
<<"content-length">> => integer_to_binary(iolist_size(GzBody)),
<<"content-encoding">> => <<"gzip">>
- }), GzBody}, State}.
+ }, GzBody}, State}.
gzip_headers({headers, Status, Headers0}, State) ->
Z = zlib:open(),
@@ -191,9 +203,15 @@ gzip_headers({headers, Status, Headers0}, State) ->
%% @todo It might be good to allow them to be configured?
zlib:deflateInit(Z, default, deflated, 31, 8, default),
Headers = maps:remove(<<"content-length">>, Headers0),
- {{headers, Status, vary(Headers#{
+ {{headers, Status, Headers#{
<<"content-encoding">> => <<"gzip">>
- })}, State#state{deflate=Z}}.
+ }}, State#state{deflate=Z}}.
+
+vary_response({response, Status, Headers, Body}) ->
+ {response, Status, vary(Headers), Body}.
+
+vary_headers({headers, Status, Headers}) ->
+ {headers, Status, vary(Headers)}.
%% We must add content-encoding to vary if it's not already there.
vary(Headers=#{<<"vary">> := Vary}) ->
diff --git a/src/cowboy_constraints.erl b/src/cowboy_constraints.erl
index 6509c4b..33f0111 100644
--- a/src/cowboy_constraints.erl
+++ b/src/cowboy_constraints.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2014-2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2014-2024, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_decompress_h.erl b/src/cowboy_decompress_h.erl
new file mode 100644
index 0000000..4e86e23
--- /dev/null
+++ b/src/cowboy_decompress_h.erl
@@ -0,0 +1,257 @@
+%% Copyright (c) 2024, jdamanalo <[email protected]>
+%% Copyright (c) 2024, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+-module(cowboy_decompress_h).
+-behavior(cowboy_stream).
+
+-export([init/3]).
+-export([data/4]).
+-export([info/3]).
+-export([terminate/3]).
+-export([early_error/5]).
+
+-record(state, {
+ next :: any(),
+ enabled = true :: boolean(),
+ ratio_limit :: non_neg_integer() | undefined,
+ compress = undefined :: undefined | gzip,
+ inflate = undefined :: undefined | zlib:zstream(),
+ is_reading = false :: boolean(),
+
+ %% We use a list of binaries to avoid doing unnecessary
+ %% memory allocations when inflating. We convert to binary
+ %% when we propagate the data. The data must be reversed
+ %% before converting to binary or inflating: this is done
+ %% via the buffer_to_binary/buffer_to_iovec functions.
+ read_body_buffer = [] :: [binary()],
+ read_body_is_fin = nofin :: nofin | {fin, non_neg_integer()}
+}).
+
+-spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts())
+ -> {cowboy_stream:commands(), #state{}}.
+init(StreamID, Req0, Opts) ->
+ Enabled = maps:get(decompress_enabled, Opts, true),
+ RatioLimit = maps:get(decompress_ratio_limit, Opts, 20),
+ {Req, State} = check_and_update_req(Req0),
+ Inflate = case State#state.compress of
+ undefined ->
+ undefined;
+ gzip ->
+ Z = zlib:open(),
+ zlib:inflateInit(Z, 31),
+ Z
+ end,
+ {Commands, Next} = cowboy_stream:init(StreamID, Req, Opts),
+ fold(Commands, State#state{next=Next, enabled=Enabled,
+ ratio_limit=RatioLimit, inflate=Inflate}).
+
+-spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State)
+ -> {cowboy_stream:commands(), State} when State::#state{}.
+data(StreamID, IsFin, Data, State=#state{next=Next0, inflate=undefined}) ->
+ {Commands, Next} = cowboy_stream:data(StreamID, IsFin, Data, Next0),
+ fold(Commands, State#state{next=Next, read_body_is_fin=IsFin});
+data(StreamID, IsFin, Data, State=#state{next=Next0, enabled=false, read_body_buffer=Buffer}) ->
+ {Commands, Next} = cowboy_stream:data(StreamID, IsFin,
+ buffer_to_binary([Data|Buffer]), Next0),
+ fold(Commands, State#state{next=Next, read_body_is_fin=IsFin});
+data(StreamID, IsFin, Data, State0=#state{next=Next0, ratio_limit=RatioLimit,
+ inflate=Z, is_reading=true, read_body_buffer=Buffer}) ->
+ case inflate(Z, RatioLimit, buffer_to_iovec([Data|Buffer])) of
+ {error, ErrorType} ->
+ zlib:close(Z),
+ Status = case ErrorType of
+ data_error -> 400;
+ size_error -> 413
+ end,
+ Commands = [
+ {error_response, Status, #{<<"content-length">> => <<"0">>}, <<>>},
+ stop
+ ],
+ fold(Commands, State0#state{inflate=undefined, read_body_buffer=[]});
+ {ok, Inflated} ->
+ State = case IsFin of
+ nofin ->
+ State0;
+ fin ->
+ zlib:close(Z),
+ State0#state{inflate=undefined}
+ end,
+ {Commands, Next} = cowboy_stream:data(StreamID, IsFin, Inflated, Next0),
+ fold(Commands, State#state{next=Next, read_body_buffer=[],
+ read_body_is_fin=IsFin})
+ end;
+data(_, IsFin, Data, State=#state{read_body_buffer=Buffer}) ->
+ {[], State#state{read_body_buffer=[Data|Buffer], read_body_is_fin=IsFin}}.
+
+-spec info(cowboy_stream:streamid(), any(), State)
+ -> {cowboy_stream:commands(), State} when State::#state{}.
+info(StreamID, Info, State=#state{next=Next0, inflate=undefined}) ->
+ {Commands, Next} = cowboy_stream:info(StreamID, Info, Next0),
+ fold(Commands, State#state{next=Next});
+info(StreamID, Info={CommandTag, _, _, _, _}, State=#state{next=Next0, read_body_is_fin=IsFin})
+ when CommandTag =:= read_body; CommandTag =:= read_body_timeout ->
+ {Commands0, Next1} = cowboy_stream:info(StreamID, Info, Next0),
+ {Commands, Next} = data(StreamID, IsFin, <<>>, State#state{next=Next1, is_reading=true}),
+ fold(Commands ++ Commands0, Next);
+info(StreamID, Info={set_options, Opts}, State0=#state{next=Next0,
+ enabled=Enabled0, ratio_limit=RatioLimit0, is_reading=IsReading}) ->
+ Enabled = maps:get(decompress_enabled, Opts, Enabled0),
+ RatioLimit = maps:get(decompress_ratio_limit, Opts, RatioLimit0),
+ {Commands, Next} = cowboy_stream:info(StreamID, Info, Next0),
+ %% We can't change the enabled setting after we start reading,
+ %% otherwise the data becomes garbage. Changing the setting
+ %% is not treated as an error, it is just ignored.
+ State = case IsReading of
+ true -> State0;
+ false -> State0#state{enabled=Enabled}
+ end,
+ fold(Commands, State#state{next=Next, ratio_limit=RatioLimit});
+info(StreamID, Info, State=#state{next=Next0}) ->
+ {Commands, Next} = cowboy_stream:info(StreamID, Info, Next0),
+ fold(Commands, State#state{next=Next}).
+
+-spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), #state{}) -> any().
+terminate(StreamID, Reason, #state{next=Next, inflate=Z}) ->
+ case Z of
+ undefined -> ok;
+ _ -> zlib:close(Z)
+ end,
+ cowboy_stream:terminate(StreamID, Reason, Next).
+
+-spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(),
+ cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp
+ when Resp::cowboy_stream:resp_command().
+early_error(StreamID, Reason, PartialReq, Resp, Opts) ->
+ cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts).
+
+%% Internal.
+
+%% Check whether the request needs content decoding, and if it does
+%% whether it fits our criteria for decoding. We also update the
+%% Req to indicate whether content was decoded.
+%%
+%% We always set the content_decoded value in the Req because it
+%% indicates whether content decoding was attempted.
+%%
+%% A malformed content-encoding header results in no decoding.
+check_and_update_req(Req=#{headers := Headers}) ->
+ ContentDecoded = maps:get(content_decoded, Req, []),
+ try cowboy_req:parse_header(<<"content-encoding">>, Req) of
+ %% We only automatically decompress when gzip is the only
+ %% encoding used. Since it's the only encoding used, we
+ %% can remove the header entirely before passing the Req
+ %% forward.
+ [<<"gzip">>] ->
+ {Req#{
+ headers => maps:remove(<<"content-encoding">>, Headers),
+ content_decoded => [<<"gzip">>|ContentDecoded]
+ }, #state{compress=gzip}};
+ _ ->
+ {Req#{content_decoded => ContentDecoded},
+ #state{compress=undefined}}
+ catch _:_ ->
+ {Req#{content_decoded => ContentDecoded},
+ #state{compress=undefined}}
+ end.
+
+buffer_to_iovec(Buffer) ->
+ lists:reverse(Buffer).
+
+buffer_to_binary(Buffer) ->
+ iolist_to_binary(lists:reverse(Buffer)).
+
+fold(Commands, State) ->
+ fold(Commands, State, []).
+
+fold([], State, Acc) ->
+ {lists:reverse(Acc), State};
+fold([{response, Status, Headers0, Body}|Tail], State=#state{enabled=true}, Acc) ->
+ Headers = add_accept_encoding(Headers0),
+ fold(Tail, State, [{response, Status, Headers, Body}|Acc]);
+fold([{headers, Status, Headers0} | Tail], State=#state{enabled=true}, Acc) ->
+ Headers = add_accept_encoding(Headers0),
+ fold(Tail, State, [{headers, Status, Headers}|Acc]);
+fold([Command|Tail], State, Acc) ->
+ fold(Tail, State, [Command|Acc]).
+
+add_accept_encoding(Headers=#{<<"accept-encoding">> := AcceptEncoding}) ->
+ try cow_http_hd:parse_accept_encoding(iolist_to_binary(AcceptEncoding)) of
+ List ->
+ case lists:keyfind(<<"gzip">>, 1, List) of
+ %% gzip is excluded but this handler is enabled; we replace.
+ {_, 0} ->
+ Replaced = lists:keyreplace(<<"gzip">>, 1, List, {<<"gzip">>, 1000}),
+ Codings = build_accept_encoding(Replaced),
+ Headers#{<<"accept-encoding">> => Codings};
+ {_, _} ->
+ Headers;
+ false ->
+ case lists:keyfind(<<"*">>, 1, List) of
+ %% Others are excluded along with gzip; we add.
+ {_, 0} ->
+ WithGzip = [{<<"gzip">>, 1000} | List],
+ Codings = build_accept_encoding(WithGzip),
+ Headers#{<<"accept-encoding">> => Codings};
+ {_, _} ->
+ Headers;
+ false ->
+ Headers#{<<"accept-encoding">> => [AcceptEncoding, <<", gzip">>]}
+ end
+ end
+ catch _:_ ->
+ %% The accept-encoding header is invalid. Probably empty. We replace it with ours.
+ Headers#{<<"accept-encoding">> => <<"gzip">>}
+ end;
+add_accept_encoding(Headers) ->
+ Headers#{<<"accept-encoding">> => <<"gzip">>}.
+
+%% @todo From cowlib, maybe expose?
+qvalue_to_iodata(0) -> <<"0">>;
+qvalue_to_iodata(Q) when Q < 10 -> [<<"0.00">>, integer_to_binary(Q)];
+qvalue_to_iodata(Q) when Q < 100 -> [<<"0.0">>, integer_to_binary(Q)];
+qvalue_to_iodata(Q) when Q < 1000 -> [<<"0.">>, integer_to_binary(Q)];
+qvalue_to_iodata(1000) -> <<"1">>.
+
+%% @todo Should be added to Cowlib.
+build_accept_encoding([{ContentCoding, Q}|Tail]) ->
+ Weight = iolist_to_binary(qvalue_to_iodata(Q)),
+ Acc = <<ContentCoding/binary, ";q=", Weight/binary>>,
+ do_build_accept_encoding(Tail, Acc).
+
+do_build_accept_encoding([{ContentCoding, Q}|Tail], Acc0) ->
+ Weight = iolist_to_binary(qvalue_to_iodata(Q)),
+ Acc = <<Acc0/binary, ", ", ContentCoding/binary, ";q=", Weight/binary>>,
+ do_build_accept_encoding(Tail, Acc);
+do_build_accept_encoding([], Acc) ->
+ Acc.
+
+inflate(Z, RatioLimit, Data) ->
+ try
+ {Status, Output} = zlib:safeInflate(Z, Data),
+ Size = iolist_size(Output),
+ do_inflate(Z, Size, iolist_size(Data) * RatioLimit, Status, [Output])
+ catch
+ error:data_error ->
+ {error, data_error}
+ end.
+
+do_inflate(_, Size, Limit, _, _) when Size > Limit ->
+ {error, size_error};
+do_inflate(Z, Size0, Limit, continue, Acc) ->
+ {Status, Output} = zlib:safeInflate(Z, []),
+ Size = Size0 + iolist_size(Output),
+ do_inflate(Z, Size, Limit, Status, [Output | Acc]);
+do_inflate(_, _, _, finished, Acc) ->
+ {ok, iolist_to_binary(lists:reverse(Acc))}.
diff --git a/src/cowboy_handler.erl b/src/cowboy_handler.erl
index c0f7ff7..5048168 100644
--- a/src/cowboy_handler.erl
+++ b/src/cowboy_handler.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl
index c9bceed..9c92ec5 100644
--- a/src/cowboy_http.erl
+++ b/src/cowboy_http.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2016-2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2016-2024, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -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]).
@@ -47,6 +49,7 @@
middlewares => [module()],
proxy_header => boolean(),
request_timeout => timeout(),
+ reset_idle_timeout_on_send => boolean(),
sendfile => boolean(),
shutdown_timeout => timeout(),
stream_handlers => [module()],
@@ -157,9 +160,11 @@
-spec init(pid(), ranch:ref(), inet:socket(), module(),
ranch_proxy_header:proxy_info(), cowboy:opts()) -> ok.
init(Parent, Ref, Socket, Transport, ProxyHeader, Opts) ->
- Peer0 = Transport:peername(Socket),
- Sock0 = Transport:sockname(Socket),
- Cert1 = case Transport:name() of
+ {ok, Peer} = maybe_socket_error(undefined, Transport:peername(Socket),
+ 'A socket error occurred when retrieving the peer name.'),
+ {ok, Sock} = maybe_socket_error(undefined, Transport:sockname(Socket),
+ 'A socket error occurred when retrieving the sock name.'),
+ CertResult = case Transport:name() of
ssl ->
case ssl:peercert(Socket) of
{error, no_peercert} ->
@@ -170,36 +175,29 @@ init(Parent, Ref, Socket, Transport, ProxyHeader, Opts) ->
_ ->
{ok, undefined}
end,
- case {Peer0, Sock0, Cert1} of
- {{ok, Peer}, {ok, Sock}, {ok, Cert}} ->
- State = #state{
- parent=Parent, ref=Ref, socket=Socket,
- transport=Transport, proxy_header=ProxyHeader, opts=Opts,
- peer=Peer, sock=Sock, cert=Cert,
- last_streamid=maps:get(max_keepalive, Opts, 1000)},
- setopts_active(State),
- loop(set_timeout(State, request_timeout));
- {{error, Reason}, _, _} ->
- terminate(undefined, {socket_error, Reason,
- 'A socket error occurred when retrieving the peer name.'});
- {_, {error, Reason}, _} ->
- terminate(undefined, {socket_error, Reason,
- 'A socket error occurred when retrieving the sock name.'});
- {_, _, {error, Reason}} ->
- terminate(undefined, {socket_error, Reason,
- 'A socket error occurred when retrieving the client TLS certificate.'})
- end.
+ {ok, Cert} = maybe_socket_error(undefined, CertResult,
+ 'A socket error occurred when retrieving the client TLS certificate.'),
+ State = #state{
+ parent=Parent, ref=Ref, socket=Socket,
+ transport=Transport, proxy_header=ProxyHeader, opts=Opts,
+ peer=Peer, sock=Sock, cert=Cert,
+ last_streamid=maps:get(max_keepalive, Opts, 1000)},
+ safe_setopts_active(State),
+ loop(set_timeout(State, request_timeout)).
setopts_active(#state{socket=Socket, transport=Transport, opts=Opts}) ->
N = maps:get(active_n, Opts, 100),
Transport:setopts(Socket, [{active, N}]).
+safe_setopts_active(State) ->
+ ok = maybe_socket_error(State, setopts_active(State)).
+
active(State) ->
- setopts_active(State),
+ safe_setopts_active(State),
State#state{active=true}.
passive(State=#state{socket=Socket, transport=Transport}) ->
- Transport:setopts(Socket, [{active, false}]),
+ ok = maybe_socket_error(State, Transport:setopts(Socket, [{active, false}])),
Messages = Transport:messages(),
flush_passive(Socket, Messages),
State#state{active=false}.
@@ -234,7 +232,7 @@ loop(State=#state{parent=Parent, socket=Socket, transport=Transport, opts=Opts,
{Passive, Socket} when Passive =:= element(4, Messages);
%% Hardcoded for compatibility with Ranch 1.x.
Passive =:= tcp_passive; Passive =:= ssl_passive ->
- setopts_active(State),
+ safe_setopts_active(State),
loop(State);
%% Timeouts.
{timeout, Ref, {shutdown, Pid}} ->
@@ -270,9 +268,24 @@ loop(State=#state{parent=Parent, socket=Socket, transport=Transport, opts=Opts,
terminate(State, {internal_error, timeout, 'No message or data received before timeout.'})
end.
-%% We do not set request_timeout if there are active streams.
-set_timeout(State=#state{streams=[_|_]}, request_timeout) ->
- State;
+%% For HTTP/1.1 we have two types of timeouts: the request_timeout
+%% is used when there is no currently ongoing request. This means
+%% that we are not currently sending or receiving data and that
+%% the next data to be received will be a new request. The
+%% request_timeout is set once when we no longer have ongoing
+%% requests, and runs until the full set of request headers
+%% is received. It is not reset.
+%%
+%% After that point we use the idle_timeout. We continue using
+%% the idle_timeout if pipelined requests come in: we are doing
+%% work and just want to ensure the socket is not half-closed.
+%% We continue using the idle_timeout up until there is no
+%% ongoing request. This includes requests that were processed
+%% and for which we only want to skip the body. Once the body
+%% has been read fully we can go back to request_timeout. The
+%% idle_timeout is reset every time we receive data and,
+%% optionally, every time we send data.
+
%% We do not set request_timeout if we are skipping a body.
set_timeout(State=#state{in_state=#ps_body{}}, request_timeout) ->
State;
@@ -299,6 +312,14 @@ set_timeout(State0=#state{opts=Opts, overriden_opts=Override}, Name) ->
end,
State#state{timer=TimerRef}.
+maybe_reset_idle_timeout(State=#state{opts=Opts}) ->
+ case maps:get(reset_idle_timeout_on_send, Opts, false) of
+ true ->
+ set_timeout(State, idle_timeout);
+ false ->
+ State
+ end.
+
cancel_timeout(State=#state{timer=TimerRef}) ->
ok = case TimerRef of
undefined ->
@@ -355,16 +376,27 @@ after_parse({request, Req=#{streamid := StreamID, method := Method,
TE = maps:get(<<"te">>, Headers, undefined),
Streams = [#stream{id=StreamID, state=StreamState,
method=Method, version=Version, te=TE}|Streams0],
- State1 = case maybe_req_close(State0, Headers, Version) of
- close -> State0#state{streams=Streams, last_streamid=StreamID, flow=Flow};
- keepalive -> State0#state{streams=Streams, flow=Flow}
+ State1 = State0#state{streams=Streams, flow=Flow},
+ State2 = case maybe_req_close(State1, Headers, Version) of
+ close ->
+ State1#state{last_streamid=StreamID};
+ keepalive ->
+ State1;
+ bad_connection_header ->
+ error_terminate(400, State1, {connection_error, protocol_error,
+ 'The Connection header is invalid. (RFC7230 6.1)'})
end,
- State = set_timeout(State1, idle_timeout),
+ State = set_timeout(State2, idle_timeout),
parse(Buffer, commands(State, StreamID, Commands))
catch Class:Exception:Stacktrace ->
cowboy:log(cowboy_stream:make_error_log(init,
[StreamID, Req, Opts],
Class, Exception, Stacktrace), Opts),
+ %% We do not reset the idle timeout on send here
+ %% because an error occurred in the application. While we
+ %% are keeping the connection open for further requests we
+ %% do not want to keep the connection up too long if no
+ %% additional requests come in.
early_error(500, State0, {internal_error, {Class, Exception},
'Unhandled exception in cowboy_stream:init/3.'}, Req),
parse(Buffer, State0)
@@ -377,10 +409,7 @@ after_parse({data, StreamID, IsFin, Data, State0=#state{opts=Opts, buffer=Buffer
{Commands, StreamState} ->
Streams = lists:keyreplace(StreamID, #stream.id, Streams0,
Stream#stream{state=StreamState}),
- State1 = set_timeout(State0, case IsFin of
- fin -> request_timeout;
- nofin -> idle_timeout
- end),
+ State1 = set_timeout(State0, idle_timeout),
State = update_flow(IsFin, Data, State1#state{streams=Streams}),
parse(Buffer, commands(State, StreamID, Commands))
catch Class:Exception:Stacktrace ->
@@ -750,39 +779,42 @@ default_port(_) -> 80.
request(Buffer, State0=#state{ref=Ref, transport=Transport, peer=Peer, sock=Sock, cert=Cert,
proxy_header=ProxyHeader, in_streamid=StreamID, in_state=
PS=#ps_header{method=Method, path=Path, qs=Qs, version=Version}},
- Headers0, Host, Port) ->
+ Headers, Host, Port) ->
Scheme = case Transport:secure() of
true -> <<"https">>;
false -> <<"http">>
end,
- {Headers, HasBody, BodyLength, TDecodeFun, TDecodeState} = case Headers0 of
+ {HasBody, BodyLength, TDecodeFun, TDecodeState} = case Headers of
+ #{<<"transfer-encoding">> := _, <<"content-length">> := _} ->
+ error_terminate(400, State0#state{in_state=PS#ps_header{headers=Headers}},
+ {stream_error, protocol_error,
+ 'The request had both transfer-encoding and content-length headers. (RFC7230 3.3.3)'});
#{<<"transfer-encoding">> := TransferEncoding0} ->
try cow_http_hd:parse_transfer_encoding(TransferEncoding0) of
[<<"chunked">>] ->
- {maps:remove(<<"content-length">>, Headers0),
- true, undefined, fun cow_http_te:stream_chunked/2, {0, 0}};
+ {true, undefined, fun cow_http_te:stream_chunked/2, {0, 0}};
_ ->
- error_terminate(400, State0#state{in_state=PS#ps_header{headers=Headers0}},
+ error_terminate(400, State0#state{in_state=PS#ps_header{headers=Headers}},
{stream_error, protocol_error,
'Cowboy only supports transfer-encoding: chunked. (RFC7230 3.3.1)'})
catch _:_ ->
- error_terminate(400, State0#state{in_state=PS#ps_header{headers=Headers0}},
+ error_terminate(400, State0#state{in_state=PS#ps_header{headers=Headers}},
{stream_error, protocol_error,
'The transfer-encoding header is invalid. (RFC7230 3.3.1)'})
end;
#{<<"content-length">> := <<"0">>} ->
- {Headers0, false, 0, undefined, undefined};
+ {false, 0, undefined, undefined};
#{<<"content-length">> := BinLength} ->
Length = try
cow_http_hd:parse_content_length(BinLength)
catch _:_ ->
- error_terminate(400, State0#state{in_state=PS#ps_header{headers=Headers0}},
+ error_terminate(400, State0#state{in_state=PS#ps_header{headers=Headers}},
{stream_error, protocol_error,
'The content-length header is invalid. (RFC7230 3.3.2)'})
end,
- {Headers0, true, Length, fun cow_http_te:stream_identity/2, {0, Length}};
+ {true, Length, fun cow_http_te:stream_identity/2, {0, Length}};
_ ->
- {Headers0, false, 0, undefined, undefined}
+ {false, 0, undefined, undefined}
end,
Req0 = #{
ref => Ref,
@@ -953,6 +985,11 @@ info(State=#state{opts=Opts, streams=Streams0}, StreamID, Msg) ->
end.
%% Commands.
+%%
+%% The order in which the commands are given matters. Cowboy may
+%% stop processing commands after the 'stop' command or when an
+%% error occurred, such as a socket error. Critical commands such
+%% as 'spawn' should always be given first.
commands(State, _, []) ->
State;
@@ -1006,19 +1043,20 @@ commands(State=#state{out_state=wait, out_streamid=StreamID}, StreamID,
commands(State, StreamID, [{error_response, _, _, _}|Tail]) ->
commands(State, StreamID, Tail);
%% Send an informational response.
-commands(State=#state{socket=Socket, transport=Transport, out_state=wait, streams=Streams},
+commands(State0=#state{socket=Socket, transport=Transport, out_state=wait, streams=Streams},
StreamID, [{inform, StatusCode, Headers}|Tail]) ->
%% @todo I'm pretty sure the last stream in the list is the one we want
%% considering all others are queued.
#stream{version=Version} = lists:keyfind(StreamID, #stream.id, Streams),
_ = case Version of
'HTTP/1.1' ->
- Transport:send(Socket, cow_http:response(StatusCode, 'HTTP/1.1',
- headers_to_list(Headers)));
+ ok = maybe_socket_error(State0, Transport:send(Socket,
+ cow_http:response(StatusCode, 'HTTP/1.1', headers_to_list(Headers))));
%% Do not send informational responses to HTTP/1.0 clients. (RFC7231 6.2)
'HTTP/1.0' ->
ok
end,
+ State = maybe_reset_idle_timeout(State0),
commands(State, StreamID, Tail);
%% Send a full response.
%%
@@ -1031,17 +1069,18 @@ commands(State0=#state{socket=Socket, transport=Transport, out_state=wait, strea
%% considering all others are queued.
#stream{version=Version} = lists:keyfind(StreamID, #stream.id, Streams),
{State1, Headers} = connection(State0, Headers0, StreamID, Version),
- State = State1#state{out_state=done},
+ State2 = State1#state{out_state=done},
%% @todo Ensure content-length is set. 204 must never have content-length set.
Response = cow_http:response(StatusCode, 'HTTP/1.1', headers_to_list(Headers)),
%% @todo 204 and 304 responses must not include a response body. (RFC7230 3.3.1, RFC7230 3.3.2)
case Body of
{sendfile, _, _, _} ->
- Transport:send(Socket, Response),
- sendfile(State, Body);
+ ok = maybe_socket_error(State2, Transport:send(Socket, Response)),
+ sendfile(State2, Body);
_ ->
- Transport:send(Socket, [Response, Body])
+ ok = maybe_socket_error(State2, Transport:send(Socket, [Response, Body]))
end,
+ State = maybe_reset_idle_timeout(State2),
commands(State, StreamID, Tail);
%% Send response headers and initiate chunked encoding or streaming.
commands(State0=#state{socket=Socket, transport=Transport,
@@ -1078,8 +1117,10 @@ commands(State0=#state{socket=Socket, transport=Transport,
trailers -> Headers1;
_ -> maps:remove(<<"trailer">>, Headers1)
end,
- {State, Headers} = connection(State1, Headers2, StreamID, Version),
- Transport:send(Socket, cow_http:response(StatusCode, 'HTTP/1.1', headers_to_list(Headers))),
+ {State2, Headers} = connection(State1, Headers2, StreamID, Version),
+ ok = maybe_socket_error(State2, Transport:send(Socket,
+ cow_http:response(StatusCode, 'HTTP/1.1', headers_to_list(Headers)))),
+ State = maybe_reset_idle_timeout(State2),
commands(State, StreamID, Tail);
%% Send a response body chunk.
%% @todo We need to kill the stream if it tries to send data before headers.
@@ -1098,27 +1139,33 @@ commands(State0=#state{socket=Socket, transport=Transport, streams=Streams0, out
Stream0=#stream{method= <<"HEAD">>} ->
Stream0;
Stream0 when Size =:= 0, IsFin =:= fin, OutState =:= chunked ->
- Transport:send(Socket, <<"0\r\n\r\n">>),
+ ok = maybe_socket_error(State0,
+ Transport:send(Socket, <<"0\r\n\r\n">>)),
Stream0;
Stream0 when Size =:= 0 ->
Stream0;
Stream0 when is_tuple(Data), OutState =:= chunked ->
- Transport:send(Socket, [integer_to_binary(Size, 16), <<"\r\n">>]),
+ ok = maybe_socket_error(State0,
+ Transport:send(Socket, [integer_to_binary(Size, 16), <<"\r\n">>])),
sendfile(State0, Data),
- Transport:send(Socket,
- case IsFin of
- fin -> <<"\r\n0\r\n\r\n">>;
- nofin -> <<"\r\n">>
- end),
+ ok = maybe_socket_error(State0,
+ Transport:send(Socket,
+ case IsFin of
+ fin -> <<"\r\n0\r\n\r\n">>;
+ nofin -> <<"\r\n">>
+ end)
+ ),
Stream0;
Stream0 when OutState =:= chunked ->
- Transport:send(Socket, [
- integer_to_binary(Size, 16), <<"\r\n">>, Data,
- case IsFin of
- fin -> <<"\r\n0\r\n\r\n">>;
- nofin -> <<"\r\n">>
- end
- ]),
+ ok = maybe_socket_error(State0,
+ Transport:send(Socket, [
+ integer_to_binary(Size, 16), <<"\r\n">>, Data,
+ case IsFin of
+ fin -> <<"\r\n0\r\n\r\n">>;
+ nofin -> <<"\r\n">>
+ end
+ ])
+ ),
Stream0;
Stream0 when OutState =:= streaming ->
#stream{local_sent_size=SentSize0, local_expected_size=ExpectedSize} = Stream0,
@@ -1130,31 +1177,36 @@ commands(State0=#state{socket=Socket, transport=Transport, streams=Streams0, out
is_tuple(Data) ->
sendfile(State0, Data);
true ->
- Transport:send(Socket, Data)
+ ok = maybe_socket_error(State0, Transport:send(Socket, Data))
end,
Stream0#stream{local_sent_size=SentSize}
end,
- State = case IsFin of
+ State1 = case IsFin of
fin -> State0#state{out_state=done};
nofin -> State0
end,
+ State = maybe_reset_idle_timeout(State1),
Streams = lists:keyreplace(StreamID, #stream.id, Streams0, Stream),
commands(State#state{streams=Streams}, StreamID, Tail);
-commands(State=#state{socket=Socket, transport=Transport, streams=Streams, out_state=OutState},
+commands(State0=#state{socket=Socket, transport=Transport, streams=Streams, out_state=OutState},
StreamID, [{trailers, Trailers}|Tail]) ->
case stream_te(OutState, lists:keyfind(StreamID, #stream.id, Streams)) of
trailers ->
- Transport:send(Socket, [
- <<"0\r\n">>,
- cow_http:headers(maps:to_list(Trailers)),
- <<"\r\n">>
- ]);
+ ok = maybe_socket_error(State0,
+ Transport:send(Socket, [
+ <<"0\r\n">>,
+ cow_http:headers(maps:to_list(Trailers)),
+ <<"\r\n">>
+ ])
+ );
no_trailers ->
- Transport:send(Socket, <<"0\r\n\r\n">>);
+ ok = maybe_socket_error(State0,
+ Transport:send(Socket, <<"0\r\n\r\n">>));
not_chunked ->
ok
end,
- commands(State#state{out_state=done}, StreamID, Tail);
+ State = maybe_reset_idle_timeout(State0#state{out_state=done}),
+ commands(State, StreamID, Tail);
%% Protocol takeover.
commands(State0=#state{ref=Ref, parent=Parent, socket=Socket, transport=Transport,
out_state=OutState, opts=Opts, buffer=Buffer, children=Children}, StreamID,
@@ -1174,11 +1226,13 @@ commands(State0=#state{ref=Ref, parent=Parent, socket=Socket, transport=Transpor
_ -> State
end,
#stream{state=StreamState} = lists:keyfind(StreamID, #stream.id, Streams),
- %% @todo We need to shutdown processes here first.
stream_call_terminate(StreamID, switch_protocol, StreamState, State),
%% Terminate children processes and flush any remaining messages from the mailbox.
cowboy_children:terminate(Children),
flush(Parent),
+ %% Turn off the trap_exit process flag
+ %% since this process will no longer be a supervisor.
+ process_flag(trap_exit, false),
Protocol:takeover(Parent, Ref, Socket, Transport, Opts, Buffer, InitialState);
%% Set options dynamically.
commands(State0=#state{overriden_opts=Opts},
@@ -1238,10 +1292,12 @@ sendfile(State=#state{socket=Socket, transport=Transport, opts=Opts},
{sendfile, Offset, Bytes, Path}) ->
try
%% When sendfile is disabled we explicitly use the fallback.
- _ = case maps:get(sendfile, Opts, true) of
- true -> Transport:sendfile(Socket, Path, Offset, Bytes);
- false -> ranch_transport:sendfile(Transport, Socket, Path, Offset, Bytes, [])
- end,
+ {ok, _} = maybe_socket_error(State,
+ case maps:get(sendfile, Opts, true) of
+ true -> Transport:sendfile(Socket, Path, Offset, Bytes);
+ false -> ranch_transport:sendfile(Transport, Socket, Path, Offset, Bytes, [])
+ end
+ ),
ok
catch _:_ ->
terminate(State, {socket_error, sendfile_crash,
@@ -1315,7 +1371,10 @@ stream_next(State0=#state{opts=Opts, active=Active, out_streamid=OutStreamID, st
NextOutStreamID = OutStreamID + 1,
case lists:keyfind(NextOutStreamID, #stream.id, Streams) of
false ->
- State0#state{out_streamid=NextOutStreamID, out_state=wait};
+ State = State0#state{out_streamid=NextOutStreamID, out_state=wait},
+ %% There are no streams remaining. We therefore can
+ %% and want to switch back to the request_timeout.
+ set_timeout(State, request_timeout);
#stream{queue=Commands} ->
State = case Active of
true -> State0;
@@ -1341,17 +1400,23 @@ stream_call_terminate(StreamID, Reason, StreamState, #state{opts=Opts}) ->
maybe_req_close(#state{opts=#{http10_keepalive := false}}, _, 'HTTP/1.0') ->
close;
maybe_req_close(_, #{<<"connection">> := Conn}, 'HTTP/1.0') ->
- Conns = cow_http_hd:parse_connection(Conn),
- case lists:member(<<"keep-alive">>, Conns) of
- true -> keepalive;
- false -> close
+ try cow_http_hd:parse_connection(Conn) of
+ Conns ->
+ case lists:member(<<"keep-alive">>, Conns) of
+ true -> keepalive;
+ false -> close
+ end
+ catch _:_ ->
+ bad_connection_header
end;
maybe_req_close(_, _, 'HTTP/1.0') ->
close;
maybe_req_close(_, #{<<"connection">> := Conn}, 'HTTP/1.1') ->
- case connection_hd_is_close(Conn) of
+ try connection_hd_is_close(Conn) of
true -> close;
false -> keepalive
+ catch _:_ ->
+ bad_connection_header
end;
maybe_req_close(_, _, _) ->
keepalive.
@@ -1420,37 +1485,55 @@ error_terminate(StatusCode, State=#state{ref=Ref, peer=Peer, in_state=StreamStat
early_error(StatusCode, State, Reason, PartialReq) ->
early_error(StatusCode, State, Reason, PartialReq, #{}).
-early_error(StatusCode0, #state{socket=Socket, transport=Transport,
+early_error(StatusCode0, State=#state{socket=Socket, transport=Transport,
opts=Opts, in_streamid=StreamID}, Reason, PartialReq, RespHeaders0) ->
RespHeaders1 = RespHeaders0#{<<"content-length">> => <<"0">>},
Resp = {response, StatusCode0, RespHeaders1, <<>>},
try cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts) of
{response, StatusCode, RespHeaders, RespBody} ->
- Transport:send(Socket, [
- cow_http:response(StatusCode, 'HTTP/1.1', maps:to_list(RespHeaders)),
- %% @todo We shouldn't send the body when the method is HEAD.
- %% @todo Technically we allow the sendfile tuple.
- RespBody
- ])
+ ok = maybe_socket_error(State,
+ Transport:send(Socket, [
+ cow_http:response(StatusCode, 'HTTP/1.1', maps:to_list(RespHeaders)),
+ %% @todo We shouldn't send the body when the method is HEAD.
+ %% @todo Technically we allow the sendfile tuple.
+ RespBody
+ ])
+ )
catch Class:Exception:Stacktrace ->
cowboy:log(cowboy_stream:make_error_log(early_error,
[StreamID, Reason, PartialReq, Resp, Opts],
Class, Exception, Stacktrace), Opts),
%% We still need to send an error response, so send what we initially
%% wanted to send. It's better than nothing.
- Transport:send(Socket, cow_http:response(StatusCode0,
- 'HTTP/1.1', maps:to_list(RespHeaders1)))
- end,
- ok.
+ ok = maybe_socket_error(State,
+ Transport:send(Socket, cow_http:response(StatusCode0,
+ 'HTTP/1.1', maps:to_list(RespHeaders1)))
+ )
+ end.
initiate_closing(State=#state{streams=[]}, Reason) ->
terminate(State, Reason);
-initiate_closing(State=#state{streams=[_Stream|Streams],
+initiate_closing(State=#state{streams=Streams,
out_streamid=OutStreamID}, Reason) ->
- terminate_all_streams(State, Streams, Reason),
- State#state{last_streamid=OutStreamID}.
-
--spec terminate(_, _) -> no_return().
+ {value, LastStream, TerminatedStreams}
+ = lists:keytake(OutStreamID, #stream.id, Streams),
+ terminate_all_streams(State, TerminatedStreams, Reason),
+ State#state{streams=[LastStream], last_streamid=OutStreamID}.
+
+%% Function replicated in cowboy_http2.
+maybe_socket_error(State, {error, closed}) ->
+ terminate(State, {socket_error, closed, 'The socket has been closed.'});
+maybe_socket_error(State, Reason) ->
+ maybe_socket_error(State, Reason, 'An error has occurred on the socket.').
+
+maybe_socket_error(_, Result = ok, _) ->
+ Result;
+maybe_socket_error(_, Result = {ok, _}, _) ->
+ Result;
+maybe_socket_error(State, {error, Reason}, Human) ->
+ terminate(State, {socket_error, Reason, Human}).
+
+-spec terminate(#state{} | undefined, _) -> no_return().
terminate(undefined, Reason) ->
exit({shutdown, Reason});
terminate(State=#state{streams=Streams, children=Children}, Reason) ->
@@ -1484,6 +1567,9 @@ terminate_linger(State=#state{socket=Socket, transport=Transport, opts=Opts}) ->
terminate_linger_before_loop(State, TimerRef, Messages) ->
%% We may already be in active mode when we do this
%% but it's OK because we are shutting down anyway.
+ %%
+ %% We specially handle the socket error to terminate
+ %% when an error occurs.
case setopts_active(State) of
ok ->
terminate_linger_loop(State, TimerRef, Messages);
diff --git a/src/cowboy_http2.erl b/src/cowboy_http2.erl
index 7440d91..2e73d5f 100644
--- a/src/cowboy_http2.erl
+++ b/src/cowboy_http2.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2015-2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2015-2024, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -44,10 +44,12 @@
max_connection_window_size => 0..16#7fffffff,
max_decode_table_size => non_neg_integer(),
max_encode_table_size => non_neg_integer(),
+ max_fragmented_header_block_size => 16384..16#7fffffff,
max_frame_size_received => 16384..16777215,
max_frame_size_sent => 16384..16777215 | infinity,
max_received_frame_rate => {pos_integer(), timeout()},
max_reset_stream_rate => {pos_integer(), timeout()},
+ max_cancel_stream_rate => {pos_integer(), timeout()},
max_stream_buffer_size => non_neg_integer(),
max_stream_window_size => 0..16#7fffffff,
metrics_callback => cowboy_metrics_h:metrics_callback(),
@@ -56,6 +58,7 @@
middlewares => [module()],
preface_timeout => timeout(),
proxy_header => boolean(),
+ reset_idle_timeout_on_send => boolean(),
sendfile => boolean(),
settings_timeout => timeout(),
shutdown_timeout => timeout(),
@@ -114,6 +117,10 @@
reset_rate_num :: undefined | pos_integer(),
reset_rate_time :: undefined | integer(),
+ %% HTTP/2 rapid reset attack protection.
+ cancel_rate_num :: undefined | pos_integer(),
+ cancel_rate_time :: undefined | integer(),
+
%% Flow requested for all streams.
flow = 0 :: non_neg_integer(),
@@ -129,9 +136,11 @@
-spec init(pid(), ranch:ref(), inet:socket(), module(),
ranch_proxy_header:proxy_info() | undefined, cowboy:opts()) -> ok.
init(Parent, Ref, Socket, Transport, ProxyHeader, Opts) ->
- Peer0 = Transport:peername(Socket),
- Sock0 = Transport:sockname(Socket),
- Cert1 = case Transport:name() of
+ {ok, Peer} = maybe_socket_error(undefined, Transport:peername(Socket),
+ 'A socket error occurred when retrieving the peer name.'),
+ {ok, Sock} = maybe_socket_error(undefined, Transport:sockname(Socket),
+ 'A socket error occurred when retrieving the sock name.'),
+ CertResult = case Transport:name() of
ssl ->
case ssl:peercert(Socket) of
{error, no_peercert} ->
@@ -142,19 +151,9 @@ init(Parent, Ref, Socket, Transport, ProxyHeader, Opts) ->
_ ->
{ok, undefined}
end,
- case {Peer0, Sock0, Cert1} of
- {{ok, Peer}, {ok, Sock}, {ok, Cert}} ->
- init(Parent, Ref, Socket, Transport, ProxyHeader, Opts, Peer, Sock, Cert, <<>>);
- {{error, Reason}, _, _} ->
- terminate(undefined, {socket_error, Reason,
- 'A socket error occurred when retrieving the peer name.'});
- {_, {error, Reason}, _} ->
- terminate(undefined, {socket_error, Reason,
- 'A socket error occurred when retrieving the sock name.'});
- {_, _, {error, Reason}} ->
- terminate(undefined, {socket_error, Reason,
- 'A socket error occurred when retrieving the client TLS certificate.'})
- end.
+ {ok, Cert} = maybe_socket_error(undefined, CertResult,
+ 'A socket error occurred when retrieving the client TLS certificate.'),
+ init(Parent, Ref, Socket, Transport, ProxyHeader, Opts, Peer, Sock, Cert, <<>>).
-spec init(pid(), ranch:ref(), inet:socket(), module(),
ranch_proxy_header:proxy_info() | undefined, cowboy:opts(),
@@ -162,20 +161,23 @@ init(Parent, Ref, Socket, Transport, ProxyHeader, Opts) ->
binary() | undefined, binary()) -> ok.
init(Parent, Ref, Socket, Transport, ProxyHeader, Opts, Peer, Sock, Cert, Buffer) ->
{ok, Preface, HTTP2Machine} = cow_http2_machine:init(server, Opts),
+ %% Send the preface before doing all the init in case we get a socket error.
+ ok = maybe_socket_error(undefined, Transport:send(Socket, Preface)),
State = set_idle_timeout(init_rate_limiting(#state{parent=Parent, ref=Ref, socket=Socket,
transport=Transport, proxy_header=ProxyHeader,
opts=Opts, peer=Peer, sock=Sock, cert=Cert,
http2_status=sequence, http2_machine=HTTP2Machine})),
- Transport:send(Socket, Preface),
- setopts_active(State),
+ safe_setopts_active(State),
case Buffer of
<<>> -> loop(State, Buffer);
_ -> parse(State, Buffer)
end.
-init_rate_limiting(State) ->
+init_rate_limiting(State0) ->
CurrentTime = erlang:monotonic_time(millisecond),
- init_reset_rate_limiting(init_frame_rate_limiting(State, CurrentTime), CurrentTime).
+ State1 = init_frame_rate_limiting(State0, CurrentTime),
+ State2 = init_reset_rate_limiting(State1, CurrentTime),
+ init_cancel_rate_limiting(State2, CurrentTime).
init_frame_rate_limiting(State=#state{opts=Opts}, CurrentTime) ->
{FrameRateNum, FrameRatePeriod} = maps:get(max_received_frame_rate, Opts, {10000, 10000}),
@@ -189,6 +191,12 @@ init_reset_rate_limiting(State=#state{opts=Opts}, CurrentTime) ->
reset_rate_num=ResetRateNum, reset_rate_time=add_period(CurrentTime, ResetRatePeriod)
}.
+init_cancel_rate_limiting(State=#state{opts=Opts}, CurrentTime) ->
+ {CancelRateNum, CancelRatePeriod} = maps:get(max_cancel_stream_rate, Opts, {500, 10000}),
+ State#state{
+ cancel_rate_num=CancelRateNum, cancel_rate_time=add_period(CurrentTime, CancelRatePeriod)
+ }.
+
add_period(_, infinity) -> infinity;
add_period(Time, Period) -> Time + Period.
@@ -215,8 +223,10 @@ init(Parent, Ref, Socket, Transport, ProxyHeader, Opts, Peer, Sock, Cert, Buffer
<<"upgrade">> => <<"h2c">>
}, ?MODULE, undefined}), %% @todo undefined or #{}?
State = set_idle_timeout(init_rate_limiting(State2#state{http2_status=sequence})),
- Transport:send(Socket, Preface),
- setopts_active(State),
+ %% In the case of HTTP/1.1 Upgrade we cannot send the Preface
+ %% until we send the 101 response.
+ ok = maybe_socket_error(State, Transport:send(Socket, Preface)),
+ safe_setopts_active(State),
case Buffer of
<<>> -> loop(State, Buffer);
_ -> parse(State, Buffer)
@@ -229,6 +239,9 @@ setopts_active(#state{socket=Socket, transport=Transport, opts=Opts}) ->
N = maps:get(active_n, Opts, 100),
Transport:setopts(Socket, [{active, N}]).
+safe_setopts_active(State) ->
+ ok = maybe_socket_error(State, setopts_active(State)).
+
loop(State=#state{parent=Parent, socket=Socket, transport=Transport,
opts=Opts, timer=TimerRef, children=Children}, Buffer) ->
Messages = Transport:messages(),
@@ -248,7 +261,7 @@ loop(State=#state{parent=Parent, socket=Socket, transport=Transport,
{Passive, Socket} when Passive =:= element(4, Messages);
%% Hardcoded for compatibility with Ranch 1.x.
Passive =:= tcp_passive; Passive =:= ssl_passive ->
- setopts_active(State),
+ safe_setopts_active(State),
loop(State, Buffer);
%% System messages.
{'EXIT', Parent, shutdown} ->
@@ -307,6 +320,14 @@ set_timeout(State=#state{timer=TimerRef0}, Timeout, Message) ->
end,
State#state{timer=TimerRef}.
+maybe_reset_idle_timeout(State=#state{opts=Opts}) ->
+ case maps:get(reset_idle_timeout_on_send, Opts, false) of
+ true ->
+ set_idle_timeout(State);
+ false ->
+ State
+ end.
+
%% HTTP/2 protocol parsing.
parse(State=#state{http2_status=sequence}, Data) ->
@@ -383,10 +404,11 @@ frame(State=#state{http2_machine=HTTP2Machine0}, Frame) ->
goaway(State#state{http2_machine=HTTP2Machine}, GoAway);
{send, SendData, HTTP2Machine} ->
%% We may need to send an alarm for each of the streams sending data.
- lists:foldl(
+ State1 = lists:foldl(
fun({StreamID, _, _}, S) -> maybe_send_data_alarm(S, HTTP2Machine0, StreamID) end,
send_data(maybe_ack(State#state{http2_machine=HTTP2Machine}, Frame), SendData, []),
- SendData);
+ SendData),
+ maybe_reset_idle_timeout(State1);
{error, {stream_error, StreamID, Reason, Human}, HTTP2Machine} ->
reset_stream(State#state{http2_machine=HTTP2Machine},
StreamID, {stream_error, Reason, Human});
@@ -398,15 +420,20 @@ frame(State=#state{http2_machine=HTTP2Machine0}, Frame) ->
%% if we were still waiting for a SETTINGS frame.
maybe_ack(State=#state{http2_status=settings}, Frame) ->
maybe_ack(State#state{http2_status=connected}, Frame);
+%% We do not reset the idle timeout on send here because we are
+%% sending data as a consequence of receiving data, which means
+%% we already resetted the idle timeout.
maybe_ack(State=#state{socket=Socket, transport=Transport}, Frame) ->
case Frame of
- {settings, _} -> Transport:send(Socket, cow_http2:settings_ack());
- {ping, Opaque} -> Transport:send(Socket, cow_http2:ping_ack(Opaque));
+ {settings, _} ->
+ ok = maybe_socket_error(State, Transport:send(Socket, cow_http2:settings_ack()));
+ {ping, Opaque} ->
+ ok = maybe_socket_error(State, Transport:send(Socket, cow_http2:ping_ack(Opaque)));
_ -> ok
end,
State.
-data_frame(State0=#state{opts=Opts, flow=Flow, streams=Streams}, StreamID, IsFin, Data) ->
+data_frame(State0=#state{opts=Opts, flow=Flow0, streams=Streams}, StreamID, IsFin, Data) ->
case Streams of
#{StreamID := Stream=#stream{status=running, flow=StreamFlow, state=StreamState0}} ->
try cowboy_stream:data(StreamID, IsFin, Data, StreamState0) of
@@ -415,11 +442,26 @@ data_frame(State0=#state{opts=Opts, flow=Flow, streams=Streams}, StreamID, IsFin
%% We may receive more data than we requested. We ensure
%% that the flow value doesn't go lower than 0.
Size = byte_size(Data),
- State = update_window(State0#state{flow=max(0, Flow - Size),
+ Flow = max(0, Flow0 - Size),
+ %% We would normally update the window when changing the flow
+ %% value. But because we are running commands, which themselves
+ %% may update the window, and we want to avoid updating the
+ %% window twice in a row, we first run the commands and then
+ %% only update the window a flow command was executed. We know
+ %% that it was because the flow value changed in the state.
+ State1 = State0#state{flow=Flow,
streams=Streams#{StreamID => Stream#stream{
flow=max(0, StreamFlow - Size), state=StreamState}}},
- StreamID),
- commands(State, StreamID, Commands)
+ State = commands(State1, StreamID, Commands),
+ case State of
+ %% No flow command was executed. We must update the window
+ %% because we changed the flow value earlier.
+ #state{flow=Flow} ->
+ update_window(State, StreamID);
+ %% Otherwise the window was updated already.
+ _ ->
+ State
+ end
catch Class:Exception:Stacktrace ->
cowboy:log(cowboy_stream:make_error_log(data,
[StreamID, IsFin, Data, StreamState0],
@@ -568,11 +610,27 @@ rst_stream_frame(State=#state{streams=Streams0, children=Children0}, StreamID, R
{#stream{state=StreamState}, Streams} ->
terminate_stream_handler(State, StreamID, Reason, StreamState),
Children = cowboy_children:shutdown(Children0, StreamID),
- State#state{streams=Streams, children=Children};
+ cancel_rate_limit(State#state{streams=Streams, children=Children});
error ->
State
end.
+cancel_rate_limit(State0=#state{cancel_rate_num=Num0, cancel_rate_time=Time}) ->
+ case Num0 - 1 of
+ 0 ->
+ CurrentTime = erlang:monotonic_time(millisecond),
+ if
+ CurrentTime < Time ->
+ terminate(State0, {connection_error, enhance_your_calm,
+ 'Stream cancel rate larger than configuration allows. Flood? (CVE-2023-44487)'});
+ true ->
+ %% When the option has a period of infinity we cannot reach this clause.
+ init_cancel_rate_limiting(State0, CurrentTime)
+ end;
+ Num ->
+ State0#state{cancel_rate_num=Num}
+ end.
+
ignored_frame(State=#state{http2_machine=HTTP2Machine0}) ->
case cow_http2_machine:ignored_frame(HTTP2Machine0) of
{ok, HTTP2Machine} ->
@@ -657,23 +715,37 @@ commands(State=#state{http2_machine=HTTP2Machine}, StreamID,
end;
%% Send an informational response.
commands(State0, StreamID, [{inform, StatusCode, Headers}|Tail]) ->
- State = send_headers(State0, StreamID, idle, StatusCode, Headers),
+ State1 = send_headers(State0, StreamID, idle, StatusCode, Headers),
+ State = maybe_reset_idle_timeout(State1),
commands(State, StreamID, Tail);
%% Send response headers.
commands(State0, StreamID, [{response, StatusCode, Headers, Body}|Tail]) ->
- State = send_response(State0, StreamID, StatusCode, Headers, Body),
+ State1 = send_response(State0, StreamID, StatusCode, Headers, Body),
+ State = maybe_reset_idle_timeout(State1),
commands(State, StreamID, Tail);
%% Send response headers.
commands(State0, StreamID, [{headers, StatusCode, Headers}|Tail]) ->
- State = send_headers(State0, StreamID, nofin, StatusCode, Headers),
+ State1 = send_headers(State0, StreamID, nofin, StatusCode, Headers),
+ State = maybe_reset_idle_timeout(State1),
commands(State, StreamID, Tail);
%% Send a response body chunk.
commands(State0, StreamID, [{data, IsFin, Data}|Tail]) ->
- State = maybe_send_data(State0, StreamID, IsFin, Data, []),
+ State = case maybe_send_data(State0, StreamID, IsFin, Data, []) of
+ {data_sent, State1} ->
+ maybe_reset_idle_timeout(State1);
+ {no_data_sent, State1} ->
+ State1
+ end,
commands(State, StreamID, Tail);
%% Send trailers.
commands(State0, StreamID, [{trailers, Trailers}|Tail]) ->
- State = maybe_send_data(State0, StreamID, fin, {trailers, maps:to_list(Trailers)}, []),
+ State = case maybe_send_data(State0, StreamID, fin,
+ {trailers, maps:to_list(Trailers)}, []) of
+ {data_sent, State1} ->
+ maybe_reset_idle_timeout(State1);
+ {no_data_sent, State1} ->
+ State1
+ end,
commands(State, StreamID, Tail);
%% Send a push promise.
%%
@@ -705,10 +777,11 @@ commands(State0=#state{socket=Socket, transport=Transport, http2_machine=HTTP2Ma
State = case cow_http2_machine:prepare_push_promise(StreamID, HTTP2Machine0,
PseudoHeaders, Headers) of
{ok, PromisedStreamID, HeaderBlock, HTTP2Machine} ->
- Transport:send(Socket, cow_http2:push_promise(
- StreamID, PromisedStreamID, HeaderBlock)),
- headers_frame(State0#state{http2_machine=HTTP2Machine},
- PromisedStreamID, fin, Headers, PseudoHeaders, 0);
+ State1 = State0#state{http2_machine=HTTP2Machine},
+ ok = maybe_socket_error(State1, Transport:send(Socket,
+ cow_http2:push_promise(StreamID, PromisedStreamID, HeaderBlock))),
+ State2 = maybe_reset_idle_timeout(State1),
+ headers_frame(State2, PromisedStreamID, fin, Headers, PseudoHeaders, 0);
{error, no_push} ->
State0
end,
@@ -731,10 +804,14 @@ commands(State, StreamID, [Error = {internal_error, _, _}|_Tail]) ->
%% @todo Only reset when the stream still exists.
reset_stream(State, StreamID, Error);
%% Upgrade to HTTP/2. This is triggered by cowboy_http2 itself.
+%%
+%% We do not need to reset the idle timeout on send because it
+%% hasn't been set yet. This is called from init/12.
commands(State=#state{socket=Socket, transport=Transport, http2_status=upgrade},
StreamID, [{switch_protocol, Headers, ?MODULE, _}|Tail]) ->
%% @todo This 101 response needs to be passed through stream handlers.
- Transport:send(Socket, cow_http:response(101, 'HTTP/1.1', maps:to_list(Headers))),
+ ok = maybe_socket_error(State, Transport:send(Socket,
+ cow_http:response(101, 'HTTP/1.1', maps:to_list(Headers)))),
commands(State, StreamID, Tail);
%% Use a different protocol within the stream (CONNECT :protocol).
%% @todo Make sure we error out when the feature is disabled.
@@ -755,22 +832,32 @@ commands(State=#state{opts=Opts}, StreamID, [Log={log, _, _, _}|Tail]) ->
%% Tentatively update the window after the flow was updated.
-update_window(State=#state{socket=Socket, transport=Transport,
+update_window(State0=#state{socket=Socket, transport=Transport,
http2_machine=HTTP2Machine0, flow=Flow, streams=Streams}, StreamID) ->
- #{StreamID := #stream{flow=StreamFlow}} = Streams,
{Data1, HTTP2Machine2} = case cow_http2_machine:ensure_window(Flow, HTTP2Machine0) of
ok -> {<<>>, HTTP2Machine0};
{ok, Increment1, HTTP2Machine1} -> {cow_http2:window_update(Increment1), HTTP2Machine1}
end,
- {Data2, HTTP2Machine} = case cow_http2_machine:ensure_window(StreamID, StreamFlow, HTTP2Machine2) of
- ok -> {<<>>, HTTP2Machine2};
- {ok, Increment2, HTTP2Machine3} -> {cow_http2:window_update(StreamID, Increment2), HTTP2Machine3}
+ {Data2, HTTP2Machine} = case Streams of
+ #{StreamID := #stream{flow=StreamFlow}} ->
+ case cow_http2_machine:ensure_window(StreamID, StreamFlow, HTTP2Machine2) of
+ ok ->
+ {<<>>, HTTP2Machine2};
+ {ok, Increment2, HTTP2Machine3} ->
+ {cow_http2:window_update(StreamID, Increment2), HTTP2Machine3}
+ end;
+ _ ->
+ %% Don't update the stream's window if it stopped.
+ {<<>>, HTTP2Machine2}
end,
+ State = State0#state{http2_machine=HTTP2Machine},
case {Data1, Data2} of
- {<<>>, <<>>} -> ok;
- _ -> Transport:send(Socket, [Data1, Data2])
- end,
- State#state{http2_machine=HTTP2Machine}.
+ {<<>>, <<>>} ->
+ State;
+ _ ->
+ ok = maybe_socket_error(State, Transport:send(Socket, [Data1, Data2])),
+ maybe_reset_idle_timeout(State)
+ end.
%% Send the response, trailers or data.
@@ -790,18 +877,21 @@ send_response(State0=#state{http2_machine=HTTP2Machine0}, StreamID, StatusCode,
= cow_http2_machine:prepare_headers(StreamID, HTTP2Machine0, nofin,
#{status => cow_http:status_to_integer(StatusCode)},
headers_to_list(Headers)),
- maybe_send_data(State0#state{http2_machine=HTTP2Machine}, StreamID, fin, Body,
- [cow_http2:headers(StreamID, nofin, HeaderBlock)])
+ {_, State} = maybe_send_data(State0#state{http2_machine=HTTP2Machine},
+ StreamID, fin, Body, [cow_http2:headers(StreamID, nofin, HeaderBlock)]),
+ State
end.
-send_headers(State=#state{socket=Socket, transport=Transport,
+send_headers(State0=#state{socket=Socket, transport=Transport,
http2_machine=HTTP2Machine0}, StreamID, IsFin0, StatusCode, Headers) ->
{ok, IsFin, HeaderBlock, HTTP2Machine}
= cow_http2_machine:prepare_headers(StreamID, HTTP2Machine0, IsFin0,
#{status => cow_http:status_to_integer(StatusCode)},
headers_to_list(Headers)),
- Transport:send(Socket, cow_http2:headers(StreamID, IsFin, HeaderBlock)),
- State#state{http2_machine=HTTP2Machine}.
+ State = State0#state{http2_machine=HTTP2Machine},
+ ok = maybe_socket_error(State, Transport:send(Socket,
+ cow_http2:headers(StreamID, IsFin, HeaderBlock))),
+ State.
%% The set-cookie header is special; we can only send one cookie per header.
headers_to_list(Headers0=#{<<"set-cookie">> := SetCookies}) ->
@@ -818,13 +908,18 @@ maybe_send_data(State0=#state{socket=Socket, transport=Transport,
end,
case cow_http2_machine:send_or_queue_data(StreamID, HTTP2Machine0, IsFin, Data) of
{ok, HTTP2Machine} ->
+ State1 = State0#state{http2_machine=HTTP2Machine},
%% If we have prefix data (like a HEADERS frame) we need to send it
%% even if we do not send any DATA frames.
- case Prefix of
- [] -> ok;
- _ -> Transport:send(Socket, Prefix)
+ WasDataSent = case Prefix of
+ [] ->
+ no_data_sent;
+ _ ->
+ ok = maybe_socket_error(State1, Transport:send(Socket, Prefix)),
+ data_sent
end,
- maybe_send_data_alarm(State0#state{http2_machine=HTTP2Machine}, HTTP2Machine0, StreamID);
+ State = maybe_send_data_alarm(State1, HTTP2Machine0, StreamID),
+ {WasDataSent, State};
{send, SendData, HTTP2Machine} ->
State = #state{http2_status=Status, streams=Streams}
= send_data(State0#state{http2_machine=HTTP2Machine}, SendData, Prefix),
@@ -833,7 +928,7 @@ maybe_send_data(State0=#state{socket=Socket, transport=Transport,
Status =:= closing, Streams =:= #{} ->
terminate(State, {stop, normal, 'The connection is going away.'});
true ->
- maybe_send_data_alarm(State, HTTP2Machine0, StreamID)
+ {data_sent, maybe_send_data_alarm(State, HTTP2Machine0, StreamID)}
end
end.
@@ -842,12 +937,15 @@ send_data(State0=#state{socket=Socket, transport=Transport, opts=Opts}, SendData
_ = [case Data of
{sendfile, Offset, Bytes, Path} ->
%% When sendfile is disabled we explicitly use the fallback.
- _ = case maps:get(sendfile, Opts, true) of
- true -> Transport:sendfile(Socket, Path, Offset, Bytes);
- false -> ranch_transport:sendfile(Transport, Socket, Path, Offset, Bytes, [])
- end;
+ {ok, _} = maybe_socket_error(State,
+ case maps:get(sendfile, Opts, true) of
+ true -> Transport:sendfile(Socket, Path, Offset, Bytes);
+ false -> ranch_transport:sendfile(Transport, Socket, Path, Offset, Bytes, [])
+ end
+ ),
+ ok;
_ ->
- Transport:send(Socket, Data)
+ ok = maybe_socket_error(State, Transport:send(Socket, Data))
end || Data <- Acc],
send_data_terminate(State, SendData).
@@ -946,22 +1044,26 @@ stream_alarm(State, StreamID, Name, Value) ->
%% We may have to cancel streams even if we receive multiple
%% GOAWAY frames as the LastStreamID value may be lower than
%% the one previously received.
+%%
+%% We do not reset the idle timeout on send here. We already
+%% disabled it if we initiated shutdown; and we already reset
+%% it if the client sent a GOAWAY frame.
goaway(State0=#state{socket=Socket, transport=Transport, http2_machine=HTTP2Machine0,
http2_status=Status, streams=Streams0}, {goaway, LastStreamID, Reason, _})
when Status =:= connected; Status =:= closing_initiated; Status =:= closing ->
Streams = goaway_streams(State0, maps:to_list(Streams0), LastStreamID,
{stop, {goaway, Reason}, 'The connection is going away.'}, []),
- State = State0#state{streams=maps:from_list(Streams)},
+ State1 = State0#state{streams=maps:from_list(Streams)},
if
Status =:= connected; Status =:= closing_initiated ->
{OurLastStreamID, HTTP2Machine} =
cow_http2_machine:set_last_streamid(HTTP2Machine0),
- Transport:send(Socket, cow_http2:goaway(
- OurLastStreamID, no_error, <<>>)),
- State#state{http2_status=closing,
- http2_machine=HTTP2Machine};
+ State = State1#state{http2_status=closing, http2_machine=HTTP2Machine},
+ ok = maybe_socket_error(State, Transport:send(Socket,
+ cow_http2:goaway(OurLastStreamID, no_error, <<>>))),
+ State;
true ->
- State
+ State1
end;
%% We terminate the connection immediately if it hasn't fully been initialized.
goaway(State, {goaway, _, Reason, _}) ->
@@ -987,7 +1089,8 @@ goaway_streams(State, [Stream|Tail], LastStreamID, Reason, Acc) ->
-spec initiate_closing(#state{}, _) -> #state{}.
initiate_closing(State=#state{http2_status=connected, socket=Socket,
transport=Transport, opts=Opts}, Reason) ->
- Transport:send(Socket, cow_http2:goaway(16#7fffffff, no_error, <<>>)),
+ ok = maybe_socket_error(State, Transport:send(Socket,
+ cow_http2:goaway(16#7fffffff, no_error, <<>>))),
Timeout = maps:get(goaway_initial_timeout, Opts, 1000),
Message = {goaway_initial_timeout, Reason},
set_timeout(State#state{http2_status=closing_initiated}, Timeout, Message);
@@ -1003,14 +1106,16 @@ initiate_closing(State, Reason) ->
-spec closing(#state{}, Reason :: term()) -> #state{}.
closing(State=#state{streams=Streams}, Reason) when Streams =:= #{} ->
terminate(State, Reason);
-closing(State=#state{http2_status=closing_initiated,
+closing(State0=#state{http2_status=closing_initiated,
http2_machine=HTTP2Machine0, socket=Socket, transport=Transport},
Reason) ->
%% Stop accepting new streams.
{LastStreamID, HTTP2Machine} =
cow_http2_machine:set_last_streamid(HTTP2Machine0),
- Transport:send(Socket, cow_http2:goaway(LastStreamID, no_error, <<>>)),
- closing(State#state{http2_status=closing, http2_machine=HTTP2Machine}, Reason);
+ State = State0#state{http2_status=closing, http2_machine=HTTP2Machine},
+ ok = maybe_socket_error(State, Transport:send(Socket,
+ cow_http2:goaway(LastStreamID, no_error, <<>>))),
+ closing(State, Reason);
closing(State=#state{http2_status=closing, opts=Opts}, Reason) ->
%% If client sent GOAWAY, we may already be in 'closing' but without the
%% goaway complete timeout set.
@@ -1021,7 +1126,20 @@ closing(State=#state{http2_status=closing, opts=Opts}, Reason) ->
stop_reason({stop, Reason, _}) -> Reason;
stop_reason(Reason) -> Reason.
--spec terminate(#state{}, _) -> no_return().
+%% Function copied from cowboy_http.
+maybe_socket_error(State, {error, closed}) ->
+ terminate(State, {socket_error, closed, 'The socket has been closed.'});
+maybe_socket_error(State, Reason) ->
+ maybe_socket_error(State, Reason, 'An error has occurred on the socket.').
+
+maybe_socket_error(_, Result = ok, _) ->
+ Result;
+maybe_socket_error(_, Result = {ok, _}, _) ->
+ Result;
+maybe_socket_error(State, {error, Reason}, Human) ->
+ terminate(State, {socket_error, Reason, Human}).
+
+-spec terminate(#state{} | undefined, _) -> no_return().
terminate(undefined, Reason) ->
exit({shutdown, Reason});
terminate(State=#state{socket=Socket, transport=Transport, http2_status=Status,
@@ -1031,7 +1149,8 @@ terminate(State=#state{socket=Socket, transport=Transport, http2_status=Status,
%% as debug data in the GOAWAY frame here. Perhaps more.
if
Status =:= connected; Status =:= closing_initiated ->
- Transport:send(Socket, cow_http2:goaway(
+ %% We are terminating so it's OK if we can't send the GOAWAY anymore.
+ _ = Transport:send(Socket, cow_http2:goaway(
cow_http2_machine:get_last_streamid(HTTP2Machine),
terminate_reason(Reason), <<>>));
%% We already sent the GOAWAY frame.
@@ -1040,10 +1159,11 @@ terminate(State=#state{socket=Socket, transport=Transport, http2_status=Status,
end,
terminate_all_streams(State, maps:to_list(Streams), Reason),
cowboy_children:terminate(Children),
+ %% @todo Don't linger on connection errors.
terminate_linger(State),
exit({shutdown, Reason});
-terminate(#state{socket=Socket, transport=Transport}, Reason) ->
- Transport:close(Socket),
+%% We are not fully connected so we can just terminate the connection.
+terminate(_State, Reason) ->
exit({shutdown, Reason}).
terminate_reason({connection_error, Reason, _}) -> Reason;
@@ -1077,6 +1197,9 @@ terminate_linger(State=#state{socket=Socket, transport=Transport, opts=Opts}) ->
terminate_linger_before_loop(State, TimerRef, Messages) ->
%% We may already be in active mode when we do this
%% but it's OK because we are shutting down anyway.
+ %%
+ %% We specially handle the socket error to terminate
+ %% when an error occurs.
case setopts_active(State) of
ok ->
terminate_linger_loop(State, TimerRef, Messages);
@@ -1101,13 +1224,18 @@ terminate_linger_loop(State=#state{socket=Socket}, TimerRef, Messages) ->
end.
%% @todo Don't send an RST_STREAM if one was already sent.
+%%
+%% When resetting the stream we are technically sending data
+%% on the socket. However due to implementation complexities
+%% we do not attempt to reset the idle timeout on send.
reset_stream(State0=#state{socket=Socket, transport=Transport,
http2_machine=HTTP2Machine0}, StreamID, Error) ->
Reason = case Error of
{internal_error, _, _} -> internal_error;
{stream_error, Reason0, _} -> Reason0
end,
- Transport:send(Socket, cow_http2:rst_stream(StreamID, Reason)),
+ ok = maybe_socket_error(State0, Transport:send(Socket,
+ cow_http2:rst_stream(StreamID, Reason))),
State1 = case cow_http2_machine:reset_stream(StreamID, HTTP2Machine0) of
{ok, HTTP2Machine} ->
terminate_stream(State0#state{http2_machine=HTTP2Machine}, StreamID, Error);
@@ -1179,7 +1307,8 @@ terminate_stream(State0=#state{socket=Socket, transport=Transport,
http2_machine=HTTP2Machine0}, StreamID) ->
State = case cow_http2_machine:get_stream_local_state(StreamID, HTTP2Machine0) of
{ok, fin, _} ->
- Transport:send(Socket, cow_http2:rst_stream(StreamID, no_error)),
+ ok = maybe_socket_error(State0, Transport:send(Socket,
+ cow_http2:rst_stream(StreamID, no_error))),
{ok, HTTP2Machine} = cow_http2_machine:reset_stream(StreamID, HTTP2Machine0),
State0#state{http2_machine=HTTP2Machine};
{error, closed} ->
diff --git a/src/cowboy_http3.erl b/src/cowboy_http3.erl
new file mode 100644
index 0000000..ef3e3f6
--- /dev/null
+++ b/src/cowboy_http3.erl
@@ -0,0 +1,973 @@
+%% Copyright (c) 2023-2024, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+%% A key difference between cowboy_http2 and cowboy_http3
+%% is that HTTP/3 streams are QUIC streams and therefore
+%% much of the connection state is handled outside of
+%% Cowboy.
+
+-module(cowboy_http3).
+
+-export([init/4]).
+
+%% Temporary callback to do sendfile over QUIC.
+-export([send/2]).
+
+%% @todo Graceful shutdown? Linger? Timeouts? Frame rates? PROXY header?
+-type opts() :: #{
+ compress_buffering => boolean(),
+ compress_threshold => non_neg_integer(),
+ connection_type => worker | supervisor,
+ enable_connect_protocol => boolean(),
+ env => cowboy_middleware:env(),
+ logger => module(),
+ max_decode_blocked_streams => 0..16#3fffffffffffffff,
+ max_decode_table_size => 0..16#3fffffffffffffff,
+ max_encode_blocked_streams => 0..16#3fffffffffffffff,
+ max_encode_table_size => 0..16#3fffffffffffffff,
+ max_ignored_frame_size_received => non_neg_integer() | infinity,
+ metrics_callback => cowboy_metrics_h:metrics_callback(),
+ metrics_req_filter => fun((cowboy_req:req()) -> map()),
+ metrics_resp_headers_filter => fun((cowboy:http_headers()) -> cowboy:http_headers()),
+ middlewares => [module()],
+ shutdown_timeout => timeout(),
+ stream_handlers => [module()],
+ tracer_callback => cowboy_tracer_h:tracer_callback(),
+ tracer_flags => [atom()],
+ tracer_match_specs => cowboy_tracer_h:tracer_match_specs(),
+ %% Open ended because configured stream handlers might add options.
+ _ => _
+}.
+-export_type([opts/0]).
+
+-record(stream, {
+ id :: cow_http3:stream_id(),
+
+ %% Whether the stream is currently in a special state.
+ status :: header | {unidi, control | encoder | decoder}
+ | normal | {data | ignore, non_neg_integer()} | stopping,
+
+ %% Stream buffer.
+ buffer = <<>> :: binary(),
+
+ %% Stream state.
+ state = undefined :: undefined | {module, any()}
+}).
+
+-record(state, {
+ parent :: pid(),
+ ref :: ranch:ref(),
+ conn :: cowboy_quicer:quicer_connection_handle(),
+ opts = #{} :: opts(),
+
+ %% Remote address and port for the connection.
+ peer = undefined :: {inet:ip_address(), inet:port_number()},
+
+ %% Local address and port for the connection.
+ sock = undefined :: {inet:ip_address(), inet:port_number()},
+
+ %% Client certificate.
+ cert :: undefined | binary(),
+
+ %% HTTP/3 state machine.
+ http3_machine :: cow_http3_machine:http3_machine(),
+
+ %% Specially handled local unidi streams.
+ local_control_id = undefined :: undefined | cow_http3:stream_id(),
+ local_encoder_id = undefined :: undefined | cow_http3:stream_id(),
+ local_decoder_id = undefined :: undefined | cow_http3:stream_id(),
+
+ %% Bidirectional streams used for requests and responses,
+ %% as well as unidirectional streams initiated by the client.
+ streams = #{} :: #{cow_http3:stream_id() => #stream{}},
+
+ %% Lingering streams that were recently reset. We may receive
+ %% pending data or messages for these streams a short while
+ %% after they have been reset.
+ lingering_streams = [] :: [non_neg_integer()],
+
+ %% Streams can spawn zero or more children which are then managed
+ %% by this module if operating as a supervisor.
+ children = cowboy_children:init() :: cowboy_children:children()
+}).
+
+-spec init(pid(), ranch:ref(), cowboy_quicer:quicer_connection_handle(), opts())
+ -> no_return().
+
+init(Parent, Ref, Conn, Opts) ->
+ {ok, SettingsBin, HTTP3Machine0} = cow_http3_machine:init(server, Opts),
+ %% Immediately open a control, encoder and decoder stream.
+ %% @todo An endpoint MAY avoid creating an encoder stream if it will not be used (for example, if its encoder does not wish to use the dynamic table or if the maximum size of the dynamic table permitted by the peer is zero).
+ %% @todo An endpoint MAY avoid creating a decoder stream if its decoder sets the maximum capacity of the dynamic table to zero.
+ {ok, ControlID} = maybe_socket_error(undefined,
+ cowboy_quicer:start_unidi_stream(Conn, [<<0>>, SettingsBin]),
+ 'A socket error occurred when opening the control stream.'),
+ {ok, EncoderID} = maybe_socket_error(undefined,
+ cowboy_quicer:start_unidi_stream(Conn, <<2>>),
+ 'A socket error occurred when opening the encoder stream.'),
+ {ok, DecoderID} = maybe_socket_error(undefined,
+ cowboy_quicer:start_unidi_stream(Conn, <<3>>),
+ 'A socket error occurred when opening the encoder stream.'),
+ %% Set the control, encoder and decoder streams in the machine.
+ HTTP3Machine = cow_http3_machine:init_unidi_local_streams(
+ ControlID, EncoderID, DecoderID, HTTP3Machine0),
+ %% Get the peername/sockname/cert.
+ {ok, Peer} = maybe_socket_error(undefined, cowboy_quicer:peername(Conn),
+ 'A socket error occurred when retrieving the peer name.'),
+ {ok, Sock} = maybe_socket_error(undefined, cowboy_quicer:sockname(Conn),
+ 'A socket error occurred when retrieving the sock name.'),
+ CertResult = case cowboy_quicer:peercert(Conn) of
+ {error, no_peercert} ->
+ {ok, undefined};
+ Cert0 ->
+ Cert0
+ end,
+ {ok, Cert} = maybe_socket_error(undefined, CertResult,
+ 'A socket error occurred when retrieving the client TLS certificate.'),
+ %% Quick! Let's go!
+ loop(#state{parent=Parent, ref=Ref, conn=Conn,
+ opts=Opts, peer=Peer, sock=Sock, cert=Cert,
+ http3_machine=HTTP3Machine, local_control_id=ControlID,
+ local_encoder_id=EncoderID, local_decoder_id=DecoderID}).
+
+loop(State0=#state{opts=Opts, children=Children}) ->
+ receive
+ Msg when element(1, Msg) =:= quic ->
+ handle_quic_msg(State0, Msg);
+ %% Timeouts.
+ {timeout, Ref, {shutdown, Pid}} ->
+ cowboy_children:shutdown_timeout(Children, Ref, Pid),
+ loop(State0);
+ %% Messages pertaining to a stream.
+ {{Pid, StreamID}, Msg} when Pid =:= self() ->
+ loop(info(State0, StreamID, Msg));
+ %% Exit signal from children.
+ Msg = {'EXIT', Pid, _} ->
+ loop(down(State0, Pid, Msg));
+ Msg ->
+ cowboy:log(warning, "Received stray message ~p.", [Msg], Opts),
+ loop(State0)
+ end.
+
+handle_quic_msg(State0=#state{opts=Opts}, Msg) ->
+ case cowboy_quicer:handle(Msg) of
+ {data, StreamID, IsFin, Data} ->
+ parse(State0, StreamID, Data, IsFin);
+ {stream_started, StreamID, StreamType} ->
+ State = stream_new_remote(State0, StreamID, StreamType),
+ loop(State);
+ {stream_closed, StreamID, ErrorCode} ->
+ State = stream_closed(State0, StreamID, ErrorCode),
+ loop(State);
+ closed ->
+ %% @todo Different error reason if graceful?
+ Reason = {socket_error, closed, 'The socket has been closed.'},
+ terminate(State0, Reason);
+ ok ->
+ loop(State0);
+ unknown ->
+ cowboy:log(warning, "Received unknown QUIC message ~p.", [Msg], Opts),
+ loop(State0);
+ {socket_error, Reason} ->
+ terminate(State0, {socket_error, Reason,
+ 'An error has occurred on the socket.'})
+ end.
+
+parse(State=#state{opts=Opts}, StreamID, Data, IsFin) ->
+ case stream_get(State, StreamID) of
+ Stream=#stream{buffer= <<>>} ->
+ parse1(State, Stream, Data, IsFin);
+ Stream=#stream{buffer=Buffer} ->
+ Stream1 = Stream#stream{buffer= <<>>},
+ parse1(stream_store(State, Stream1),
+ Stream1, <<Buffer/binary, Data/binary>>, IsFin);
+ %% Pending data for a stream that has been reset. Ignore.
+ error ->
+ case is_lingering_stream(State, StreamID) of
+ true ->
+ ok;
+ false ->
+ %% We avoid logging the data as it could be quite large.
+ cowboy:log(warning, "Received data for unknown stream ~p.",
+ [StreamID], Opts)
+ end,
+ loop(State)
+ end.
+
+parse1(State, Stream=#stream{status=header}, Data, IsFin) ->
+ parse_unidirectional_stream_header(State, Stream, Data, IsFin);
+parse1(State=#state{http3_machine=HTTP3Machine0},
+ #stream{status={unidi, Type}, id=StreamID}, Data, IsFin)
+ when Type =:= encoder; Type =:= decoder ->
+ case cow_http3_machine:unidi_data(Data, IsFin, StreamID, HTTP3Machine0) of
+ {ok, Instrs, HTTP3Machine} ->
+ loop(send_instructions(State#state{http3_machine=HTTP3Machine}, Instrs));
+ {error, Error={connection_error, _, _}, HTTP3Machine} ->
+ terminate(State#state{http3_machine=HTTP3Machine}, Error)
+ end;
+parse1(State, Stream=#stream{status={data, Len}, id=StreamID}, Data, IsFin) ->
+ DataLen = byte_size(Data),
+ if
+ DataLen < Len ->
+ %% We don't have the full frame but this is the end of the
+ %% data we have. So FrameIsFin is equivalent to IsFin here.
+ loop(frame(State, Stream#stream{status={data, Len - DataLen}}, {data, Data}, IsFin));
+ true ->
+ <<Data1:Len/binary, Rest/bits>> = Data,
+ FrameIsFin = is_fin(IsFin, Rest),
+ parse(frame(State, Stream#stream{status=normal}, {data, Data1}, FrameIsFin),
+ StreamID, Rest, IsFin)
+ end;
+parse1(State, Stream=#stream{status={ignore, Len}, id=StreamID}, Data, IsFin) ->
+ DataLen = byte_size(Data),
+ if
+ DataLen < Len ->
+ loop(stream_store(State, Stream#stream{status={ignore, Len - DataLen}}));
+ true ->
+ <<_:Len/binary, Rest/bits>> = Data,
+ parse(stream_store(State, Stream#stream{status=normal}),
+ StreamID, Rest, IsFin)
+ end;
+%% @todo Clause that discards receiving data for stopping streams.
+%% We may receive a few more frames after we abort receiving.
+parse1(State=#state{opts=Opts}, Stream=#stream{id=StreamID}, Data, IsFin) ->
+ case cow_http3:parse(Data) of
+ {ok, Frame, Rest} ->
+ FrameIsFin = is_fin(IsFin, Rest),
+ parse(frame(State, Stream, Frame, FrameIsFin), StreamID, Rest, IsFin);
+ {more, Frame = {data, _}, Len} ->
+ %% We're at the end of the data so FrameIsFin is equivalent to IsFin.
+ case IsFin of
+ nofin ->
+ %% The stream will be stored at the end of processing commands.
+ loop(frame(State, Stream#stream{status={data, Len}}, Frame, nofin));
+ fin ->
+ terminate(State, {connection_error, h3_frame_error,
+ 'Last frame on stream was truncated. (RFC9114 7.1)'})
+ end;
+ {more, ignore, Len} ->
+ %% @todo This setting should be tested.
+ %%
+ %% While the default value doesn't warrant doing a streaming ignore
+ %% (and could work just fine with the 'more' clause), this value
+ %% is configurable and users may want to set it large.
+ MaxIgnoredLen = maps:get(max_ignored_frame_size_received, Opts, 16384),
+ %% We're at the end of the data so FrameIsFin is equivalent to IsFin.
+ case IsFin of
+ nofin when Len < MaxIgnoredLen ->
+ %% We are not processing commands so we must store the stream.
+ %% We also call ignored_frame here; we will not need to call
+ %% it again when ignoring the rest of the data.
+ Stream1 = Stream#stream{status={ignore, Len}},
+ State1 = ignored_frame(State, Stream1),
+ loop(stream_store(State1, Stream1));
+ nofin ->
+ terminate(State, {connection_error, h3_excessive_load,
+ 'Ignored frame larger than limit. (RFC9114 10.5)'});
+ fin ->
+ terminate(State, {connection_error, h3_frame_error,
+ 'Last frame on stream was truncated. (RFC9114 7.1)'})
+ end;
+ {ignore, Rest} ->
+ parse(ignored_frame(State, Stream), StreamID, Rest, IsFin);
+ Error = {connection_error, _, _} ->
+ terminate(State, Error);
+ more when Data =:= <<>> ->
+ %% The buffer was already reset to <<>>.
+ loop(stream_store(State, Stream));
+ more ->
+ %% We're at the end of the data so FrameIsFin is equivalent to IsFin.
+ case IsFin of
+ nofin ->
+ loop(stream_store(State, Stream#stream{buffer=Data}));
+ fin ->
+ terminate(State, {connection_error, h3_frame_error,
+ 'Last frame on stream was truncated. (RFC9114 7.1)'})
+ end
+ end.
+
+%% We may receive multiple frames in a single QUIC packet.
+%% The FIN flag applies to the QUIC packet, not to the frame.
+%% We must therefore only consider the frame to have a FIN
+%% flag if there's no data remaining to be read.
+is_fin(fin, <<>>) -> fin;
+is_fin(_, _) -> nofin.
+
+parse_unidirectional_stream_header(State0=#state{http3_machine=HTTP3Machine0},
+ Stream0=#stream{id=StreamID}, Data, IsFin) ->
+ case cow_http3:parse_unidi_stream_header(Data) of
+ {ok, Type, Rest} when Type =:= control; Type =:= encoder; Type =:= decoder ->
+ case cow_http3_machine:set_unidi_remote_stream_type(
+ StreamID, Type, HTTP3Machine0) of
+ {ok, HTTP3Machine} ->
+ State = State0#state{http3_machine=HTTP3Machine},
+ Stream = Stream0#stream{status={unidi, Type}},
+ parse(stream_store(State, Stream), StreamID, Rest, IsFin);
+ {error, Error={connection_error, _, _}, HTTP3Machine} ->
+ terminate(State0#state{http3_machine=HTTP3Machine}, Error)
+ end;
+ {ok, push, _} ->
+ terminate(State0, {connection_error, h3_stream_creation_error,
+ 'Only servers can push. (RFC9114 6.2.2)'});
+ %% Unknown stream types must be ignored. We choose to abort the
+ %% stream instead of reading and discarding the incoming data.
+ {undefined, _} ->
+ loop(stream_abort_receive(State0, Stream0, h3_stream_creation_error))
+ end.
+
+frame(State=#state{http3_machine=HTTP3Machine0},
+ Stream=#stream{id=StreamID}, Frame, IsFin) ->
+ case cow_http3_machine:frame(Frame, IsFin, StreamID, HTTP3Machine0) of
+ {ok, HTTP3Machine} ->
+ State#state{http3_machine=HTTP3Machine};
+ {ok, {data, Data}, HTTP3Machine} ->
+ data_frame(State#state{http3_machine=HTTP3Machine}, Stream, IsFin, Data);
+ {ok, {headers, Headers, PseudoHeaders, BodyLen}, Instrs, HTTP3Machine} ->
+ headers_frame(send_instructions(State#state{http3_machine=HTTP3Machine}, Instrs),
+ Stream, IsFin, Headers, PseudoHeaders, BodyLen);
+ {ok, {trailers, _Trailers}, Instrs, HTTP3Machine} ->
+ %% @todo Propagate trailers.
+ send_instructions(State#state{http3_machine=HTTP3Machine}, Instrs);
+ {ok, GoAway={goaway, _}, HTTP3Machine} ->
+ goaway(State#state{http3_machine=HTTP3Machine}, GoAway);
+ {error, Error={stream_error, _Reason, _Human}, Instrs, HTTP3Machine} ->
+ State1 = send_instructions(State#state{http3_machine=HTTP3Machine}, Instrs),
+ reset_stream(State1, Stream, Error);
+ {error, Error={connection_error, _, _}, HTTP3Machine} ->
+ terminate(State#state{http3_machine=HTTP3Machine}, Error)
+ end.
+
+data_frame(State=#state{opts=Opts},
+ Stream=#stream{id=StreamID, state=StreamState0}, IsFin, Data) ->
+ try cowboy_stream:data(StreamID, IsFin, Data, StreamState0) of
+ {Commands, StreamState} ->
+ commands(State, Stream#stream{state=StreamState}, Commands)
+ catch Class:Exception:Stacktrace ->
+ cowboy:log(cowboy_stream:make_error_log(data,
+ [StreamID, IsFin, Data, StreamState0],
+ Class, Exception, Stacktrace), Opts),
+ reset_stream(State, Stream, {internal_error, {Class, Exception},
+ 'Unhandled exception in cowboy_stream:data/4.'})
+ end.
+
+headers_frame(State, Stream, IsFin, Headers,
+ PseudoHeaders=#{method := <<"CONNECT">>}, _)
+ when map_size(PseudoHeaders) =:= 2 ->
+ early_error(State, Stream, IsFin, Headers, PseudoHeaders, 501,
+ 'The CONNECT method is currently not implemented. (RFC7231 4.3.6)');
+headers_frame(State, Stream, IsFin, Headers,
+ PseudoHeaders=#{method := <<"TRACE">>}, _) ->
+ early_error(State, Stream, IsFin, Headers, PseudoHeaders, 501,
+ 'The TRACE method is currently not implemented. (RFC9114 4.4, RFC7231 4.3.8)');
+headers_frame(State, Stream, IsFin, Headers, PseudoHeaders=#{authority := Authority}, BodyLen) ->
+ headers_frame_parse_host(State, Stream, IsFin, Headers, PseudoHeaders, BodyLen, Authority);
+headers_frame(State, Stream, IsFin, Headers, PseudoHeaders, BodyLen) ->
+ case lists:keyfind(<<"host">>, 1, Headers) of
+ {_, Authority} ->
+ headers_frame_parse_host(State, Stream, IsFin, Headers, PseudoHeaders, BodyLen, Authority);
+ _ ->
+ reset_stream(State, Stream, {stream_error, h3_message_error,
+ 'Requests translated from HTTP/1.1 must include a host header. (RFC7540 8.1.2.3, RFC7230 5.4)'})
+ end.
+
+headers_frame_parse_host(State=#state{ref=Ref, peer=Peer, sock=Sock, cert=Cert},
+ Stream=#stream{id=StreamID}, IsFin, Headers,
+ PseudoHeaders=#{method := Method, scheme := Scheme, path := PathWithQs},
+ BodyLen, Authority) ->
+ try cow_http_hd:parse_host(Authority) of
+ {Host, Port0} ->
+ Port = ensure_port(Scheme, Port0),
+ try cow_http:parse_fullpath(PathWithQs) of
+ {<<>>, _} ->
+ reset_stream(State, Stream, {stream_error, h3_message_error,
+ 'The path component must not be empty. (RFC7540 8.1.2.3)'});
+ {Path, Qs} ->
+ Req0 = #{
+ ref => Ref,
+ pid => self(),
+ streamid => StreamID,
+ peer => Peer,
+ sock => Sock,
+ cert => Cert,
+ method => Method,
+ scheme => Scheme,
+ host => Host,
+ port => Port,
+ path => Path,
+ qs => Qs,
+ version => 'HTTP/3',
+ headers => headers_to_map(Headers, #{}),
+ has_body => IsFin =:= nofin,
+ body_length => BodyLen
+ },
+ %% We add the protocol information for extended CONNECTs.
+ Req = case PseudoHeaders of
+ #{protocol := Protocol} -> Req0#{protocol => Protocol};
+ _ -> Req0
+ end,
+ headers_frame(State, Stream, Req)
+ catch _:_ ->
+ reset_stream(State, Stream, {stream_error, h3_message_error,
+ 'The :path pseudo-header is invalid. (RFC7540 8.1.2.3)'})
+ end
+ catch _:_ ->
+ reset_stream(State, Stream, {stream_error, h3_message_error,
+ 'The :authority pseudo-header is invalid. (RFC7540 8.1.2.3)'})
+ end.
+
+%% @todo Copied from cowboy_http2.
+%% @todo How to handle "http"?
+ensure_port(<<"http">>, undefined) -> 80;
+ensure_port(<<"https">>, undefined) -> 443;
+ensure_port(_, Port) -> Port.
+
+%% @todo Copied from cowboy_http2.
+%% This function is necessary to properly handle duplicate headers
+%% and the special-case cookie header.
+headers_to_map([], Acc) ->
+ Acc;
+headers_to_map([{Name, Value}|Tail], Acc0) ->
+ Acc = case Acc0 of
+ %% The cookie header does not use proper HTTP header lists.
+ #{Name := Value0} when Name =:= <<"cookie">> ->
+ Acc0#{Name => << Value0/binary, "; ", Value/binary >>};
+ #{Name := Value0} ->
+ Acc0#{Name => << Value0/binary, ", ", Value/binary >>};
+ _ ->
+ Acc0#{Name => Value}
+ end,
+ headers_to_map(Tail, Acc).
+
+headers_frame(State=#state{opts=Opts}, Stream=#stream{id=StreamID}, Req) ->
+ try cowboy_stream:init(StreamID, Req, Opts) of
+ {Commands, StreamState} ->
+ commands(State, Stream#stream{state=StreamState}, Commands)
+ catch Class:Exception:Stacktrace ->
+ cowboy:log(cowboy_stream:make_error_log(init,
+ [StreamID, Req, Opts],
+ Class, Exception, Stacktrace), Opts),
+ reset_stream(State, Stream, {internal_error, {Class, Exception},
+ 'Unhandled exception in cowboy_stream:init/3.'})
+ end.
+
+early_error(State0=#state{ref=Ref, opts=Opts, peer=Peer},
+ Stream=#stream{id=StreamID}, _IsFin, Headers, #{method := Method},
+ StatusCode0, HumanReadable) ->
+ %% We automatically terminate the stream but it is not an error
+ %% per se (at least not in the first implementation).
+ Reason = {stream_error, h3_no_error, HumanReadable},
+ %% The partial Req is minimal for now. We only have one case
+ %% where it can be called (when a method is completely disabled).
+ PartialReq = #{
+ ref => Ref,
+ peer => Peer,
+ method => Method,
+ headers => headers_to_map(Headers, #{})
+ },
+ Resp = {response, StatusCode0, RespHeaders0=#{<<"content-length">> => <<"0">>}, <<>>},
+ try cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts) of
+ {response, StatusCode, RespHeaders, RespBody} ->
+ send_response(State0, Stream, StatusCode, RespHeaders, RespBody)
+ catch Class:Exception:Stacktrace ->
+ cowboy:log(cowboy_stream:make_error_log(early_error,
+ [StreamID, Reason, PartialReq, Resp, Opts],
+ Class, Exception, Stacktrace), Opts),
+ %% We still need to send an error response, so send what we initially
+ %% wanted to send. It's better than nothing.
+ send_headers(State0, Stream, fin, StatusCode0, RespHeaders0)
+ end.
+
+%% Erlang messages.
+
+down(State0=#state{opts=Opts, children=Children0}, Pid, Msg) ->
+ State = case cowboy_children:down(Children0, Pid) of
+ %% The stream was terminated already.
+ {ok, undefined, Children} ->
+ State0#state{children=Children};
+ %% The stream is still running.
+ {ok, StreamID, Children} ->
+ info(State0#state{children=Children}, StreamID, Msg);
+ %% The process was unknown.
+ error ->
+ cowboy:log(warning, "Received EXIT signal ~p for unknown process ~p.~n",
+ [Msg, Pid], Opts),
+ State0
+ end,
+ if
+%% @todo
+% State#state.http2_status =:= closing, State#state.streams =:= #{} ->
+% terminate(State, {stop, normal, 'The connection is going away.'});
+ true ->
+ State
+ end.
+
+info(State=#state{opts=Opts, http3_machine=_HTTP3Machine}, StreamID, Msg) ->
+ case stream_get(State, StreamID) of
+ Stream=#stream{state=StreamState0} ->
+ try cowboy_stream:info(StreamID, Msg, StreamState0) of
+ {Commands, StreamState} ->
+ commands(State, Stream#stream{state=StreamState}, Commands)
+ catch Class:Exception:Stacktrace ->
+ cowboy:log(cowboy_stream:make_error_log(info,
+ [StreamID, Msg, StreamState0],
+ Class, Exception, Stacktrace), Opts),
+ reset_stream(State, Stream, {internal_error, {Class, Exception},
+ 'Unhandled exception in cowboy_stream:info/3.'})
+ end;
+ error ->
+ case is_lingering_stream(State, StreamID) of
+ true ->
+ ok;
+ false ->
+ cowboy:log(warning, "Received message ~p for unknown stream ~p.",
+ [Msg, StreamID], Opts)
+ end,
+ State
+ end.
+
+%% Stream handler commands.
+
+commands(State, Stream, []) ->
+ stream_store(State, Stream);
+%% Error responses are sent only if a response wasn't sent already.
+commands(State=#state{http3_machine=HTTP3Machine}, Stream=#stream{id=StreamID},
+ [{error_response, StatusCode, Headers, Body}|Tail]) ->
+ case cow_http3_machine:get_bidi_stream_local_state(StreamID, HTTP3Machine) of
+ {ok, idle} ->
+ commands(State, Stream, [{response, StatusCode, Headers, Body}|Tail]);
+ _ ->
+ commands(State, Stream, Tail)
+ end;
+%% Send an informational response.
+commands(State0, Stream, [{inform, StatusCode, Headers}|Tail]) ->
+ State = send_headers(State0, Stream, idle, StatusCode, Headers),
+ commands(State, Stream, Tail);
+%% Send response headers.
+commands(State0, Stream, [{response, StatusCode, Headers, Body}|Tail]) ->
+ State = send_response(State0, Stream, StatusCode, Headers, Body),
+ commands(State, Stream, Tail);
+%% Send response headers.
+commands(State0, Stream, [{headers, StatusCode, Headers}|Tail]) ->
+ State = send_headers(State0, Stream, nofin, StatusCode, Headers),
+ commands(State, Stream, Tail);
+%%% Send a response body chunk.
+commands(State0=#state{conn=Conn}, Stream=#stream{id=StreamID}, [{data, IsFin, Data}|Tail]) ->
+ _ = case Data of
+ {sendfile, Offset, Bytes, Path} ->
+ %% Temporary solution to do sendfile over QUIC.
+ {ok, _} = ranch_transport:sendfile(?MODULE, {Conn, StreamID},
+ Path, Offset, Bytes, []),
+ ok = maybe_socket_error(State0,
+ cowboy_quicer:send(Conn, StreamID, cow_http3:data(<<>>), IsFin));
+ _ ->
+ ok = maybe_socket_error(State0,
+ cowboy_quicer:send(Conn, StreamID, cow_http3:data(Data), IsFin))
+ end,
+ State = maybe_send_is_fin(State0, Stream, IsFin),
+ commands(State, Stream, Tail);
+%%% Send trailers.
+commands(State0=#state{conn=Conn, http3_machine=HTTP3Machine0},
+ Stream=#stream{id=StreamID}, [{trailers, Trailers}|Tail]) ->
+ State = case cow_http3_machine:prepare_trailers(
+ StreamID, HTTP3Machine0, maps:to_list(Trailers)) of
+ {trailers, HeaderBlock, Instrs, HTTP3Machine} ->
+ State1 = send_instructions(State0#state{http3_machine=HTTP3Machine}, Instrs),
+ ok = maybe_socket_error(State1,
+ cowboy_quicer:send(Conn, StreamID, cow_http3:headers(HeaderBlock), fin)),
+ State1;
+ {no_trailers, HTTP3Machine} ->
+ ok = maybe_socket_error(State0,
+ cowboy_quicer:send(Conn, StreamID, cow_http3:data(<<>>), fin)),
+ State0#state{http3_machine=HTTP3Machine}
+ end,
+ commands(State, Stream, Tail);
+%% Send a push promise.
+%%
+%% @todo Responses sent as a result of a push_promise request
+%% must not send push_promise frames themselves.
+%%
+%% @todo We should not send push_promise frames when we are
+%% in the closing http2_status.
+%commands(State0=#state{socket=Socket, transport=Transport, http3_machine=HTTP3Machine0},
+% Stream, [{push, Method, Scheme, Host, Port, Path, Qs, Headers0}|Tail]) ->
+% Authority = case {Scheme, Port} of
+% {<<"http">>, 80} -> Host;
+% {<<"https">>, 443} -> Host;
+% _ -> iolist_to_binary([Host, $:, integer_to_binary(Port)])
+% end,
+% PathWithQs = iolist_to_binary(case Qs of
+% <<>> -> Path;
+% _ -> [Path, $?, Qs]
+% end),
+% PseudoHeaders = #{
+% method => Method,
+% scheme => Scheme,
+% authority => Authority,
+% path => PathWithQs
+% },
+% %% We need to make sure the header value is binary before we can
+% %% create the Req object, as it expects them to be flat.
+% Headers = maps:to_list(maps:map(fun(_, V) -> iolist_to_binary(V) end, Headers0)),
+% %% @todo
+% State = case cow_http2_machine:prepare_push_promise(StreamID, HTTP3Machine0,
+% PseudoHeaders, Headers) of
+% {ok, PromisedStreamID, HeaderBlock, HTTP3Machine} ->
+% Transport:send(Socket, cow_http2:push_promise(
+% StreamID, PromisedStreamID, HeaderBlock)),
+% headers_frame(State0#state{http3_machine=HTTP2Machine},
+% PromisedStreamID, fin, Headers, PseudoHeaders, 0);
+% {error, no_push} ->
+% State0
+% end,
+% commands(State, Stream, Tail);
+%%% Read the request body.
+%commands(State0=#state{flow=Flow, streams=Streams}, Stream, [{flow, Size}|Tail]) ->
+commands(State, Stream, [{flow, _Size}|Tail]) ->
+ %% @todo We should tell the QUIC stream to increase its window size.
+% #{StreamID := Stream=#stream{flow=StreamFlow}} = Streams,
+% State = update_window(State0#state{flow=Flow + Size,
+% streams=Streams#{StreamID => Stream#stream{flow=StreamFlow + Size}}},
+% StreamID),
+ commands(State, Stream, Tail);
+%% Supervise a child process.
+commands(State=#state{children=Children}, Stream=#stream{id=StreamID},
+ [{spawn, Pid, Shutdown}|Tail]) ->
+ commands(State#state{children=cowboy_children:up(Children, Pid, StreamID, Shutdown)},
+ Stream, Tail);
+%% Error handling.
+commands(State, Stream, [Error = {internal_error, _, _}|_Tail]) ->
+ %% @todo Do we want to run the commands after an internal_error?
+ %% @todo Do we even allow commands after?
+ %% @todo Only reset when the stream still exists.
+ reset_stream(State, Stream, Error);
+%% Use a different protocol within the stream (CONNECT :protocol).
+%% @todo Make sure we error out when the feature is disabled.
+commands(State0, Stream0=#stream{id=StreamID},
+ [{switch_protocol, Headers, _Mod, _ModState}|Tail]) ->
+ State = info(stream_store(State0, Stream0), StreamID, {headers, 200, Headers}),
+ Stream = stream_get(State, StreamID),
+ commands(State, Stream, Tail);
+%% Set options dynamically.
+commands(State, Stream, [{set_options, _Opts}|Tail]) ->
+ commands(State, Stream, Tail);
+commands(State, Stream, [stop|_Tail]) ->
+ %% @todo Do we want to run the commands after a stop?
+ %% @todo Do we even allow commands after?
+ stop_stream(State, Stream);
+%% Log event.
+commands(State=#state{opts=Opts}, Stream, [Log={log, _, _, _}|Tail]) ->
+ cowboy:log(Log, Opts),
+ commands(State, Stream, Tail).
+
+send_response(State0=#state{conn=Conn, http3_machine=HTTP3Machine0},
+ Stream=#stream{id=StreamID}, StatusCode, Headers, Body) ->
+ Size = case Body of
+ {sendfile, _, Bytes0, _} -> Bytes0;
+ _ -> iolist_size(Body)
+ end,
+ case Size of
+ 0 ->
+ State = send_headers(State0, Stream, fin, StatusCode, Headers),
+ maybe_send_is_fin(State, Stream, fin);
+ _ ->
+ %% @todo Add a test for HEAD to make sure we don't send the body when
+ %% returning {response...} from a stream handler (or {headers...} then {data...}).
+ {ok, _IsFin, HeaderBlock, Instrs, HTTP3Machine}
+ = cow_http3_machine:prepare_headers(StreamID, HTTP3Machine0, nofin,
+ #{status => cow_http:status_to_integer(StatusCode)},
+ headers_to_list(Headers)),
+ State = send_instructions(State0#state{http3_machine=HTTP3Machine}, Instrs),
+ %% @todo It might be better to do async sends.
+ _ = case Body of
+ {sendfile, Offset, Bytes, Path} ->
+ ok = maybe_socket_error(State,
+ cowboy_quicer:send(Conn, StreamID, cow_http3:headers(HeaderBlock))),
+ %% Temporary solution to do sendfile over QUIC.
+ {ok, _} = maybe_socket_error(State,
+ ranch_transport:sendfile(?MODULE, {Conn, StreamID},
+ Path, Offset, Bytes, [])),
+ ok = maybe_socket_error(State,
+ cowboy_quicer:send(Conn, StreamID, cow_http3:data(<<>>), fin));
+ _ ->
+ ok = maybe_socket_error(State,
+ cowboy_quicer:send(Conn, StreamID, [
+ cow_http3:headers(HeaderBlock),
+ cow_http3:data(Body)
+ ], fin))
+ end,
+ maybe_send_is_fin(State, Stream, fin)
+ end.
+
+maybe_send_is_fin(State=#state{http3_machine=HTTP3Machine0},
+ Stream=#stream{id=StreamID}, fin) ->
+ HTTP3Machine = cow_http3_machine:close_bidi_stream_for_sending(StreamID, HTTP3Machine0),
+ maybe_terminate_stream(State#state{http3_machine=HTTP3Machine}, Stream);
+maybe_send_is_fin(State, _, _) ->
+ State.
+
+%% Temporary callback to do sendfile over QUIC.
+-spec send({cowboy_quicer:quicer_connection_handle(), cow_http3:stream_id()},
+ iodata()) -> ok | {error, any()}.
+
+send({Conn, StreamID}, IoData) ->
+ cowboy_quicer:send(Conn, StreamID, cow_http3:data(IoData)).
+
+send_headers(State0=#state{conn=Conn, http3_machine=HTTP3Machine0},
+ #stream{id=StreamID}, IsFin0, StatusCode, Headers) ->
+ {ok, IsFin, HeaderBlock, Instrs, HTTP3Machine}
+ = cow_http3_machine:prepare_headers(StreamID, HTTP3Machine0, IsFin0,
+ #{status => cow_http:status_to_integer(StatusCode)},
+ headers_to_list(Headers)),
+ State = send_instructions(State0#state{http3_machine=HTTP3Machine}, Instrs),
+ ok = maybe_socket_error(State,
+ cowboy_quicer:send(Conn, StreamID, cow_http3:headers(HeaderBlock), IsFin)),
+ State.
+
+%% The set-cookie header is special; we can only send one cookie per header.
+headers_to_list(Headers0=#{<<"set-cookie">> := SetCookies}) ->
+ Headers = maps:to_list(maps:remove(<<"set-cookie">>, Headers0)),
+ Headers ++ [{<<"set-cookie">>, Value} || Value <- SetCookies];
+headers_to_list(Headers) ->
+ maps:to_list(Headers).
+
+%% @todo We would open unidi streams here if we only open on-demand.
+%% No instructions.
+send_instructions(State, undefined) ->
+ State;
+%% Decoder instructions.
+send_instructions(State=#state{conn=Conn, local_decoder_id=DecoderID},
+ {decoder_instructions, DecData}) ->
+ ok = maybe_socket_error(State,
+ cowboy_quicer:send(Conn, DecoderID, DecData)),
+ State;
+%% Encoder instructions.
+send_instructions(State=#state{conn=Conn, local_encoder_id=EncoderID},
+ {encoder_instructions, EncData}) ->
+ ok = maybe_socket_error(State,
+ cowboy_quicer:send(Conn, EncoderID, EncData)),
+ State.
+
+reset_stream(State0=#state{conn=Conn, http3_machine=HTTP3Machine0},
+ Stream=#stream{id=StreamID}, Error) ->
+ Reason = case Error of
+ {internal_error, _, _} -> h3_internal_error;
+ {stream_error, Reason0, _} -> Reason0
+ end,
+ %% @todo Do we want to close both sides?
+ %% @todo Should we close the send side if the receive side was already closed?
+ cowboy_quicer:shutdown_stream(Conn, StreamID,
+ both, cow_http3:error_to_code(Reason)),
+ State1 = case cow_http3_machine:reset_stream(StreamID, HTTP3Machine0) of
+ {ok, HTTP3Machine} ->
+ terminate_stream(State0#state{http3_machine=HTTP3Machine}, Stream, Error);
+ {error, not_found} ->
+ terminate_stream(State0, Stream, Error)
+ end,
+%% @todo
+% case reset_rate(State1) of
+% {ok, State} ->
+% State;
+% error ->
+% terminate(State1, {connection_error, enhance_your_calm,
+% 'Stream reset rate larger than configuration allows. Flood? (CVE-2019-9514)'})
+% end.
+ State1.
+
+stop_stream(State0=#state{http3_machine=HTTP3Machine}, Stream=#stream{id=StreamID}) ->
+ %% We abort reading when stopping the stream but only
+ %% if the client was not finished sending data.
+ %% We mark the stream as 'stopping' either way.
+ State = case cow_http3_machine:get_bidi_stream_remote_state(StreamID, HTTP3Machine) of
+ {ok, fin} ->
+ stream_store(State0, Stream#stream{status=stopping});
+ {error, not_found} ->
+ stream_store(State0, Stream#stream{status=stopping});
+ _ ->
+ stream_abort_receive(State0, Stream, h3_no_error)
+ end,
+ %% Then we may need to send a response or terminate it
+ %% if the stream handler did not do so already.
+ case cow_http3_machine:get_bidi_stream_local_state(StreamID, HTTP3Machine) of
+ %% When the stream terminates normally (without resetting the stream)
+ %% and no response was sent, we need to send a proper response back to the client.
+ {ok, idle} ->
+ info(State, StreamID, {response, 204, #{}, <<>>});
+ %% When a response was sent but not terminated, we need to close the stream.
+ %% We send a final DATA frame to complete the stream.
+ {ok, nofin} ->
+ info(State, StreamID, {data, fin, <<>>});
+ %% When a response was sent fully we can terminate the stream,
+ %% regardless of the stream being in half-closed or closed state.
+ _ ->
+ terminate_stream(State, Stream, normal)
+ end.
+
+maybe_terminate_stream(State, Stream=#stream{status=stopping}) ->
+ terminate_stream(State, Stream, normal);
+%% The Stream will be stored in the State at the end of commands processing.
+maybe_terminate_stream(State, _) ->
+ State.
+
+terminate_stream(State=#state{streams=Streams0, children=Children0},
+ #stream{id=StreamID, state=StreamState}, Reason) ->
+ Streams = maps:remove(StreamID, Streams0),
+ terminate_stream_handler(State, StreamID, Reason, StreamState),
+ Children = cowboy_children:shutdown(Children0, StreamID),
+ stream_linger(State#state{streams=Streams, children=Children}, StreamID).
+
+terminate_stream_handler(#state{opts=Opts}, StreamID, Reason, StreamState) ->
+ try
+ cowboy_stream:terminate(StreamID, Reason, StreamState)
+ catch Class:Exception:Stacktrace ->
+ cowboy:log(cowboy_stream:make_error_log(terminate,
+ [StreamID, Reason, StreamState],
+ Class, Exception, Stacktrace), Opts)
+ end.
+
+ignored_frame(State=#state{http3_machine=HTTP3Machine0}, #stream{id=StreamID}) ->
+ case cow_http3_machine:ignored_frame(StreamID, HTTP3Machine0) of
+ {ok, HTTP3Machine} ->
+ State#state{http3_machine=HTTP3Machine};
+ {error, Error={connection_error, _, _}, HTTP3Machine} ->
+ terminate(State#state{http3_machine=HTTP3Machine}, Error)
+ end.
+
+stream_abort_receive(State=#state{conn=Conn}, Stream=#stream{id=StreamID}, Reason) ->
+ cowboy_quicer:shutdown_stream(Conn, StreamID,
+ receiving, cow_http3:error_to_code(Reason)),
+ stream_store(State, Stream#stream{status=stopping}).
+
+%% @todo Graceful connection shutdown.
+%% We terminate the connection immediately if it hasn't fully been initialized.
+-spec goaway(#state{}, {goaway, _}) -> no_return().
+goaway(State, {goaway, _}) ->
+ terminate(State, {stop, goaway, 'The connection is going away.'}).
+
+%% Function copied from cowboy_http.
+maybe_socket_error(State, {error, closed}) ->
+ terminate(State, {socket_error, closed, 'The socket has been closed.'});
+maybe_socket_error(State, Reason) ->
+ maybe_socket_error(State, Reason, 'An error has occurred on the socket.').
+
+maybe_socket_error(_, Result = ok, _) ->
+ Result;
+maybe_socket_error(_, Result = {ok, _}, _) ->
+ Result;
+maybe_socket_error(State, {error, Reason}, Human) ->
+ terminate(State, {socket_error, Reason, Human}).
+
+-spec terminate(#state{} | undefined, _) -> no_return().
+terminate(undefined, Reason) ->
+ exit({shutdown, Reason});
+terminate(State=#state{conn=Conn, %http3_status=Status,
+ %http3_machine=HTTP3Machine,
+ streams=Streams, children=Children}, Reason) ->
+% if
+% Status =:= connected; Status =:= closing_initiated ->
+%% @todo
+% %% We are terminating so it's OK if we can't send the GOAWAY anymore.
+% _ = cowboy_quicer:send(Conn, ControlID, cow_http3:goaway(
+% cow_http3_machine:get_last_streamid(HTTP3Machine))),
+ %% We already sent the GOAWAY frame.
+% Status =:= closing ->
+% ok
+% end,
+ terminate_all_streams(State, maps:to_list(Streams), Reason),
+ cowboy_children:terminate(Children),
+% terminate_linger(State),
+ _ = cowboy_quicer:shutdown(Conn, cow_http3:error_to_code(terminate_reason(Reason))),
+ exit({shutdown, Reason}).
+
+terminate_reason({connection_error, Reason, _}) -> Reason;
+terminate_reason({stop, _, _}) -> h3_no_error;
+terminate_reason({socket_error, _, _}) -> h3_internal_error.
+%terminate_reason({internal_error, _, _}) -> internal_error.
+
+terminate_all_streams(_, [], _) ->
+ ok;
+terminate_all_streams(State, [{StreamID, #stream{state=StreamState}}|Tail], Reason) ->
+ terminate_stream_handler(State, StreamID, Reason, StreamState),
+ terminate_all_streams(State, Tail, Reason).
+
+stream_get(#state{streams=Streams}, StreamID) ->
+ maps:get(StreamID, Streams, error).
+
+stream_new_remote(State=#state{http3_machine=HTTP3Machine0, streams=Streams},
+ StreamID, StreamType) ->
+ {HTTP3Machine, Status} = case StreamType of
+ unidi ->
+ {cow_http3_machine:init_unidi_stream(StreamID, unidi_remote, HTTP3Machine0),
+ header};
+ bidi ->
+ {cow_http3_machine:init_bidi_stream(StreamID, HTTP3Machine0),
+ normal}
+ end,
+ Stream = #stream{id=StreamID, status=Status},
+ State#state{http3_machine=HTTP3Machine, streams=Streams#{StreamID => Stream}}.
+
+%% Stream closed message for a local (write-only) unidi stream.
+stream_closed(State=#state{local_control_id=StreamID}, StreamID, _) ->
+ stream_closed1(State, StreamID);
+stream_closed(State=#state{local_encoder_id=StreamID}, StreamID, _) ->
+ stream_closed1(State, StreamID);
+stream_closed(State=#state{local_decoder_id=StreamID}, StreamID, _) ->
+ stream_closed1(State, StreamID);
+stream_closed(State=#state{opts=Opts,
+ streams=Streams0, children=Children0}, StreamID, ErrorCode) ->
+ case maps:take(StreamID, Streams0) of
+ {#stream{state=undefined}, Streams} ->
+ %% Unidi stream has no handler/children.
+ stream_closed1(State#state{streams=Streams}, StreamID);
+ %% We only stop bidi streams if the stream was closed with an error
+ %% or the stream was already in the process of stopping.
+ {#stream{status=Status, state=StreamState}, Streams}
+ when Status =:= stopping; ErrorCode =/= 0 ->
+ terminate_stream_handler(State, StreamID, closed, StreamState),
+ Children = cowboy_children:shutdown(Children0, StreamID),
+ stream_closed1(State#state{streams=Streams, children=Children}, StreamID);
+ %% Don't remove a stream that terminated properly but
+ %% has chosen to remain up (custom stream handlers).
+ {_, _} ->
+ stream_closed1(State, StreamID);
+ %% Stream closed message for a stream that has been reset. Ignore.
+ error ->
+ case is_lingering_stream(State, StreamID) of
+ true ->
+ ok;
+ false ->
+ %% We avoid logging the data as it could be quite large.
+ cowboy:log(warning, "Received stream_closed for unknown stream ~p. ~p ~p",
+ [StreamID, self(), Streams0], Opts)
+ end,
+ State
+ end.
+
+stream_closed1(State=#state{http3_machine=HTTP3Machine0}, StreamID) ->
+ case cow_http3_machine:close_stream(StreamID, HTTP3Machine0) of
+ {ok, HTTP3Machine} ->
+ State#state{http3_machine=HTTP3Machine};
+ {error, Error={connection_error, _, _}, HTTP3Machine} ->
+ terminate(State#state{http3_machine=HTTP3Machine}, Error)
+ end.
+
+stream_store(State=#state{streams=Streams}, Stream=#stream{id=StreamID}) ->
+ State#state{streams=Streams#{StreamID => Stream}}.
+
+stream_linger(State=#state{lingering_streams=Lingering0}, StreamID) ->
+ %% We only keep up to 100 streams in this state. @todo Make it configurable?
+ Lingering = [StreamID|lists:sublist(Lingering0, 100 - 1)],
+ State#state{lingering_streams=Lingering}.
+
+is_lingering_stream(#state{lingering_streams=Lingering}, StreamID) ->
+ lists:member(StreamID, Lingering).
diff --git a/src/cowboy_loop.erl b/src/cowboy_loop.erl
index 21eb96e..6859c82 100644
--- a/src/cowboy_loop.erl
+++ b/src/cowboy_loop.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -17,12 +17,15 @@
-export([upgrade/4]).
-export([upgrade/5]).
--export([loop/4]).
+-export([loop/5]).
-export([system_continue/3]).
-export([system_terminate/4]).
-export([system_code_change/4]).
+%% From gen_server.
+-define(is_timeout(X), ((X) =:= infinity orelse (is_integer(X) andalso (X) >= 0))).
+
-callback init(Req, any())
-> {ok | module(), Req, any()}
| {module(), Req, any(), any()}
@@ -41,40 +44,46 @@
-> {ok, Req, Env} | {suspend, ?MODULE, loop, [any()]}
when Req::cowboy_req:req(), Env::cowboy_middleware:env().
upgrade(Req, Env, Handler, HandlerState) ->
- loop(Req, Env, Handler, HandlerState).
+ loop(Req, Env, Handler, HandlerState, infinity).
--spec upgrade(Req, Env, module(), any(), hibernate)
+-spec upgrade(Req, Env, module(), any(), hibernate | timeout())
-> {suspend, ?MODULE, loop, [any()]}
when Req::cowboy_req:req(), Env::cowboy_middleware:env().
upgrade(Req, Env, Handler, HandlerState, hibernate) ->
- suspend(Req, Env, Handler, HandlerState).
+ suspend(Req, Env, Handler, HandlerState);
+upgrade(Req, Env, Handler, HandlerState, Timeout) when ?is_timeout(Timeout) ->
+ loop(Req, Env, Handler, HandlerState, Timeout).
--spec loop(Req, Env, module(), any())
+-spec loop(Req, Env, module(), any(), timeout())
-> {ok, Req, Env} | {suspend, ?MODULE, loop, [any()]}
when Req::cowboy_req:req(), Env::cowboy_middleware:env().
%% @todo Handle system messages.
-loop(Req=#{pid := Parent}, Env, Handler, HandlerState) ->
+loop(Req=#{pid := Parent}, Env, Handler, HandlerState, Timeout) ->
receive
%% System messages.
{'EXIT', Parent, Reason} ->
terminate(Req, Env, Handler, HandlerState, Reason);
{system, From, Request} ->
sys:handle_system_msg(Request, From, Parent, ?MODULE, [],
- {Req, Env, Handler, HandlerState});
+ {Req, Env, Handler, HandlerState, Timeout});
%% Calls from supervisor module.
{'$gen_call', From, Call} ->
cowboy_children:handle_supervisor_call(Call, From, [], ?MODULE),
- loop(Req, Env, Handler, HandlerState);
+ loop(Req, Env, Handler, HandlerState, Timeout);
Message ->
- call(Req, Env, Handler, HandlerState, Message)
+ call(Req, Env, Handler, HandlerState, Timeout, Message)
+ after Timeout ->
+ call(Req, Env, Handler, HandlerState, Timeout, timeout)
end.
-call(Req0, Env, Handler, HandlerState0, Message) ->
+call(Req0, Env, Handler, HandlerState0, Timeout, Message) ->
try Handler:info(Message, Req0, HandlerState0) of
{ok, Req, HandlerState} ->
- loop(Req, Env, Handler, HandlerState);
+ loop(Req, Env, Handler, HandlerState, Timeout);
{ok, Req, HandlerState, hibernate} ->
suspend(Req, Env, Handler, HandlerState);
+ {ok, Req, HandlerState, NewTimeout} when ?is_timeout(NewTimeout) ->
+ loop(Req, Env, Handler, HandlerState, NewTimeout);
{stop, Req, HandlerState} ->
terminate(Req, Env, Handler, HandlerState, stop)
catch Class:Reason:Stacktrace ->
@@ -83,7 +92,7 @@ call(Req0, Env, Handler, HandlerState0, Message) ->
end.
suspend(Req, Env, Handler, HandlerState) ->
- {suspend, ?MODULE, loop, [Req, Env, Handler, HandlerState]}.
+ {suspend, ?MODULE, loop, [Req, Env, Handler, HandlerState, infinity]}.
terminate(Req, Env, Handler, HandlerState, Reason) ->
Result = cowboy_handler:terminate(Reason, Req, HandlerState, Handler),
@@ -91,15 +100,15 @@ terminate(Req, Env, Handler, HandlerState, Reason) ->
%% System callbacks.
--spec system_continue(_, _, {Req, Env, module(), any()})
+-spec system_continue(_, _, {Req, Env, module(), any(), timeout()})
-> {ok, Req, Env} | {suspend, ?MODULE, loop, [any()]}
when Req::cowboy_req:req(), Env::cowboy_middleware:env().
-system_continue(_, _, {Req, Env, Handler, HandlerState}) ->
- loop(Req, Env, Handler, HandlerState).
+system_continue(_, _, {Req, Env, Handler, HandlerState, Timeout}) ->
+ loop(Req, Env, Handler, HandlerState, Timeout).
--spec system_terminate(any(), _, _, {Req, Env, module(), any()})
+-spec system_terminate(any(), _, _, {Req, Env, module(), any(), timeout()})
-> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env().
-system_terminate(Reason, _, _, {Req, Env, Handler, HandlerState}) ->
+system_terminate(Reason, _, _, {Req, Env, Handler, HandlerState, _}) ->
terminate(Req, Env, Handler, HandlerState, Reason).
-spec system_code_change(Misc, _, _, _) -> {ok, Misc}
diff --git a/src/cowboy_metrics_h.erl b/src/cowboy_metrics_h.erl
index 4107aac..27f14d4 100644
--- a/src/cowboy_metrics_h.erl
+++ b/src/cowboy_metrics_h.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_middleware.erl b/src/cowboy_middleware.erl
index 9a739f1..efeef4f 100644
--- a/src/cowboy_middleware.erl
+++ b/src/cowboy_middleware.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2013-2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2013-2024, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_quicer.erl b/src/cowboy_quicer.erl
new file mode 100644
index 0000000..d9bbe1f
--- /dev/null
+++ b/src/cowboy_quicer.erl
@@ -0,0 +1,231 @@
+%% Copyright (c) 2023, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+%% QUIC transport using the emqx/quicer NIF.
+
+-module(cowboy_quicer).
+
+%% Connection.
+-export([peername/1]).
+-export([sockname/1]).
+-export([peercert/1]).
+-export([shutdown/2]).
+
+%% Streams.
+-export([start_unidi_stream/2]).
+-export([send/3]).
+-export([send/4]).
+-export([shutdown_stream/4]).
+
+%% Messages.
+-export([handle/1]).
+
+-ifndef(COWBOY_QUICER).
+
+-spec peername(_) -> no_return().
+peername(_) -> no_quicer().
+
+-spec sockname(_) -> no_return().
+sockname(_) -> no_quicer().
+
+-spec peercert(_) -> no_return().
+peercert(_) -> no_quicer().
+
+-spec shutdown(_, _) -> no_return().
+shutdown(_, _) -> no_quicer().
+
+-spec start_unidi_stream(_, _) -> no_return().
+start_unidi_stream(_, _) -> no_quicer().
+
+-spec send(_, _, _) -> no_return().
+send(_, _, _) -> no_quicer().
+
+-spec send(_, _, _, _) -> no_return().
+send(_, _, _, _) -> no_quicer().
+
+-spec shutdown_stream(_, _, _, _) -> no_return().
+shutdown_stream(_, _, _, _) -> no_quicer().
+
+-spec handle(_) -> no_return().
+handle(_) -> no_quicer().
+
+no_quicer() ->
+ error({no_quicer,
+ "Cowboy must be compiled with environment variable COWBOY_QUICER=1 "
+ "or with compilation flag -D COWBOY_QUICER=1 in order to enable "
+ "QUIC support using the emqx/quic NIF"}).
+
+-else.
+
+%% @todo Make quicer export these types.
+-type quicer_connection_handle() :: reference().
+-export_type([quicer_connection_handle/0]).
+
+-type quicer_app_errno() :: non_neg_integer().
+
+-include_lib("quicer/include/quicer.hrl").
+
+%% Connection.
+
+-spec peername(quicer_connection_handle())
+ -> {ok, {inet:ip_address(), inet:port_number()}}
+ | {error, any()}.
+
+peername(Conn) ->
+ quicer:peername(Conn).
+
+-spec sockname(quicer_connection_handle())
+ -> {ok, {inet:ip_address(), inet:port_number()}}
+ | {error, any()}.
+
+sockname(Conn) ->
+ quicer:sockname(Conn).
+
+-spec peercert(quicer_connection_handle())
+ -> {ok, public_key:der_encoded()}
+ | {error, any()}.
+
+peercert(Conn) ->
+ quicer_nif:peercert(Conn).
+
+-spec shutdown(quicer_connection_handle(), quicer_app_errno())
+ -> ok | {error, any()}.
+
+shutdown(Conn, ErrorCode) ->
+ quicer:shutdown_connection(Conn,
+ ?QUIC_CONNECTION_SHUTDOWN_FLAG_NONE,
+ ErrorCode).
+
+%% Streams.
+
+-spec start_unidi_stream(quicer_connection_handle(), iodata())
+ -> {ok, cow_http3:stream_id()}
+ | {error, any()}.
+
+start_unidi_stream(Conn, HeaderData) ->
+ case quicer:start_stream(Conn, #{
+ active => true,
+ open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}) of
+ {ok, StreamRef} ->
+ case quicer:send(StreamRef, HeaderData) of
+ {ok, _} ->
+ {ok, StreamID} = quicer:get_stream_id(StreamRef),
+ put({quicer_stream, StreamID}, StreamRef),
+ {ok, StreamID};
+ Error ->
+ Error
+ end;
+ {error, Reason1, Reason2} ->
+ {error, {Reason1, Reason2}};
+ Error ->
+ Error
+ end.
+
+-spec send(quicer_connection_handle(), cow_http3:stream_id(), iodata())
+ -> ok | {error, any()}.
+
+send(Conn, StreamID, Data) ->
+ send(Conn, StreamID, Data, nofin).
+
+-spec send(quicer_connection_handle(), cow_http3:stream_id(), iodata(), cow_http:fin())
+ -> ok | {error, any()}.
+
+send(_Conn, StreamID, Data, IsFin) ->
+ StreamRef = get({quicer_stream, StreamID}),
+ Size = iolist_size(Data),
+ case quicer:send(StreamRef, Data, send_flag(IsFin)) of
+ {ok, Size} ->
+ ok;
+ {error, Reason1, Reason2} ->
+ {error, {Reason1, Reason2}};
+ Error ->
+ Error
+ end.
+
+send_flag(nofin) -> ?QUIC_SEND_FLAG_NONE;
+send_flag(fin) -> ?QUIC_SEND_FLAG_FIN.
+
+-spec shutdown_stream(quicer_connection_handle(),
+ cow_http3:stream_id(), both | receiving, quicer_app_errno())
+ -> ok.
+
+shutdown_stream(_Conn, StreamID, Dir, ErrorCode) ->
+ StreamRef = get({quicer_stream, StreamID}),
+ _ = quicer:shutdown_stream(StreamRef, shutdown_flag(Dir), ErrorCode, infinity),
+ ok.
+
+shutdown_flag(both) -> ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT;
+shutdown_flag(receiving) -> ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT_RECEIVE.
+
+%% Messages.
+
+%% @todo Probably should have the Conn given as argument too?
+-spec handle({quic, _, _, _})
+ -> {data, cow_http3:stream_id(), cow_http:fin(), binary()}
+ | {stream_started, cow_http3:stream_id(), unidi | bidi}
+ | {stream_closed, cow_http3:stream_id(), quicer_app_errno()}
+ | closed
+ | ok
+ | unknown
+ | {socket_error, any()}.
+
+handle({quic, Data, StreamRef, #{flags := Flags}}) when is_binary(Data) ->
+ {ok, StreamID} = quicer:get_stream_id(StreamRef),
+ IsFin = case Flags band ?QUIC_RECEIVE_FLAG_FIN of
+ ?QUIC_RECEIVE_FLAG_FIN -> fin;
+ _ -> nofin
+ end,
+ {data, StreamID, IsFin, Data};
+%% QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED.
+handle({quic, new_stream, StreamRef, #{flags := Flags}}) ->
+ case quicer:setopt(StreamRef, active, true) of
+ ok ->
+ {ok, StreamID} = quicer:get_stream_id(StreamRef),
+ put({quicer_stream, StreamID}, StreamRef),
+ StreamType = case quicer:is_unidirectional(Flags) of
+ true -> unidi;
+ false -> bidi
+ end,
+ {stream_started, StreamID, StreamType};
+ {error, Reason} ->
+ {socket_error, Reason}
+ end;
+%% QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE.
+handle({quic, stream_closed, StreamRef, #{error := ErrorCode}}) ->
+ {ok, StreamID} = quicer:get_stream_id(StreamRef),
+ {stream_closed, StreamID, ErrorCode};
+%% QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE.
+handle({quic, closed, Conn, _Flags}) ->
+ _ = quicer:close_connection(Conn),
+ closed;
+%% The following events are currently ignored either because
+%% I do not know what they do or because we do not need to
+%% take action.
+handle({quic, streams_available, _Conn, _Props}) ->
+ ok;
+handle({quic, dgram_state_changed, _Conn, _Props}) ->
+ ok;
+%% QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT
+handle({quic, transport_shutdown, _Conn, _Flags}) ->
+ ok;
+handle({quic, peer_send_shutdown, _StreamRef, undefined}) ->
+ ok;
+handle({quic, send_shutdown_complete, _StreamRef, _IsGraceful}) ->
+ ok;
+handle({quic, shutdown, _Conn, success}) ->
+ ok;
+handle(_Msg) ->
+ unknown.
+
+-endif.
diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl
index 90c5a3a..3f87677 100644
--- a/src/cowboy_req.erl
+++ b/src/cowboy_req.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
%% Copyright (c) 2011, Anthony Ramine <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
@@ -521,7 +521,11 @@ read_body(Req=#{has_read_body := true}, _) ->
read_body(Req, Opts) ->
Length = maps:get(length, Opts, 8000000),
Period = maps:get(period, Opts, 15000),
- Timeout = maps:get(timeout, Opts, Period + 1000),
+ DefaultTimeout = case Period of
+ infinity -> infinity; %% infinity + 1000 = infinity.
+ _ -> Period + 1000
+ end,
+ Timeout = maps:get(timeout, Opts, DefaultTimeout),
Ref = make_ref(),
cast({read_body, self(), Ref, Length, Period}, Req),
receive
@@ -710,10 +714,13 @@ set_resp_cookie(Name, Value, Req, Opts) ->
RespCookies = maps:get(resp_cookies, Req, #{}),
Req#{resp_cookies => RespCookies#{Name => Cookie}}.
-%% @todo We could add has_resp_cookie and delete_resp_cookie now.
+%% @todo We could add has_resp_cookie and unset_resp_cookie now.
-spec set_resp_header(binary(), iodata(), Req)
-> Req when Req::req().
+set_resp_header(<<"set-cookie">>, _, _) ->
+ exit({response_error, invalid_header,
+ 'Response cookies must be set using cowboy_req:set_resp_cookie/3,4.'});
set_resp_header(Name, Value, Req=#{resp_headers := RespHeaders}) ->
Req#{resp_headers => RespHeaders#{Name => Value}};
set_resp_header(Name,Value, Req) ->
@@ -721,6 +728,9 @@ set_resp_header(Name,Value, Req) ->
-spec set_resp_headers(cowboy:http_headers(), Req)
-> Req when Req::req().
+set_resp_headers(#{<<"set-cookie">> := _}, _) ->
+ exit({response_error, invalid_header,
+ 'Response cookies must be set using cowboy_req:set_resp_cookie/3,4.'});
set_resp_headers(Headers, Req=#{resp_headers := RespHeaders}) ->
Req#{resp_headers => maps:merge(RespHeaders, Headers)};
set_resp_headers(Headers, Req) ->
@@ -775,7 +785,11 @@ inform(Status, Req) ->
-spec inform(cowboy:http_status(), cowboy:http_headers(), req()) -> ok.
inform(_, _, #{has_sent_resp := _}) ->
- error(function_clause); %% @todo Better error message.
+ exit({response_error, response_already_sent,
+ 'The final response has already been sent.'});
+inform(_, #{<<"set-cookie">> := _}, _) ->
+ exit({response_error, invalid_header,
+ 'Response cookies must be set using cowboy_req:set_resp_cookie/3,4.'});
inform(Status, Headers, Req) when is_integer(Status); is_binary(Status) ->
cast({inform, Status, Headers}, Req).
@@ -793,7 +807,11 @@ reply(Status, Headers, Req) ->
-spec reply(cowboy:http_status(), cowboy:http_headers(), resp_body(), Req)
-> Req when Req::req().
reply(_, _, _, #{has_sent_resp := _}) ->
- error(function_clause); %% @todo Better error message.
+ exit({response_error, response_already_sent,
+ 'The final response has already been sent.'});
+reply(_, #{<<"set-cookie">> := _}, _, _) ->
+ exit({response_error, invalid_header,
+ 'Response cookies must be set using cowboy_req:set_resp_cookie/3,4.'});
reply(Status, Headers, {sendfile, _, 0, _}, Req)
when is_integer(Status); is_binary(Status) ->
do_reply(Status, Headers#{
@@ -809,20 +827,26 @@ reply(Status, Headers, SendFile = {sendfile, _, Len, _}, Req)
%% Neither status code must include a response body. (RFC7230 3.3)
reply(Status, Headers, Body, Req)
when Status =:= 204; Status =:= 304 ->
- 0 = iolist_size(Body),
- do_reply(Status, Headers, Body, Req);
+ do_reply_ensure_no_body(Status, Headers, Body, Req);
reply(Status = <<"204",_/bits>>, Headers, Body, Req) ->
- 0 = iolist_size(Body),
- do_reply(Status, Headers, Body, Req);
+ do_reply_ensure_no_body(Status, Headers, Body, Req);
reply(Status = <<"304",_/bits>>, Headers, Body, Req) ->
- 0 = iolist_size(Body),
- do_reply(Status, Headers, Body, Req);
+ do_reply_ensure_no_body(Status, Headers, Body, Req);
reply(Status, Headers, Body, Req)
when is_integer(Status); is_binary(Status) ->
do_reply(Status, Headers#{
<<"content-length">> => integer_to_binary(iolist_size(Body))
}, Body, Req).
+do_reply_ensure_no_body(Status, Headers, Body, Req) ->
+ case iolist_size(Body) of
+ 0 ->
+ do_reply(Status, Headers, Body, Req);
+ _ ->
+ exit({response_error, payload_too_large,
+ '204 and 304 responses must not include a body. (RFC7230 3.3)'})
+ end.
+
%% Don't send any body for HEAD responses. While the protocol code is
%% supposed to enforce this rule, we prefer to avoid copying too much
%% data around if we can avoid it.
@@ -843,16 +867,19 @@ stream_reply(Status, Req) ->
-spec stream_reply(cowboy:http_status(), cowboy:http_headers(), Req)
-> Req when Req::req().
stream_reply(_, _, #{has_sent_resp := _}) ->
- error(function_clause);
+ exit({response_error, response_already_sent,
+ 'The final response has already been sent.'});
+stream_reply(_, #{<<"set-cookie">> := _}, _) ->
+ exit({response_error, invalid_header,
+ 'Response cookies must be set using cowboy_req:set_resp_cookie/3,4.'});
%% 204 and 304 responses must NOT send a body. We therefore
%% transform the call to a full response and expect the user
%% to NOT call stream_body/3 afterwards. (RFC7230 3.3)
-stream_reply(Status = 204, Headers=#{}, Req) ->
+stream_reply(Status, Headers=#{}, Req)
+ when Status =:= 204; Status =:= 304 ->
reply(Status, Headers, <<>>, Req);
stream_reply(Status = <<"204",_/bits>>, Headers=#{}, Req) ->
reply(Status, Headers, <<>>, Req);
-stream_reply(Status = 304, Headers=#{}, Req) ->
- reply(Status, Headers, <<>>, Req);
stream_reply(Status = <<"304",_/bits>>, Headers=#{}, Req) ->
reply(Status, Headers, <<>>, Req);
stream_reply(Status, Headers=#{}, Req) when is_integer(Status); is_binary(Status) ->
@@ -896,6 +923,9 @@ stream_events(Events, IsFin, Req=#{has_sent_resp := headers}) ->
stream_body({data, self(), IsFin, cow_sse:events(Events)}, Req).
-spec stream_trailers(cowboy:http_headers(), req()) -> ok.
+stream_trailers(#{<<"set-cookie">> := _}, _) ->
+ exit({response_error, invalid_header,
+ 'Response cookies must be set using cowboy_req:set_resp_cookie/3,4.'});
stream_trailers(Trailers, Req=#{has_sent_resp := headers}) ->
cast({trailers, Trailers}, Req).
@@ -907,6 +937,9 @@ push(Path, Headers, Req) ->
%% @todo Path, Headers, Opts, everything should be in proper binary,
%% or normalized when creating the Req object.
-spec push(iodata(), cowboy:http_headers(), req(), push_opts()) -> ok.
+push(_, _, #{has_sent_resp := _}, _) ->
+ exit({response_error, response_already_sent,
+ 'The final response has already been sent.'});
push(Path, Headers, Req=#{scheme := Scheme0, host := Host0, port := Port0}, Opts) ->
Method = maps:get(method, Opts, <<"GET">>),
Scheme = maps:get(scheme, Opts, Scheme0),
@@ -991,7 +1024,12 @@ filter([], Map, Errors) ->
_ -> {error, Errors}
end;
filter([{Key, Constraints}|Tail], Map, Errors) ->
- filter_constraints(Tail, Map, Errors, Key, maps:get(Key, Map), Constraints);
+ case maps:find(Key, Map) of
+ {ok, Value} ->
+ filter_constraints(Tail, Map, Errors, Key, Value, Constraints);
+ error ->
+ filter(Tail, Map, Errors#{Key => required})
+ end;
filter([{Key, Constraints, Default}|Tail], Map, Errors) ->
case maps:find(Key, Map) of
{ok, Value} ->
diff --git a/src/cowboy_rest.erl b/src/cowboy_rest.erl
index 7d0fe80..fcea71c 100644
--- a/src/cowboy_rest.erl
+++ b/src/cowboy_rest.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -97,7 +97,7 @@
-optional_callbacks([forbidden/2]).
-callback generate_etag(Req, State)
- -> {binary() | {weak | strong, binary()}, Req, State}
+ -> {binary() | {weak | strong, binary()} | undefined, Req, State}
when Req::cowboy_req:req(), State::any().
-optional_callbacks([generate_etag/2]).
@@ -1196,6 +1196,7 @@ if_range(Req=#{headers := #{<<"if-range">> := _, <<"range">> := _}},
if_range(Req, State) ->
range(Req, State).
+%% @todo This can probably be moved to if_range directly.
range(Req, State=#state{ranges_a=[]}) ->
set_resp_body(Req, State);
range(Req, State) ->
@@ -1527,6 +1528,12 @@ generate_etag(Req, State=#state{etag=undefined}) ->
case unsafe_call(Req, State, generate_etag) of
no_call ->
{undefined, Req, State#state{etag=no_call}};
+ %% We allow the callback to return 'undefined'
+ %% to allow conditionally generating etags. We
+ %% handle 'undefined' the same as if the function
+ %% was not exported.
+ {undefined, Req2, State2} ->
+ {undefined, Req2, State2#state{etag=no_call}};
{Etag, Req2, State2} when is_binary(Etag) ->
Etag2 = cow_http_hd:parse_etag(Etag),
{Etag2, Req2, State2#state{etag=Etag2}};
diff --git a/src/cowboy_router.erl b/src/cowboy_router.erl
index 0b7fe41..61c9012 100644
--- a/src/cowboy_router.erl
+++ b/src/cowboy_router.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_static.erl b/src/cowboy_static.erl
index b0cf146..a185ef1 100644
--- a/src/cowboy_static.erl
+++ b/src/cowboy_static.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2013-2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2013-2024, Loïc Hoguin <[email protected]>
%% Copyright (c) 2011, Magnus Klaar <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
diff --git a/src/cowboy_stream.erl b/src/cowboy_stream.erl
index 2dad6d0..6ceb5ba 100644
--- a/src/cowboy_stream.erl
+++ b/src/cowboy_stream.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2015-2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2015-2024, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_stream_h.erl b/src/cowboy_stream_h.erl
index f516f3d..b373344 100644
--- a/src/cowboy_stream_h.erl
+++ b/src/cowboy_stream_h.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2016-2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2016-2024, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -151,6 +151,11 @@ info(StreamID, Exit={'EXIT', Pid, {Reason, Stacktrace}}, State=#state{ref=Ref, p
[Ref, self(), StreamID, Pid, Reason, Stacktrace]}
|Commands0]
end,
+ %% @todo We are trying to send a 500 response before resetting
+ %% the stream. But due to the way the RESET_STREAM frame
+ %% works in QUIC the data may be lost. The problem is
+ %% known and a draft RFC exists at
+ %% https://www.ietf.org/id/draft-ietf-quic-reliable-stream-reset-03.html
do_info(StreamID, Exit, [
{error_response, 500, #{<<"content-length">> => <<"0">>}, <<>>}
|Commands], State);
diff --git a/src/cowboy_sub_protocol.erl b/src/cowboy_sub_protocol.erl
index 6714289..062fd38 100644
--- a/src/cowboy_sub_protocol.erl
+++ b/src/cowboy_sub_protocol.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2013-2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2013-2024, Loïc Hoguin <[email protected]>
%% Copyright (c) 2013, James Fish <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
diff --git a/src/cowboy_sup.erl b/src/cowboy_sup.erl
index d3ac3b0..e37f4cf 100644
--- a/src/cowboy_sup.erl
+++ b/src/cowboy_sup.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_tls.erl b/src/cowboy_tls.erl
index c049ecb..60ab2ed 100644
--- a/src/cowboy_tls.erl
+++ b/src/cowboy_tls.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2015-2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2015-2024, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -33,13 +33,7 @@ start_link(Ref, Transport, Opts) ->
-spec connection_process(pid(), ranch:ref(), module(), cowboy:opts()) -> ok.
connection_process(Parent, Ref, Transport, Opts) ->
- ProxyInfo = case maps:get(proxy_header, Opts, false) of
- true ->
- {ok, ProxyInfo0} = ranch:recv_proxy_header(Ref, 1000),
- ProxyInfo0;
- false ->
- undefined
- end,
+ ProxyInfo = get_proxy_info(Ref, Opts),
{ok, Socket} = ranch:handshake(Ref),
case ssl:negotiated_protocol(Socket) of
{ok, <<"h2">>} ->
@@ -54,3 +48,11 @@ init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, Protocol) ->
supervisor -> process_flag(trap_exit, true)
end,
Protocol:init(Parent, Ref, Socket, Transport, ProxyInfo, Opts).
+
+get_proxy_info(Ref, #{proxy_header := true}) ->
+ case ranch:recv_proxy_header(Ref, 1000) of
+ {ok, ProxyInfo} -> ProxyInfo;
+ {error, closed} -> exit({shutdown, closed})
+ end;
+get_proxy_info(_, _) ->
+ undefined.
diff --git a/src/cowboy_tracer_h.erl b/src/cowboy_tracer_h.erl
index 9a19ae1..b1196fe 100644
--- a/src/cowboy_tracer_h.erl
+++ b/src/cowboy_tracer_h.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_websocket.erl b/src/cowboy_websocket.erl
index e7d8f31..3cc4d30 100644
--- a/src/cowboy_websocket.erl
+++ b/src/cowboy_websocket.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2017, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -103,7 +103,8 @@
%% is trying to upgrade to the Websocket protocol.
-spec is_upgrade_request(cowboy_req:req()) -> boolean().
-is_upgrade_request(#{version := 'HTTP/2', method := <<"CONNECT">>, protocol := Protocol}) ->
+is_upgrade_request(#{version := Version, method := <<"CONNECT">>, protocol := Protocol})
+ when Version =:= 'HTTP/2'; Version =:= 'HTTP/3' ->
<<"websocket">> =:= cowboy_bstr:to_lower(Protocol);
is_upgrade_request(Req=#{version := 'HTTP/1.1', method := <<"GET">>}) ->
ConnTokens = cowboy_req:parse_header(<<"connection">>, Req, []),
@@ -148,13 +149,13 @@ upgrade(Req0=#{version := Version}, Env, Handler, HandlerState, Opts) ->
<<"connection">> => <<"upgrade">>,
<<"upgrade">> => <<"websocket">>
}, Req0), Env};
- %% Use a generic 400 error for HTTP/2.
+ %% Use 501 Not Implemented for HTTP/2 and HTTP/3 as recommended
+ %% by RFC9220 3 (WebSockets Upgrade over HTTP/3).
{error, upgrade_required} ->
- {ok, cowboy_req:reply(400, Req0), Env}
+ {ok, cowboy_req:reply(501, Req0), Env}
catch _:_ ->
%% @todo Probably log something here?
%% @todo Test that we can have 2 /ws 400 status code in a row on the same connection.
- %% @todo Does this even work?
{ok, cowboy_req:reply(400, Req0), Env}
end.
@@ -286,9 +287,12 @@ websocket_handshake(State, Req=#{ref := Ref, pid := Pid, streamid := StreamID},
module() | undefined, any(), binary(),
{#state{}, any()}) -> no_return().
takeover(Parent, Ref, Socket, Transport, _Opts, Buffer,
- {State0=#state{handler=Handler}, HandlerState}) ->
- %% @todo We should have an option to disable this behavior.
- ranch:remove_connection(Ref),
+ {State0=#state{handler=Handler, req=Req}, HandlerState}) ->
+ case Req of
+ #{version := 'HTTP/3'} -> ok;
+ %% @todo We should have an option to disable this behavior.
+ _ -> ranch:remove_connection(Ref)
+ end,
Messages = case Transport of
undefined -> undefined;
_ -> Transport:messages()