Browse Source

WIP more HTTP/3 work

Loïc Hoguin 1 year ago
parent
commit
139c620ec0
4 changed files with 350 additions and 195 deletions
  1. 54 13
      src/cow_http3_machine.erl
  2. 1 1
      src/cowboy.erl
  3. 22 15
      src/cowboy_http3.erl
  4. 273 166
      test/rfc9114_SUITE.erl

+ 54 - 13
src/cow_http3_machine.erl

@@ -18,6 +18,7 @@
 -export([init_unidi_local_streams/7]).
 -export([init_unidi_local_streams/7]).
 -export([init_stream/5]).
 -export([init_stream/5]).
 -export([set_unidi_remote_stream_type/3]).
 -export([set_unidi_remote_stream_type/3]).
+-export([close_stream/2]).
 -export([frame/4]).
 -export([frame/4]).
 -export([prepare_headers/5]).
 -export([prepare_headers/5]).
 -export([reset_stream/2]).
 -export([reset_stream/2]).
@@ -62,6 +63,9 @@
 	%% Currently active HTTP/3 streams. Streams may be initiated either
 	%% Currently active HTTP/3 streams. Streams may be initiated either
 	%% by the client or by the server through PUSH_PROMISE frames.
 	%% by the client or by the server through PUSH_PROMISE frames.
 	streams = #{} :: #{reference() => stream()},
 	streams = #{} :: #{reference() => stream()},
+	%% @todo Maybe merge these two in some kind of control stream state.
+	has_peer_control_stream = false :: boolean(),
+	has_received_peer_settings = false :: boolean(),
 
 
 	%% QPACK decoding and encoding state.
 	%% QPACK decoding and encoding state.
 	decode_state = cow_qpack:init() :: cow_qpack:state(),
 	decode_state = cow_qpack:init() :: cow_qpack:state(),
@@ -96,10 +100,29 @@ init_stream(StreamRef, StreamID, StreamDir, StreamType,
 
 
 -spec set_unidi_remote_stream_type(_, _, _) -> _. %% @todo
 -spec set_unidi_remote_stream_type(_, _, _) -> _. %% @todo
 
 
+set_unidi_remote_stream_type(_, control, State=#http3_machine{has_peer_control_stream=true}) ->
+	{error, {connection_error, h3_stream_creation_error,
+		'There can be only one control stream; yet a second one was opened. (RFC9114 6.2.1)'},
+		State};
 set_unidi_remote_stream_type(StreamRef, Type,
 set_unidi_remote_stream_type(StreamRef, Type,
-		State=#http3_machine{streams=Streams}) ->
+		State=#http3_machine{streams=Streams, has_peer_control_stream=HasControl}) ->
 	#{StreamRef := Stream} = Streams,
 	#{StreamRef := Stream} = Streams,
-	State#http3_machine{streams=Streams#{StreamRef => Stream#stream{type=Type}}}.
+	{ok, State#http3_machine{
+		streams=Streams#{StreamRef => Stream#stream{type=Type}},
+		has_peer_control_stream=HasControl orelse (Type =:= control)
+	}}.
+
+-spec close_stream(_, _) -> _. %% @todo
+
+close_stream(StreamRef, State=#http3_machine{streams=Streams0}) ->
+	case maps:take(StreamRef, Streams0) of
+		{#stream{type=control}, Streams} ->
+			{error, {connection_error, h3_closed_critical_stream,
+				'The control stream was closed. (RFC9114 6.2.1)'},
+				State#http3_machine{streams=Streams}};
+		{_, Streams} ->
+			{ok, State#http3_machine{streams=Streams}}
+	end.
 
 
 -spec frame(_, _, _, _) -> _. %% @todo
 -spec frame(_, _, _, _) -> _. %% @todo
 
 
@@ -107,7 +130,7 @@ frame(Frame, IsFin, StreamRef, State) ->
 	case element(1, Frame) of
 	case element(1, Frame) of
 		data -> data_frame(Frame, IsFin, StreamRef, State);
 		data -> data_frame(Frame, IsFin, StreamRef, State);
 		headers -> headers_frame(Frame, IsFin, StreamRef, State);
 		headers -> headers_frame(Frame, IsFin, StreamRef, State);
-		settings -> {ok, State} %% @todo
+		settings -> settings_frame(Frame, IsFin, StreamRef, State)
 	end.
 	end.
 
 
 %% DATA frame.
 %% DATA frame.
@@ -122,16 +145,18 @@ frame(Frame, IsFin, StreamRef, State) ->
 data_frame(Frame={data, Data}, IsFin, StreamRef, State) ->
 data_frame(Frame={data, Data}, IsFin, StreamRef, State) ->
 	DataLen = byte_size(Data),
 	DataLen = byte_size(Data),
 	case stream_get(StreamRef, State) of
 	case stream_get(StreamRef, State) of
-		Stream = #stream{remote=nofin} ->
+		Stream = #stream{type=req, remote=nofin} ->
 			data_frame(Frame, IsFin, Stream, State, DataLen);
 			data_frame(Frame, IsFin, Stream, State, DataLen);
-		#stream{remote=idle} ->
+		#stream{type=req, remote=idle} ->
 			{error, {connection_error, h3_frame_unexpected,
 			{error, {connection_error, h3_frame_unexpected,
 				'DATA frame received before a HEADERS frame. (RFC9114 4.1)'},
 				'DATA frame received before a HEADERS frame. (RFC9114 4.1)'},
 				State};
 				State};
-		#stream{remote=fin} ->
+		#stream{type=req, remote=fin} ->
 			{error, {connection_error, h3_frame_unexpected,
 			{error, {connection_error, h3_frame_unexpected,
 				'DATA frame received after trailer HEADERS frame. (RFC9114 4.1)'},
 				'DATA frame received after trailer HEADERS frame. (RFC9114 4.1)'},
-				State}
+				State};
+		#stream{type=control} ->
+			control_frame(Frame, State)
 	end.
 	end.
 
 
 data_frame(Frame, IsFin, Stream0=#stream{remote_read_size=StreamRead}, State0, DataLen) ->
 data_frame(Frame, IsFin, Stream0=#stream{remote_read_size=StreamRead}, State0, DataLen) ->
@@ -170,19 +195,21 @@ headers_frame(Frame, IsFin, StreamRef, State=#http3_machine{mode=Mode}) ->
 	end.
 	end.
 
 
 %% @todo We may receive HEADERS before or after DATA.
 %% @todo We may receive HEADERS before or after DATA.
-server_headers_frame(Frame, IsFin, StreamRef, State=#http3_machine{streams=Streams}) ->
-	case Streams of
+server_headers_frame(Frame, IsFin, StreamRef, State) ->
+	case stream_get(StreamRef, State) of
 		%% Headers.
 		%% Headers.
-		#{StreamRef := Stream=#stream{remote=idle}} ->
+		Stream=#stream{type=req, remote=idle} ->
 			headers_decode(Frame, IsFin, Stream, State, request);
 			headers_decode(Frame, IsFin, Stream, State, request);
 		%% Trailers.
 		%% Trailers.
-		#{StreamRef := Stream=#stream{remote=nofin}} ->
+		Stream=#stream{type=req, remote=nofin} ->
 			headers_decode(Frame, IsFin, Stream, State, trailers);
 			headers_decode(Frame, IsFin, Stream, State, trailers);
 		%% Additional frame received after trailers.
 		%% Additional frame received after trailers.
-		#{StreamRef := #stream{remote=fin}} ->
+		#stream{type=req, remote=fin} ->
 			{error, {connection_error, h3_frame_unexpected,
 			{error, {connection_error, h3_frame_unexpected,
 				'HEADERS frame received after trailer HEADERS frame. (RFC9114 4.1)'},
 				'HEADERS frame received after trailer HEADERS frame. (RFC9114 4.1)'},
-				State}
+				State};
+		#stream{type=control} ->
+			control_frame(Frame, State)
 	end.
 	end.
 
 
 %% @todo Check whether connection_error or stream_error fits better.
 %% @todo Check whether connection_error or stream_error fits better.
@@ -412,6 +439,20 @@ trailers_frame(Stream0, State0=#http3_machine{local_decoder_ref=DecoderRef}, Dec
 %				'The total size of DATA frames is different than the content-length. (RFC7540 8.1.2.6)')
 %				'The total size of DATA frames is different than the content-length. (RFC7540 8.1.2.6)')
 %	end.
 %	end.
 
 
+settings_frame(Frame, _IsFin, StreamRef, State) ->
+	case stream_get(StreamRef, State) of
+		#stream{type=control} ->
+			control_frame(Frame, State)
+	end.
+
+control_frame({settings, _Settings}, State=#http3_machine{has_received_peer_settings=false}) ->
+	{ok, State#http3_machine{has_received_peer_settings=true}};
+control_frame(_Frame, State=#http3_machine{has_received_peer_settings=false}) ->
+	{error, {connection_error, h3_missing_settings,
+		'The first frame on the control stream must be a SETTINGS frame. (RFC9114 6.2.1)'},
+		State}.
+%% @todo control_frame(Frame, State=#http3_machine{has_received_peer_settings=true}) ->
+
 
 
 %% Functions for sending a message header or body. Note that
 %% Functions for sending a message header or body. Note that
 %% this module does not send data directly, instead it returns
 %% this module does not send data directly, instead it returns

+ 1 - 1
src/cowboy.erl

@@ -74,7 +74,7 @@ start_quic(TransOpts, ProtoOpts) ->
 	SocketOpts0 = maps:get(socket_opts, TransOpts, []),
 	SocketOpts0 = maps:get(socket_opts, TransOpts, []),
 	SocketOpts = [
 	SocketOpts = [
 		{alpn, ["h3"]}, %% @todo Why not binary?
 		{alpn, ["h3"]}, %% @todo Why not binary?
-		{peer_unidi_stream_count, 100}, %% @todo Good default?
+		{peer_unidi_stream_count, 3}, %% We only need control and QPACK enc/dec.
 		{peer_bidi_stream_count, 100}
 		{peer_bidi_stream_count, 100}
 	|SocketOpts0],
 	|SocketOpts0],
 	{ok, Listen} = quicer:listen(Port, SocketOpts),
 	{ok, Listen} = quicer:listen(Port, SocketOpts),

+ 22 - 15
src/cowboy_http3.erl

@@ -69,7 +69,6 @@
 -spec init(_, _, _) -> no_return().
 -spec init(_, _, _) -> no_return().
 init(Parent, Conn, Opts) ->
 init(Parent, Conn, Opts) ->
 	{ok, SettingsBin, HTTP3Machine0} = cow_http3_machine:init(server, Opts),
 	{ok, SettingsBin, HTTP3Machine0} = cow_http3_machine:init(server, Opts),
-	{ok, Conn} = quicer:async_accept_stream(Conn, []),
 	%% Immediately open a control, encoder and decoder stream.
 	%% Immediately open a control, encoder and decoder stream.
 	{ok, ControlRef} = quicer:start_stream(Conn,
 	{ok, ControlRef} = quicer:start_stream(Conn,
 		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
 		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
@@ -114,8 +113,7 @@ loop(State0=#state{conn=Conn}) ->
 		%% QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED
 		%% QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED
 		{quic, new_stream, StreamRef, #{flags := Flags}} ->
 		{quic, new_stream, StreamRef, #{flags := Flags}} ->
 %			ct:pal("new_stream ~p flags ~p", [StreamRef, Flags]),
 %			ct:pal("new_stream ~p flags ~p", [StreamRef, Flags]),
-			%% Conn does not change.
-			{ok, Conn} = quicer:async_accept_stream(Conn, []),
+			ok = quicer:setopt(StreamRef, active, true),
 			State = stream_new_remote(State0, StreamRef, Flags),
 			State = stream_new_remote(State0, StreamRef, Flags),
 			loop(State);
 			loop(State);
 		%% QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE
 		%% QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE
@@ -213,11 +211,15 @@ parse_unidirectional_stream_header(State0=#state{http3_machine=HTTP3Machine0},
 		Data, Stream0=#stream{ref=StreamRef}, Props) ->
 		Data, Stream0=#stream{ref=StreamRef}, Props) ->
 	case cow_http3:parse_unidi_stream_header(Data) of
 	case cow_http3:parse_unidi_stream_header(Data) of
 		{ok, Type, Rest} when Type =:= control; Type =:= encoder; Type =:= decoder ->
 		{ok, Type, Rest} when Type =:= control; Type =:= encoder; Type =:= decoder ->
-			HTTP3Machine = cow_http3_machine:set_unidi_remote_stream_type(
-				StreamRef, Type, HTTP3Machine0),
-			State = State0#state{http3_machine=HTTP3Machine},
-			Stream = Stream0#stream{status=normal},
-			parse(stream_update(State, Stream), Rest, StreamRef, Props);
+			case cow_http3_machine:set_unidi_remote_stream_type(
+					StreamRef, Type, HTTP3Machine0) of
+				{ok, HTTP3Machine} ->
+					State = State0#state{http3_machine=HTTP3Machine},
+					Stream = Stream0#stream{status=normal},
+					parse(stream_update(State, Stream), Rest, StreamRef, Props);
+				{error, Error={connection_error, _, _}, HTTP3Machine} ->
+					terminate(State0#state{http3_machine=HTTP3Machine}, Error)
+			end;
 		{ok, push, _} ->
 		{ok, push, _} ->
 			terminate(State0, {connection_error, h3_stream_creation_error,
 			terminate(State0, {connection_error, h3_stream_creation_error,
 				'Only servers can push. (RFC9114 6.2.2)'});
 				'Only servers can push. (RFC9114 6.2.2)'});
@@ -554,7 +556,7 @@ reset_stream(State0=#state{http3_machine=HTTP3Machine0}, StreamRef, Error) ->
 	end,
 	end,
 	%% @todo Do we want to close both sides?
 	%% @todo Do we want to close both sides?
 	%% @todo Should we close the send side if the receive side was already closed?
 	%% @todo Should we close the send side if the receive side was already closed?
-	quicer:shutdown_stream(StreamRef, ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT_RECEIVE,
+	quicer:shutdown_stream(StreamRef, ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT,
 		cow_http3:error_to_code(Reason), infinity),
 		cow_http3:error_to_code(Reason), infinity),
 	State1 = case cow_http3_machine:reset_stream(StreamRef, HTTP3Machine0) of
 	State1 = case cow_http3_machine:reset_stream(StreamRef, HTTP3Machine0) of
 		{ok, HTTP3Machine} ->
 		{ok, HTTP3Machine} ->
@@ -676,12 +678,17 @@ stream_new_remote(State=#state{http3_machine=HTTP3Machine0, streams=Streams}, St
 %	ct:pal("new stream ~p ~p", [Stream, HTTP3Machine]),
 %	ct:pal("new stream ~p ~p", [Stream, HTTP3Machine]),
 	State#state{http3_machine=HTTP3Machine, streams=Streams#{StreamRef => Stream}}.
 	State#state{http3_machine=HTTP3Machine, streams=Streams#{StreamRef => Stream}}.
 
 
-stream_closed(State=#state{streams=Streams0}, StreamRef, _Flags) ->
-	%% @todo Some streams may not be bidi or remote. Need to inform cow_http3_machine too.
-	logger:error("stream_closed ~p", [StreamRef]),
-	Streams = maps:remove(StreamRef, Streams0),
-	%% @todo terminate stream
-	State#state{streams=Streams}.
+stream_closed(State=#state{http3_machine=HTTP3Machine0, streams=Streams0},
+		StreamRef, _Flags) ->
+	case cow_http3_machine:close_stream(StreamRef, HTTP3Machine0) of
+		{ok, HTTP3Machine} ->
+			%% @todo Some streams may not be bidi or remote.
+			Streams = maps:remove(StreamRef, Streams0),
+			%% @todo terminate stream
+			State#state{streams=Streams};
+		{error, Error={connection_error, _, _}, HTTP3Machine} ->
+			terminate(State#state{http3_machine=HTTP3Machine}, Error)
+	end.
 
 
 stream_update(State=#state{streams=Streams}, Stream=#stream{ref=StreamRef}) ->
 stream_update(State=#state{streams=Streams}, Stream=#stream{ref=StreamRef}) ->
 	State#state{streams=Streams#{StreamRef => Stream}}.
 	State#state{streams=Streams#{StreamRef => Stream}}.

+ 273 - 166
test/rfc9114_SUITE.erl

@@ -895,7 +895,7 @@ reject_transfer_encoding_header(Config) ->
 
 
 reject_upgrade_header(Config) ->
 reject_upgrade_header(Config) ->
 	doc("Requests containing an upgrade header must be rejected "
 	doc("Requests containing an upgrade header must be rejected "
-		"with an H3_MESSAGE_ERROR stream error. (RFC9114 4.2, RFC9114 4.1.2)"),
+		"with an H3_MESSAGE_ERROR stream error. (RFC9114 4.2, RFC9114 4.5, RFC9114 4.1.2)"),
 	do_reject_malformed_header(Config,
 	do_reject_malformed_header(Config,
 		{<<"upgrade">>, <<"websocket">>}
 		{<<"upgrade">>, <<"websocket">>}
 	).
 	).
@@ -1096,25 +1096,28 @@ reject_missing_pseudo_header_authority(Config) ->
 		{<<":path">>, <<"/">>}
 		{<<":path">>, <<"/">>}
 	]).
 	]).
 
 
-%% @todo
-%accept_host_header_on_missing_pseudo_header_authority(Config) ->
-%	doc("A request without an authority but with a host header must be accepted. "
-%		"(RFC7540 8.1.2.3, RFC7540 8.1.3)"),
-%	{ok, Socket} = do_handshake(Config),
-%	%% Send a HEADERS frame with host header and without an :authority pseudo-header.
-%	{HeadersBlock, _} = cow_hpack:encode([
-%		{<<":method">>, <<"GET">>},
-%		{<<":scheme">>, <<"http">>},
-%		{<<":path">>, <<"/">>},
-%		{<<"host">>, <<"localhost">>}
-%	]),
-%	ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
-%	%% Receive a 200 response.
-%	{ok, << Len:24, 1:8, _:8, _:32 >>} = gen_tcp:recv(Socket, 9, 6000),
-%	{ok, RespHeadersBlock} = gen_tcp:recv(Socket, Len, 6000),
-%	{RespHeaders, _} = cow_hpack:decode(RespHeadersBlock),
-%	{_, <<"200">>} = lists:keyfind(<<":status">>, 1, RespHeaders),
-%	ok.
+accept_host_header_on_missing_pseudo_header_authority(Config) ->
+	doc("A request without an authority but with a host header must be accepted. "
+		"(RFC9114 4.3.1)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, StreamRef} = quicer:start_stream(Conn, #{}),
+	{ok, EncodedHeaders, _EncData1, _EncSt0} = cow_qpack:encode_field_section([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"https">>},
+		{<<":path">>, <<"/">>},
+		{<<"host">>, <<"localhost">>}
+	], 0, cow_qpack:init()),
+	{ok, _} = quicer:send(StreamRef, [
+		<<1>>, %% HEADERS frame.
+		cow_http3:encode_int(iolist_size(EncodedHeaders)),
+		EncodedHeaders
+	]),
+%	ok = do_async_stream_shutdown(StreamRef),
+	#{
+		headers := #{<<":status">> := <<"200">>},
+		body := <<"Hello world!">>
+	} = do_receive_response(StreamRef),
+	ok.
 
 
 %% @todo
 %% @todo
 %% If the :scheme pseudo-header field identifies a scheme that has a mandatory
 %% If the :scheme pseudo-header field identifies a scheme that has a mandatory
@@ -1154,19 +1157,6 @@ reject_many_pseudo_header_path(Config) ->
 		{<<":path">>, <<"/">>}
 		{<<":path">>, <<"/">>}
 	]).
 	]).
 
 
-
-
-
-
-
-
-
-
-
-
-
-
-
 do_reject_malformed_header(Config, Header) ->
 do_reject_malformed_header(Config, Header) ->
 	do_reject_malformed_headers(Config, [
 	do_reject_malformed_headers(Config, [
 		{<<":method">>, <<"GET">>},
 		{<<":method">>, <<"GET">>},
@@ -1190,21 +1180,249 @@ do_reject_malformed_headers(Config, Headers) ->
 	#{reason := h3_message_error} = do_wait_stream_aborted(StreamRef),
 	#{reason := h3_message_error} = do_wait_stream_aborted(StreamRef),
 	ok.
 	ok.
 
 
+%% For responses, a single ":status" pseudo-header field is defined that
+%% carries the HTTP status code; see Section 15 of [HTTP]. This pseudo-header
+%% field MUST be included in all responses; otherwise, the response is malformed
+%% (see Section 4.1.2).
+
+%% @todo Implement CONNECT. (RFC9114 4.4. The CONNECT Method)
+
+%% @todo Maybe block the sending of 101 responses? (RFC9114 4.5. HTTP Upgrade) - also HTTP/2.
+
+%% @todo Implement server push (RFC9114 4.6. Server Push)
+
+%% @todo 5.2 Connection Shutdown - need a way to list connections.
+%% @todo 5.3. Immediate Application Closure
+
+bidi_allow_at_least_a_hundred(Config) ->
+	doc("Endpoints must allow the peer to create at least "
+		"one hundred bidirectional streams. (RFC9114 6.1"),
+	#{conn := Conn} = do_connect(Config),
+	receive
+		{quic, streams_available, Conn, #{bidi_streams := NumStreams}} ->
+			true = NumStreams >= 100,
+			ok
+	after 5000 ->
+		error(timeout)
+	end.
+
+unidi_allow_at_least_three(Config) ->
+	doc("Endpoints must allow the peer to create at least "
+		"three unidirectional streams. (RFC9114 6.2"),
+	#{conn := Conn} = do_connect(Config),
+	%% Confirm that the server advertised support for at least 3 unidi streams.
+	receive
+		{quic, streams_available, Conn, #{unidi_streams := NumStreams}} ->
+			true = NumStreams >= 3,
+			ok
+	after 5000 ->
+		error(timeout)
+	end,
+	%% Confirm that we can create the unidi streams.
+	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(server, #{}),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, _} = quicer:send(ControlRef, [<<0>>, SettingsBin]),
+	{ok, EncoderRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, _} = quicer:send(EncoderRef, <<2>>),
+	{ok, DecoderRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, _} = quicer:send(DecoderRef, <<3>>),
+	%% Streams shouldn't get closed.
+	receive
+		Msg ->
+			error(Msg)
+	after 1000 ->
+		ok
+	end.
+
+unidi_create_critical_first(Config) ->
+	doc("Endpoints should create the HTTP control stream as well as "
+		"the QPACK encoder and decoder streams first. (RFC9114 6.2"),
+	%% The control stream is accepted in the do_connect/1 function.
+	#{conn := Conn} = do_connect(Config, #{peer_unidi_stream_count => 3}),
+	Unidi1 = do_accept_qpack_stream(Conn),
+	Unidi2 = do_accept_qpack_stream(Conn),
+	case {Unidi1, Unidi2} of
+		{{encoder, _}, {decoder, _}} ->
+			ok;
+		{{decoder, _}, {encoder, _}} ->
+			ok
+	end.
+
+do_accept_qpack_stream(Conn) ->
+	receive
+		{quic, new_stream, StreamRef, #{flags := Flags}} ->
+			ok = quicer:setopt(StreamRef, active, true),
+			true = quicer:is_unidirectional(Flags),
+			receive {quic, <<Type>>, StreamRef, _} ->
+				{case Type of
+					2 -> encoder;
+					3 -> decoder
+				end, StreamRef}
+			after 5000 ->
+				error(timeout)
+			end
+	after 5000 ->
+		error(timeout)
+	end.
+
+%% @todo We should also confirm that there's at least 1,024 bytes of
+%%       flow-control credit for each unidi stream the server creates. (How?)
+%%       It can be set via stream_recv_window_default in quicer.
+
+%% Recipients of unknown stream types MUST either abort reading of the stream
+%% or discard incoming data without further processing. If reading is aborted,
+%% the recipient SHOULD use the H3_STREAM_CREATION_ERROR error code or a reserved
+%% error code (Section 8.1). The recipient MUST NOT consider unknown stream types
+%% to be a connection error of any kind.
+%% @todo Cowboy limits the number of unidi streams to 3. But trying to create
+%%       more streams doesn't seem to generate an error from QUIC, it swallows it.
+
+%% As certain stream types can affect connection state, a recipient SHOULD NOT
+%% discard data from incoming unidirectional streams prior to reading the stream type.
+
+%% Implementations MAY send stream types before knowing whether the peer
+%supports them. However, stream types that could modify the state or semantics
+%of existing protocol components, including QPACK or other extensions, MUST NOT
+%be sent until the peer is known to support them.
+%% @todo It may make sense for Cowboy to delay the creation of unidi streams
+%%       a little in order to save resources. We could create them when the
+%%       client does as well, or something similar.
+
+%% A receiver MUST tolerate unidirectional streams being closed or reset prior
+%% to the reception of the unidirectional stream header.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+%% A control stream is indicated by a stream type of 0x00. Data on this stream
+%% consists of HTTP/3 frames, as defined in Section 7.2.
+
+%% Each side MUST initiate a single control stream at the beginning of the
+%% connection and send its SETTINGS frame as the first frame on this stream.
+%% @todo What to do when the client never opens a control stream?
+%% @todo Similarly, a stream could be opened but with no data being sent.
+%% @todo Similarly, a control stream could be opened with no SETTINGS frame sent.
+
+
+
+
+
+%% If
+%% the first frame of the control stream is any other frame type, this MUST be
+%% treated as a connection error of type H3_MISSING_SETTINGS.
+
+control_reject_first_frame_data(Config) ->
+	doc("The first frame on a control stream "
+		"must be a SETTINGS frame. (RFC9114 6.2.1)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, _} = quicer:send(ControlRef, [<<0>>, <<0, 12, "Hello world!">>]),
+	%% The connection should have been closed.
+	#{reason := h3_missing_settings} = do_wait_connection_closed(Conn),
+	ok.
+
+%% @todo
+%control_reject_first_frame_headers(Config) ->
+%control_reject_first_frame_cancel_push(Config) ->
+%control_reject_first_frame_push_promise(Config) ->
+%control_accept_first_frame_settings(Config) ->
+%control_reject_first_frame_goaway(Config) ->
+%control_reject_first_frame_max_push_id(Config) ->
+%control_reject_first_frame_reserved(Config) ->
 
 
 
 
 
 
 
 
 
 
+control_reject_multiple(Config) ->
+	doc("Endpoints must not create multiple control streams. (RFC9114 6.2.1)"),
+	#{conn := Conn} = do_connect(Config),
+	%% Create two control streams.
+	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(server, #{}),
+	{ok, ControlRef1} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, _} = quicer:send(ControlRef1, [<<0>>, SettingsBin]),
+	{ok, ControlRef2} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, _} = quicer:send(ControlRef2, [<<0>>, SettingsBin]),
+	%% The connection should have been closed.
+	#{reason := h3_stream_creation_error} = do_wait_connection_closed(Conn),
+	ok.
 
 
+control_local_closed_abort(Config) ->
+	doc("Endpoints must not close the control stream. (RFC9114 6.2.1)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(server, #{}),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, _} = quicer:send(ControlRef, [<<0>>, SettingsBin]),
+	%% Wait a little to make sure the stream data was received before we abort.
+	timer:sleep(100),
+	%% Close the control stream.
+	quicer:async_shutdown_stream(ControlRef, ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT, 0),
+	%% The connection should have been closed.
+	#{reason := h3_closed_critical_stream} = do_wait_connection_closed(Conn),
+	ok.
 
 
+control_local_closed_graceful(Config) ->
+	doc("Endpoints must not close the control stream. (RFC9114 6.2.1)"),
+	#{conn := Conn} = do_connect(Config),
+	{ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(server, #{}),
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	{ok, _} = quicer:send(ControlRef, [<<0>>, SettingsBin]),
+	%% Close the control stream.
+	quicer:async_shutdown_stream(ControlRef, ?QUIC_STREAM_SHUTDOWN_FLAG_GRACEFUL, 0),
+	%% The connection should have been closed.
+	#{reason := h3_closed_critical_stream} = do_wait_connection_closed(Conn),
+	ok.
 
 
+control_remote_closed_abort(Config) ->
+	doc("Endpoints must not close the control stream. (RFC9114 6.2.1)"),
+	#{conn := Conn, control := ControlRef} = do_connect(Config),
+	%% Close the control stream.
+	quicer:async_shutdown_stream(ControlRef, ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT, 0),
+	%% The connection should have been closed.
+	#{reason := h3_closed_critical_stream} = do_wait_connection_closed(Conn),
+	ok.
 
 
+%% We cannot gracefully shutdown a remote unidi stream; only abort reading.
 
 
 
 
 
 
 
 
 
 
 
 
+%% Because the contents of the control stream are used to manage the behavior
+%of other streams, endpoints SHOULD provide enough flow-control credit to keep
+%the peer's control stream from becoming blocked.
+
+%% 2 control streams => error
+%% no stream type sent (= no control stream)
+%% no settings frame sent
+%% another frame sent instead of settings
+%% close control stream
+%% flow control?
+
+
+
+
+
 
 
 
 
 
 
@@ -1218,20 +1436,23 @@ do_reject_malformed_headers(Config, Headers) ->
 %% Helper functions.
 %% Helper functions.
 
 
 do_connect(Config) ->
 do_connect(Config) ->
+	do_connect(Config, #{}).
+
+do_connect(Config, Opts) ->
 	{ok, Conn} = quicer:connect("localhost", config(port, Config),
 	{ok, Conn} = quicer:connect("localhost", config(port, Config),
-		#{alpn => ["h3"], verify => none}, 5000),
+		Opts#{alpn => ["h3"], verify => none}, 5000),
 	%% To make sure the connection is fully established we wait
 	%% To make sure the connection is fully established we wait
 	%% to receive the SETTINGS frame on the control stream.
 	%% to receive the SETTINGS frame on the control stream.
 	{ok, ControlRef, _Settings} = do_wait_settings(Conn),
 	{ok, ControlRef, _Settings} = do_wait_settings(Conn),
 	#{
 	#{
 		conn => Conn,
 		conn => Conn,
-		control => ControlRef
+		control => ControlRef %% This is the peer control stream.
 	}.
 	}.
 
 
 do_wait_settings(Conn) ->
 do_wait_settings(Conn) ->
-	{ok, Conn} = quicer:async_accept_stream(Conn, []),
 	receive
 	receive
 		{quic, new_stream, StreamRef, #{flags := Flags}} ->
 		{quic, new_stream, StreamRef, #{flags := Flags}} ->
+			ok = quicer:setopt(StreamRef, active, true),
 			true = quicer:is_unidirectional(Flags),
 			true = quicer:is_unidirectional(Flags),
 			receive {quic, <<
 			receive {quic, <<
 				0, %% Control stream.
 				0, %% Control stream.
@@ -1325,7 +1546,7 @@ do_receive_response(StreamRef) ->
 	>> = Rest,
 	>> = Rest,
 	BodyLen = integer_to_binary(byte_size(Body)),
 	BodyLen = integer_to_binary(byte_size(Body)),
 	ok = do_wait_peer_send_shutdown(StreamRef),
 	ok = do_wait_peer_send_shutdown(StreamRef),
-	ok = do_wait_stream_closed(StreamRef),
+%	ok = do_wait_stream_closed(StreamRef),
 	#{
 	#{
 		headers => Headers,
 		headers => Headers,
 		body => Body
 		body => Body
@@ -1343,84 +1564,6 @@ do_wait_connection_closed(Conn) ->
 
 
 
 
 
 
-
-%% 4.3.2. Response Pseudo-Header Fields
-%% For responses, a single ":status" pseudo-header field is defined that
-%carries the HTTP status code; see Section 15 of [HTTP]. This pseudo-header
-%field MUST be included in all responses; otherwise, the response is malformed
-%(see Section 4.1.2).
-%% HTTP/3 does not define a way to carry the version or reason phrase that is
-%included in an HTTP/1.1 status line. HTTP/3 responses implicitly have a
-%protocol version of "3.0".
-
-%% 4.4. The CONNECT Method
-%% A CONNECT request MUST be constructed as follows:
-%%The :method pseudo-header field is set to "CONNECT"
-%%The :scheme and :path pseudo-header fields are omitted
-%%The :authority pseudo-header field contains the host and port to connect to
-%(equivalent to the authority-form of the request-target of CONNECT requests;
-%see Section 7.1 of [HTTP]).
-%% The request stream remains open at the end of the request to carry the data
-%to be transferred. A CONNECT request that does not conform to these
-%restrictions is malformed.
-%%
-%% Once the CONNECT method has completed, only DATA frames are permitted to be
-%sent on the stream. Extension frames MAY be used if specifically permitted by
-%the definition of the extension. Receipt of any other known frame type MUST be
-%treated as a connection error of type H3_FRAME_UNEXPECTED.%% @todo + review
-%how it should work beyond the handling of the CONNECT request
-
-%% 4.5. HTTP Upgrade
-%% HTTP/3 does not support the HTTP Upgrade mechanism (Section 7.8 of [HTTP])
-%or the 101 (Switching Protocols) informational status code (Section 15.2.2 of
-%[HTTP]).
-
-%% 4.6. Server Push
-%% The push ID space begins at zero and ends at a maximum value set by the
-%MAX_PUSH_ID frame. In particular, a server is not able to push until after the
-%client sends a MAX_PUSH_ID frame. A client sends MAX_PUSH_ID frames to control
-%the number of pushes that a server can promise. A server SHOULD use push IDs
-%sequentially, beginning from zero. A client MUST treat receipt of a push
-%stream as a connection error of type H3_ID_ERROR when no MAX_PUSH_ID frame has
-%been sent or when the stream references a push ID that is greater than the
-%maximum push ID.
-%% When the same push ID is promised on multiple request streams, the
-%decompressed request field sections MUST contain the same fields in the same
-%order, and both the name and the value in each field MUST be identical.
-%% Not all requests can be pushed. A server MAY push requests that have the following properties:
-%cacheable; see Section 9.2.3 of [HTTP]
-%safe; see Section 9.2.1 of [HTTP]
-%does not include request content or a trailer section
-%
-%% The server MUST include a value in the :authority pseudo-header field for
-%which the server is authoritative. If the client has not yet validated the
-%connection for the origin indicated by the pushed request, it MUST perform the
-%same verification process it would do before sending a request for that origin
-%on the connection; see Section 3.3. If this verification fails, the client
-%MUST NOT consider the server authoritative for that origin.
-%% Clients SHOULD send a CANCEL_PUSH frame upon receipt of a PUSH_PROMISE frame
-%carrying a request that is not cacheable, is not known to be safe, that
-%indicates the presence of request content, or for which it does not consider
-%the server authoritative. Any corresponding responses MUST NOT be used or
-%cached.
-%% Ordering of a PUSH_PROMISE frame in relation to certain parts of the
-%response is important. The server SHOULD send PUSH_PROMISE frames prior to
-%sending HEADERS or DATA frames that reference the promised responses. This
-%reduces the chance that a client requests a resource that will be pushed by
-%the server.
-%% Push stream data can also arrive after a client has cancelled a push. In
-%this case, the client can abort reading the stream with an error code of
-%H3_REQUEST_CANCELLED. This asks the server not to transfer additional data and
-%indicates that it will be discarded upon receipt.
-
-%% 5. Connection Closure
-%% 5.1. Idle Connections
-%% HTTP/3 implementations will need to open a new HTTP/3 connection for new
-%requests if the existing connection has been idle for longer than the idle
-%timeout negotiated during the QUIC handshake, and they SHOULD do so if
-%approaching the idle timeout; see Section 10.1 of [QUIC-TRANSPORT].
-%% Servers SHOULD NOT actively keep connections open.
-
 %% 5.2. Connection Shutdown
 %% 5.2. Connection Shutdown
 %% Endpoints initiate the graceful shutdown of an HTTP/3 connection by sending
 %% Endpoints initiate the graceful shutdown of an HTTP/3 connection by sending
 %a GOAWAY frame. The GOAWAY frame contains an identifier that indicates to the
 %a GOAWAY frame. The GOAWAY frame contains an identifier that indicates to the
@@ -1429,11 +1572,13 @@ do_wait_connection_closed(Conn) ->
 %the client sends a push ID. Requests or pushes with the indicated identifier
 %the client sends a push ID. Requests or pushes with the indicated identifier
 %or greater are rejected (Section 4.1.1) by the sender of the GOAWAY. This
 %or greater are rejected (Section 4.1.1) by the sender of the GOAWAY. This
 %identifier MAY be zero if no requests or pushes were processed.
 %identifier MAY be zero if no requests or pushes were processed.
+
 %% Upon sending a GOAWAY frame, the endpoint SHOULD explicitly cancel (see
 %% Upon sending a GOAWAY frame, the endpoint SHOULD explicitly cancel (see
 %Sections 4.1.1 and 7.2.3) any requests or pushes that have identifiers greater
 %Sections 4.1.1 and 7.2.3) any requests or pushes that have identifiers greater
 %than or equal to the one indicated, in order to clean up transport state for
 %than or equal to the one indicated, in order to clean up transport state for
 %the affected streams. The endpoint SHOULD continue to do so as more requests
 %the affected streams. The endpoint SHOULD continue to do so as more requests
 %or pushes arrive.
 %or pushes arrive.
+
 %% Endpoints MUST NOT initiate new requests or promise new pushes on the
 %% Endpoints MUST NOT initiate new requests or promise new pushes on the
 %connection after receipt of a GOAWAY frame from the peer.
 %connection after receipt of a GOAWAY frame from the peer.
 %% Requests on stream IDs less than the stream ID in a GOAWAY frame from the
 %% Requests on stream IDs less than the stream ID in a GOAWAY frame from the
@@ -1441,31 +1586,39 @@ do_wait_connection_closed(Conn) ->
 %response is received, the stream is reset individually, another GOAWAY is
 %response is received, the stream is reset individually, another GOAWAY is
 %received with a lower stream ID than that of the request in question, or the
 %received with a lower stream ID than that of the request in question, or the
 %connection terminates.
 %connection terminates.
+
 %% Servers MAY reject individual requests on streams below the indicated ID if
 %% Servers MAY reject individual requests on streams below the indicated ID if
 %these requests were not processed.
 %these requests were not processed.
+
 %% If a server receives a GOAWAY frame after having promised pushes with a push
 %% If a server receives a GOAWAY frame after having promised pushes with a push
 %ID greater than or equal to the identifier contained in the GOAWAY frame,
 %ID greater than or equal to the identifier contained in the GOAWAY frame,
 %those pushes will not be accepted.
 %those pushes will not be accepted.
+
 %% Servers SHOULD send a GOAWAY frame when the closing of a connection is known
 %% Servers SHOULD send a GOAWAY frame when the closing of a connection is known
 %in advance, even if the advance notice is small, so that the remote peer can
 %in advance, even if the advance notice is small, so that the remote peer can
 %know whether or not a request has been partially processed.
 %know whether or not a request has been partially processed.
+
 %% An endpoint MAY send multiple GOAWAY frames indicating different
 %% An endpoint MAY send multiple GOAWAY frames indicating different
 %identifiers, but the identifier in each frame MUST NOT be greater than the
 %identifiers, but the identifier in each frame MUST NOT be greater than the
 %identifier in any previous frame, since clients might already have retried
 %identifier in any previous frame, since clients might already have retried
 %unprocessed requests on another HTTP connection. Receiving a GOAWAY containing
 %unprocessed requests on another HTTP connection. Receiving a GOAWAY containing
 %a larger identifier than previously received MUST be treated as a connection
 %a larger identifier than previously received MUST be treated as a connection
 %error of type H3_ID_ERROR.
 %error of type H3_ID_ERROR.
+
 %% An endpoint that is attempting to gracefully shut down a connection can send
 %% An endpoint that is attempting to gracefully shut down a connection can send
-%a GOAWAY frame with a value set to the maximum possible value (262-4 for
-%servers, 262-1 for clients).
+%a GOAWAY frame with a value set to the maximum possible value (2^62-4 for
+%servers, 2^62-1 for clients).
+
 %% Even when a GOAWAY indicates that a given request or push will not be
 %% Even when a GOAWAY indicates that a given request or push will not be
 %processed or accepted upon receipt, the underlying transport resources still
 %processed or accepted upon receipt, the underlying transport resources still
 %exist. The endpoint that initiated these requests can cancel them to clean up
 %exist. The endpoint that initiated these requests can cancel them to clean up
 %transport state.
 %transport state.
+
 %% Once all accepted requests and pushes have been processed, the endpoint can
 %% Once all accepted requests and pushes have been processed, the endpoint can
 %permit the connection to become idle, or it MAY initiate an immediate closure
 %permit the connection to become idle, or it MAY initiate an immediate closure
 %of the connection. An endpoint that completes a graceful shutdown SHOULD use
 %of the connection. An endpoint that completes a graceful shutdown SHOULD use
 %the H3_NO_ERROR error code when closing the connection.
 %the H3_NO_ERROR error code when closing the connection.
+
 %% If a client has consumed all available bidirectional stream IDs with
 %% If a client has consumed all available bidirectional stream IDs with
 %requests, the server need not send a GOAWAY frame, since the client is unable
 %requests, the server need not send a GOAWAY frame, since the client is unable
 %to make further requests. @todo OK that one's some weird stuff lol
 %to make further requests. @todo OK that one's some weird stuff lol
@@ -1476,56 +1629,10 @@ do_wait_connection_closed(Conn) ->
 %as the QUIC CONNECTION_CLOSE frame improves the chances of the frame being
 %as the QUIC CONNECTION_CLOSE frame improves the chances of the frame being
 %received by clients.
 %received by clients.
 
 
-%% 6. Stream Mapping and Usage
-%% 6.1. Bidirectional Streams
-%% an HTTP/3 server SHOULD configure non-zero minimum values for the number of
-%permitted streams and the initial stream flow-control window. So as to not
-%unnecessarily limit parallelism, at least 100 request streams SHOULD be
-%permitted at a time.
-
-%% 6.2. Unidirectional Streams
-%% Therefore, the transport parameters sent by both clients and servers MUST
-%allow the peer to create at least three unidirectional streams. These
-%transport parameters SHOULD also provide at least 1,024 bytes of flow-control
-%credit to each unidirectional stream.
-%% Note that an endpoint is not required to grant additional credits to create
-%more unidirectional streams if its peer consumes all the initial credits
-%before creating the critical unidirectional streams. Endpoints SHOULD create
-%the HTTP control stream as well as the unidirectional streams required by
-%mandatory extensions (such as the QPACK encoder and decoder streams) first,
-%and then create additional streams as allowed by their peer.
-%% Recipients of unknown stream types MUST either abort reading of the stream
-%or discard incoming data without further processing. If reading is aborted,
-%the recipient SHOULD use the H3_STREAM_CREATION_ERROR error code or a reserved
-%error code (Section 8.1). The recipient MUST NOT consider unknown stream types
-%to be a connection error of any kind.
-%% As certain stream types can affect connection state, a recipient SHOULD NOT
-%discard data from incoming unidirectional streams prior to reading the stream
-%type.
-%% Implementations MAY send stream types before knowing whether the peer
-%supports them. However, stream types that could modify the state or semantics
-%of existing protocol components, including QPACK or other extensions, MUST NOT
-%be sent until the peer is known to support them.
-%% A receiver MUST tolerate unidirectional streams being closed or reset prior
-%to the reception of the unidirectional stream header.
 
 
-%% 6.2.1. Control Streams
-%% A control stream is indicated by a stream type of 0x00. Data on this stream
-%consists of HTTP/3 frames, as defined in Section 7.2.
-%% Each side MUST initiate a single control stream at the beginning of the
-%connection and send its SETTINGS frame as the first frame on this stream. If
-%the first frame of the control stream is any other frame type, this MUST be
-%treated as a connection error of type H3_MISSING_SETTINGS. Only one control
-%stream per peer is permitted; receipt of a second stream claiming to be a
-%control stream MUST be treated as a connection error of type
-%H3_STREAM_CREATION_ERROR. The sender MUST NOT close the control stream, and
-%the receiver MUST NOT request that the sender close the control stream. If
-%either control stream is closed at any point, this MUST be treated as a
-%connection error of type H3_CLOSED_CRITICAL_STREAM. Connection errors are
-%described in Section 8.
-%% Because the contents of the control stream are used to manage the behavior
-%of other streams, endpoints SHOULD provide enough flow-control credit to keep
-%the peer's control stream from becoming blocked.
+
+
+
 
 
 %% 6.2.2. Push Streams
 %% 6.2.2. Push Streams
 %% A push stream is indicated by a stream type of 0x01, followed by the push ID
 %% A push stream is indicated by a stream type of 0x01, followed by the push ID