Просмотр исходного кода

WIP: Weekly HTTP/3 progress

Now has some req_SUITE tests passing (with the http3 gun branch).
Loïc Hoguin 1 год назад
Родитель
Сommit
f1b8b9acea
3 измененных файлов с 139 добавлено и 55 удалено
  1. 114 38
      src/cow_http3_machine.erl
  2. 15 15
      src/cowboy_http3.erl
  3. 10 2
      test/cowboy_test.erl

+ 114 - 38
src/cow_http3_machine.erl

@@ -15,9 +15,11 @@
 -module(cow_http3_machine).
 
 -export([init/2]).
--export([init_unidi_local_streams/7]).
--export([init_stream/5]).
+-export([init_unidi_local_streams/4]).
+-export([init_unidi_stream/3]).
 -export([set_unidi_remote_stream_type/3]).
+-export([init_bidi_stream/2]).
+-export([init_bidi_stream/3]).
 -export([close_stream/2]).
 -export([frame/4]).
 -export([ignored_frame/2]).
@@ -26,7 +28,6 @@
 
 -record(stream, {
 	ref :: any(), %% @todo specs
-	id = undefined :: non_neg_integer(), %% @todo spec from quicer?
 	dir :: unidi_local | unidi_remote | bidi,
 	type :: undefined | req | control | push | encoder | decoder,
 
@@ -81,26 +82,24 @@
 init(Mode, _Opts) ->
 	{ok, cow_http3:settings(#{}), #http3_machine{mode=Mode}}.
 
--spec init_unidi_local_streams(_, _, _, _, _ ,_ ,_) -> _. %% @todo
+-spec init_unidi_local_streams(_, _ ,_ ,_) -> _. %% @todo
 
-init_unidi_local_streams(ControlRef, ControlID,
-		EncoderRef, EncoderID, DecoderRef, DecoderID,
+init_unidi_local_streams(ControlRef, EncoderRef, DecoderRef,
 		State=#http3_machine{streams=Streams}) ->
 	State#http3_machine{
 		local_encoder_ref=EncoderRef,
 		local_decoder_ref=DecoderRef,
 		streams=Streams#{
-			ControlRef => #stream{ref=ControlRef, id=ControlID, dir=unidi_local, type=control},
-			EncoderRef => #stream{ref=EncoderRef, id=EncoderID, dir=unidi_local, type=encoder},
-			DecoderRef => #stream{ref=DecoderRef, id=DecoderID, dir=unidi_local, type=decoder}
+			ControlRef => #stream{ref=ControlRef, dir=unidi_local, type=control},
+			EncoderRef => #stream{ref=EncoderRef, dir=unidi_local, type=encoder},
+			DecoderRef => #stream{ref=DecoderRef, dir=unidi_local, type=decoder}
 	}}.
 
--spec init_stream(_, _, _, _, _) -> _. %% @todo
+-spec init_unidi_stream(_, _, _) -> _. %% @todo
 
-init_stream(StreamRef, StreamID, StreamDir, StreamType,
-		State=#http3_machine{streams=Streams}) ->
+init_unidi_stream(StreamRef, StreamDir, State=#http3_machine{streams=Streams}) ->
 	State#http3_machine{streams=Streams#{StreamRef => #stream{
-		ref=StreamRef, id=StreamID, dir=StreamDir, type=StreamType}}}.
+		ref=StreamRef, dir=StreamDir, type=undefined}}}.
 
 -spec set_unidi_remote_stream_type(_, _, _) -> _. %% @todo
 
@@ -116,6 +115,22 @@ set_unidi_remote_stream_type(StreamRef, Type,
 		has_peer_control_stream=HasControl orelse (Type =:= control)
 	}}.
 
+-spec init_bidi_stream(_, _) -> _. %% @todo
+
+%% All bidi streams are request/response.
+init_bidi_stream(StreamRef, State=#http3_machine{streams=Streams}) ->
+	State#http3_machine{streams=Streams#{StreamRef => #stream{
+		ref=StreamRef, dir=bidi, type=req}}}.
+
+-spec init_bidi_stream(_, _, _) -> _. %% @todo
+
+%% All bidi streams are request/response.
+init_bidi_stream(StreamRef, Method, State=#http3_machine{streams=Streams}) ->
+	State#http3_machine{streams=Streams#{StreamRef => #stream{
+		ref=StreamRef, dir=bidi, type=req, method=Method}}}.
+
+%% @todo set_bidi_method?
+
 -spec close_stream(_, _) -> _. %% @todo
 
 close_stream(StreamRef, State=#http3_machine{streams=Streams0}) ->
@@ -198,16 +213,13 @@ is_body_size_valid(_) ->
 %% HEADERS frame.
 
 headers_frame(Frame, IsFin, StreamRef, State=#http3_machine{mode=Mode}) ->
-	case Mode of
-		server -> server_headers_frame(Frame, IsFin, StreamRef, State)
-	end.
-
-%% @todo We may receive HEADERS before or after DATA.
-server_headers_frame(Frame, IsFin, StreamRef, State) ->
 	case stream_get(StreamRef, State) of
 		%% Headers.
 		Stream=#stream{type=req, remote=idle} ->
-			headers_decode(Frame, IsFin, Stream, State, request);
+			headers_decode(Frame, IsFin, Stream, State, case Mode of
+				server -> request;
+				client -> response
+			end);
 		%% Trailers.
 		Stream=#stream{type=req, remote=nofin} ->
 			headers_decode(Frame, IsFin, Stream, State, trailers);
@@ -221,8 +233,9 @@ server_headers_frame(Frame, IsFin, StreamRef, State) ->
 	end.
 
 %% @todo Check whether connection_error or stream_error fits better.
-headers_decode({headers, EncodedFieldSection}, IsFin, Stream=#stream{id=StreamID},
+headers_decode({headers, EncodedFieldSection}, IsFin, Stream=#stream{ref=StreamRef},
 		State=#http3_machine{decode_state=DecodeState0}, Type) ->
+	{ok, StreamID} = quicer:get_stream_id(StreamRef),
 	try cow_qpack:decode_field_section(EncodedFieldSection, StreamID, DecodeState0) of
 		{ok, Headers, DecData, DecodeState} ->
 			headers_pseudo_headers(Stream,
@@ -270,6 +283,17 @@ headers_pseudo_headers(Stream, State,%=#http3_machine{local_settings=LocalSettin
 		{error, HumanReadable} ->
 			headers_malformed(Stream, State, HumanReadable)
 	end;
+headers_pseudo_headers(Stream, State, IsFin, Type=response, DecData, Headers0) ->
+	case response_pseudo_headers(Headers0, #{}) of
+		{ok, PseudoHeaders=#{status := _}, Headers} ->
+			headers_regular_headers(Stream, State, IsFin, Type, DecData, PseudoHeaders, Headers);
+		{ok, _, _} ->
+			{error, {stream_error, protocol_error,
+				'A required pseudo-header was not found. (RFC7540 8.1.2.4)'},
+				State};
+		{error, HumanReadable} ->
+			{error, {stream_error, protocol_error, HumanReadable}, State}
+	end;
 headers_pseudo_headers(Stream, State, IsFin, Type=trailers, DecData, Headers) ->
 	case trailers_contain_pseudo_headers(Headers) of
 		false ->
@@ -306,6 +330,20 @@ request_pseudo_headers([{<<":", _/bits>>, _}|_], _) ->
 request_pseudo_headers(Headers, PseudoHeaders) ->
 	{ok, PseudoHeaders, Headers}.
 
+response_pseudo_headers([{<<":status">>, _}|_], #{status := _}) ->
+	{error, 'Multiple :status pseudo-headers were found. (RFC7540 8.1.2.3)'};
+response_pseudo_headers([{<<":status">>, Status}|Tail], PseudoHeaders) ->
+	try cow_http:status_to_integer(Status) of
+		IntStatus ->
+			response_pseudo_headers(Tail, PseudoHeaders#{status => IntStatus})
+	catch _:_ ->
+		{error, 'The :status pseudo-header value is invalid. (RFC7540 8.1.2.4)'}
+	end;
+response_pseudo_headers([{<<":", _/bits>>, _}|_], _) ->
+	{error, 'An unknown or invalid pseudo-header was found. (RFC7540 8.1.2.1)'};
+response_pseudo_headers(Headers, PseudoHeaders) ->
+	{ok, PseudoHeaders, Headers}.
+
 trailers_contain_pseudo_headers([]) ->
 	false;
 trailers_contain_pseudo_headers([{<<":", _/bits>>, _}|_]) ->
@@ -313,20 +351,20 @@ trailers_contain_pseudo_headers([{<<":", _/bits>>, _}|_]) ->
 trailers_contain_pseudo_headers([_|Tail]) ->
 	trailers_contain_pseudo_headers(Tail).
 
-headers_malformed(#stream{id=_StreamID}, State, HumanReadable) ->
+headers_malformed(#stream{}, State, HumanReadable) ->
 	%% @todo StreamID?
 	{error, {stream_error, h3_message_error, HumanReadable}, State}.
 
 %% Rejecting invalid regular headers might be a bit too strong for clients.
-headers_regular_headers(Stream=#stream{id=_StreamID},
+headers_regular_headers(Stream=#stream{},
 		State, IsFin, Type, DecData, PseudoHeaders, Headers) ->
 	case regular_headers(Headers, Type) of
 		ok when Type =:= request ->
 			request_expected_size(Stream, State, IsFin, Type, DecData, PseudoHeaders, Headers);
 %		ok when Type =:= push_promise ->
 %			push_promise_frame(Frame, State, Stream, PseudoHeaders, Headers);
-%		ok when Type =:= response ->
-%			response_expected_size(Frame, State, Type, Stream, PseudoHeaders, Headers);
+		ok when Type =:= response ->
+			response_expected_size(Stream, State, IsFin, Type, DecData, PseudoHeaders, Headers);
 		ok when Type =:= trailers ->
 			trailers_frame(Stream, State, DecData, Headers);
 		{error, HumanReadable} when Type =:= request ->
@@ -390,8 +428,48 @@ request_expected_size(Stream, State, IsFin, Type, DecData, PseudoHeaders, Header
 				'Multiple content-length headers were received. (RFC7230 3.3.2)')
 	end.
 
-headers_parse_expected_size(Stream=#stream{id=_StreamID},
-		State, IsFin, Type, DecData, PseudoHeaders, Headers, BinLen) ->
+response_expected_size(Stream=#stream{method=Method}, State, IsFin, Type, DecData,
+		PseudoHeaders=#{status := Status}, Headers) ->
+	case [CL || {<<"content-length">>, CL} <- Headers] of
+		[] when IsFin =:= fin ->
+			headers_frame(Stream, State, IsFin, Type, DecData, PseudoHeaders, Headers, 0);
+		[] ->
+			headers_frame(Stream, State, IsFin, Type, DecData, PseudoHeaders, Headers, undefined);
+		[_] when Status >= 100, Status =< 199 ->
+			{error, {stream_error, protocol_error,
+				'Content-length header received in a 1xx response. (RFC7230 3.3.2)'},
+				State};
+		[_] when Status =:= 204 ->
+			{error, {stream_error, protocol_error,
+				'Content-length header received in a 204 response. (RFC7230 3.3.2)'},
+				State};
+		[_] when Status >= 200, Status =< 299, Method =:= <<"CONNECT">> ->
+			{error, {stream_error, protocol_error,
+				'Content-length header received in a 2xx response to a CONNECT request. (RFC7230 3.3.2).'},
+				State};
+		%% Responses to HEAD requests, and 304 responses may contain
+		%% a content-length header that must be ignored. (RFC7230 3.3.2)
+		[_] when Method =:= <<"HEAD">> ->
+			headers_frame(Stream, State, IsFin, Type, DecData, PseudoHeaders, Headers, 0);
+		[_] when Status =:= 304 ->
+			headers_frame(Stream, State, IsFin, Type, DecData, PseudoHeaders, Headers, 0);
+		[<<"0">>] when IsFin =:= fin ->
+			headers_frame(Stream, State, IsFin, Type, DecData, PseudoHeaders, Headers, 0);
+		[_] when IsFin =:= fin ->
+			{error, {stream_error, protocol_error,
+				'HEADERS frame with the END_STREAM flag contains a non-zero content-length. (RFC7540 8.1.2.6)'},
+				State};
+		[BinLen] ->
+			headers_parse_expected_size(Stream, State, IsFin, Type, DecData,
+				PseudoHeaders, Headers, BinLen);
+		_ ->
+			{error, {stream_error, protocol_error,
+				'Multiple content-length headers were received. (RFC7230 3.3.2)'},
+				State}
+	end.
+
+headers_parse_expected_size(Stream, State, IsFin, Type, DecData,
+		PseudoHeaders, Headers, BinLen) ->
 	try cow_http_hd:parse_content_length(BinLen) of
 		Len ->
 			headers_frame(Stream, State, IsFin, Type, DecData, PseudoHeaders, Headers, Len)
@@ -413,16 +491,14 @@ headers_frame(Stream0, State0=#http3_machine{local_decoder_ref=DecoderRef},
 				false -> undefined
 			end,
 			Stream0#stream{method=maps:get(method, PseudoHeaders),
-				remote=IsFin, remote_expected_size=Len, te=TE}%;
-%		response ->
-%			Stream1 = case PseudoHeaders of
-%				#{status := Status} when Status >= 100, Status =< 199 -> Stream0;
-%				_ -> Stream0#stream{remote=IsFin, remote_expected_size=Len}
-%			end,
-%			{Stream1, State0}
+				remote=IsFin, remote_expected_size=Len, te=TE};
+		response ->
+			case PseudoHeaders of
+				#{status := Status} when Status >= 100, Status =< 199 -> Stream0;
+				_ -> Stream0#stream{remote=IsFin, remote_expected_size=Len}
+			end
 	end,
 	State = stream_store(Stream, State0),
-	%% @todo Maybe don't return DecData if empty, but return the StreamRef with it if we must send.
 	case DecData of
 		<<>> ->
 			{ok, {headers, IsFin, Headers, PseudoHeaders, Len}, State};
@@ -530,8 +606,6 @@ ignored_frame(StreamRef, State) ->
 			{ok, State}
 	end.
 
-
-
 %% Functions for sending a message header or body. Note that
 %% this module does not send data directly, instead it returns
 %% a value that can then be used to send the frames.
@@ -543,7 +617,8 @@ ignored_frame(StreamRef, State) ->
 
 prepare_headers(StreamRef, State=#http3_machine{encode_state=EncodeState0},
 		IsFin0, PseudoHeaders, Headers0) ->
-	Stream = #stream{id=StreamID, method=Method, local=idle} = stream_get(StreamRef, State),
+	{ok, StreamID} = quicer:get_stream_id(StreamRef),
+	Stream = #stream{method=Method, local=idle} = stream_get(StreamRef, State),
 	IsFin = case {IsFin0, Method} of
 		{idle, _} -> nofin;
 		{_, <<"HEAD">>} -> fin;
@@ -551,6 +626,7 @@ prepare_headers(StreamRef, State=#http3_machine{encode_state=EncodeState0},
 	end,
 	Headers = merge_pseudo_headers(PseudoHeaders, remove_http11_headers(Headers0)),
 	{ok, HeaderBlock, EncData, EncodeState} = cow_qpack:encode_field_section(Headers, StreamID, EncodeState0),
+	%% @todo Return the EncoderRef with the EncData.
 	{ok, IsFin, HeaderBlock, EncData, stream_store(Stream#stream{local=IsFin0},
 		State#http3_machine{encode_state=EncodeState})}.
 

+ 15 - 15
src/cowboy_http3.erl

@@ -73,19 +73,15 @@ init(Parent, Conn, Opts) ->
 	{ok, ControlRef} = quicer:start_stream(Conn,
 		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
 	{ok, _} = quicer:send(ControlRef, [<<0>>, SettingsBin]),
-	{ok, ControlID} = quicer:get_stream_id(ControlRef),
 	{ok, EncoderRef} = quicer:start_stream(Conn,
 		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
 	{ok, _} = quicer:send(EncoderRef, <<2>>),
-	{ok, EncoderID} = quicer:get_stream_id(EncoderRef),
 	{ok, DecoderRef} = quicer:start_stream(Conn,
 		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
 	{ok, _} = quicer:send(DecoderRef, <<3>>),
-	{ok, DecoderID} = quicer:get_stream_id(DecoderRef),
 	%% Set the control, encoder and decoder streams in the machine.
 	HTTP3Machine = cow_http3_machine:init_unidi_local_streams(
-		ControlRef, ControlID, EncoderRef, EncoderID, DecoderRef, DecoderID,
-		HTTP3Machine0),
+		ControlRef, EncoderRef, DecoderRef, HTTP3Machine0),
 	%% Get the peername/sockname.
 	Peer0 = quicer:peername(Conn),
 	Sock0 = quicer:sockname(Conn),
@@ -548,6 +544,7 @@ send_response(State0=#state{http3_machine=HTTP3Machine0}, StreamRef, StatusCode,
 		_ ->
 			%% @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...}).
+			%% @todo We must send EncData!
 			{ok, _IsFin, HeaderBlock, _EncData, HTTP3Machine}
 				= cow_http3_machine:prepare_headers(StreamRef, HTTP3Machine0, nofin,
 					#{status => cow_http:status_to_integer(StatusCode)},
@@ -563,11 +560,12 @@ send_response(State0=#state{http3_machine=HTTP3Machine0}, StreamRef, StatusCode,
 
 send_headers(State=#state{http3_machine=HTTP3Machine0},
 		StreamRef, IsFin0, StatusCode, Headers) ->
-	{ok, IsFin, HeaderBlock, HTTP3Machine}
+	{ok, IsFin, HeaderBlock, _EncData, HTTP3Machine}
 		= cow_http3_machine:prepare_headers(StreamRef, HTTP3Machine0, IsFin0,
 			#{status => cow_http:status_to_integer(StatusCode)},
 			headers_to_list(Headers)),
 	{ok, _} = quicer:send(StreamRef, cow_http3:headers(HeaderBlock), send_flag(IsFin)),
+	%% @todo Send _EncData.
 	State#state{http3_machine=HTTP3Machine}.
 
 %% The set-cookie header is special; we can only send one cookie per header.
@@ -627,10 +625,11 @@ terminate_stream_handler(#state{opts=Opts}, StreamRef, Reason, StreamState) ->
 
 
 stop_stream(_, _) ->
-	todo.
+	error(todo).
 
-maybe_terminate_stream(_, _, _) ->
-	todo.
+%% @todo Maybe do this on stream close instead.
+maybe_terminate_stream(State, _, _) ->
+	State.
 
 ignored_frame(State=#state{http3_machine=HTTP3Machine0}, #stream{ref=StreamRef}) ->
 	case cow_http3_machine:ignored_frame(StreamRef, HTTP3Machine0) of
@@ -687,13 +686,14 @@ terminate_all_streams(State, [{StreamID, #stream{state=StreamState}}|Tail], Reas
 
 
 stream_new_remote(State=#state{http3_machine=HTTP3Machine0, streams=Streams}, StreamRef, Flags) ->
-	{ok, StreamID} = quicer:get_stream_id(StreamRef),
-	{StreamDir, StreamType, Status} = case quicer:is_unidirectional(Flags) of
-		true -> {unidi_remote, undefined, header};
-		false -> {bidi, req, normal}
+	{HTTP3Machine, Status} = case quicer:is_unidirectional(Flags) of
+		true ->
+			{cow_http3_machine:init_unidi_stream(StreamRef, unidi_remote, HTTP3Machine0),
+				header};
+		false ->
+			{cow_http3_machine:init_bidi_stream(StreamRef, HTTP3Machine0),
+				normal}
 	end,
-	HTTP3Machine = cow_http3_machine:init_stream(StreamRef,
-		StreamID, StreamDir, StreamType, HTTP3Machine0),
 	Stream = #stream{ref=StreamRef, status=Status},
 %	ct:pal("new stream ~p ~p", [Stream, HTTP3Machine]),
 	State#state{http3_machine=HTTP3Machine, streams=Streams#{StreamRef => Stream}}.

+ 10 - 2
test/cowboy_test.erl

@@ -44,10 +44,12 @@ init_http3(Ref, ProtoOpts, Config) ->
 	%%       so we use quicer test certificates for now.
 	%% @todo Quicer also does not support cacerts which means
 	%%       we currently have no authentication based security.
+	DataDir = filename:dirname(filename:dirname(config(data_dir, Config)))
+		++ "/rfc9114_SUITE_data",
 	TransOpts = #{
 		socket_opts => [
-			{cert, config(data_dir, Config) ++ "../rfc9114_SUITE_data/server.pem"},
-			{key, config(data_dir, Config) ++ "../rfc9114_SUITE_data/server.key"}
+			{cert, DataDir ++ "/server.pem"},
+			{key, DataDir ++ "/server.key"}
 		]
 	},
 	{ok, _} = cowboy:start_quic(TransOpts, ProtoOpts), %% @todo Ref argument.
@@ -61,6 +63,7 @@ common_all() ->
 		{group, https},
 		{group, h2},
 		{group, h2c},
+		{group, h3},
 		{group, http_compress},
 		{group, https_compress},
 		{group, h2_compress},
@@ -77,6 +80,7 @@ common_groups(Tests) ->
 		{https, Opts, Tests},
 		{h2, Opts, Tests},
 		{h2c, Opts, Tests},
+		{h3, [], Tests}, %% @todo Enable parallel when issues get fixed.
 		{http_compress, Opts, Tests},
 		{https_compress, Opts, Tests},
 		{h2_compress, Opts, Tests},
@@ -100,6 +104,10 @@ init_common_groups(Name = h2c, Config, Mod) ->
 		env => #{dispatch => Mod:init_dispatch(Config)}
 	}, [{flavor, vanilla}|Config]),
 	lists:keyreplace(protocol, 1, Config1, {protocol, http2});
+init_common_groups(Name = h3, Config, Mod) ->
+	init_http3(Name, #{
+		env => #{dispatch => Mod:init_dispatch(Config)}
+	}, [{flavor, vanilla}|Config]);
 init_common_groups(Name = http_compress, Config, Mod) ->
 	init_http(Name, #{
 		env => #{dispatch => Mod:init_dispatch(Config)},