|
@@ -15,9 +15,11 @@
|
|
-module(cow_http3_machine).
|
|
-module(cow_http3_machine).
|
|
|
|
|
|
-export([init/2]).
|
|
-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([set_unidi_remote_stream_type/3]).
|
|
|
|
+-export([init_bidi_stream/2]).
|
|
|
|
+-export([init_bidi_stream/3]).
|
|
-export([close_stream/2]).
|
|
-export([close_stream/2]).
|
|
-export([frame/4]).
|
|
-export([frame/4]).
|
|
-export([ignored_frame/2]).
|
|
-export([ignored_frame/2]).
|
|
@@ -26,7 +28,6 @@
|
|
|
|
|
|
-record(stream, {
|
|
-record(stream, {
|
|
ref :: any(), %% @todo specs
|
|
ref :: any(), %% @todo specs
|
|
- id = undefined :: non_neg_integer(), %% @todo spec from quicer?
|
|
|
|
dir :: unidi_local | unidi_remote | bidi,
|
|
dir :: unidi_local | unidi_remote | bidi,
|
|
type :: undefined | req | control | push | encoder | decoder,
|
|
type :: undefined | req | control | push | encoder | decoder,
|
|
|
|
|
|
@@ -81,26 +82,24 @@
|
|
init(Mode, _Opts) ->
|
|
init(Mode, _Opts) ->
|
|
{ok, cow_http3:settings(#{}), #http3_machine{mode=Mode}}.
|
|
{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{streams=Streams}) ->
|
|
State#http3_machine{
|
|
State#http3_machine{
|
|
local_encoder_ref=EncoderRef,
|
|
local_encoder_ref=EncoderRef,
|
|
local_decoder_ref=DecoderRef,
|
|
local_decoder_ref=DecoderRef,
|
|
streams=Streams#{
|
|
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{
|
|
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
|
|
-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)
|
|
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
|
|
-spec close_stream(_, _) -> _. %% @todo
|
|
|
|
|
|
close_stream(StreamRef, State=#http3_machine{streams=Streams0}) ->
|
|
close_stream(StreamRef, State=#http3_machine{streams=Streams0}) ->
|
|
@@ -198,16 +213,13 @@ is_body_size_valid(_) ->
|
|
%% HEADERS frame.
|
|
%% HEADERS frame.
|
|
|
|
|
|
headers_frame(Frame, IsFin, StreamRef, State=#http3_machine{mode=Mode}) ->
|
|
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
|
|
case stream_get(StreamRef, State) of
|
|
%% Headers.
|
|
%% Headers.
|
|
Stream=#stream{type=req, remote=idle} ->
|
|
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.
|
|
%% Trailers.
|
|
Stream=#stream{type=req, remote=nofin} ->
|
|
Stream=#stream{type=req, remote=nofin} ->
|
|
headers_decode(Frame, IsFin, Stream, State, trailers);
|
|
headers_decode(Frame, IsFin, Stream, State, trailers);
|
|
@@ -221,8 +233,9 @@ server_headers_frame(Frame, IsFin, StreamRef, State) ->
|
|
end.
|
|
end.
|
|
|
|
|
|
%% @todo Check whether connection_error or stream_error fits better.
|
|
%% @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) ->
|
|
State=#http3_machine{decode_state=DecodeState0}, Type) ->
|
|
|
|
+ {ok, StreamID} = quicer:get_stream_id(StreamRef),
|
|
try cow_qpack:decode_field_section(EncodedFieldSection, StreamID, DecodeState0) of
|
|
try cow_qpack:decode_field_section(EncodedFieldSection, StreamID, DecodeState0) of
|
|
{ok, Headers, DecData, DecodeState} ->
|
|
{ok, Headers, DecData, DecodeState} ->
|
|
headers_pseudo_headers(Stream,
|
|
headers_pseudo_headers(Stream,
|
|
@@ -270,6 +283,17 @@ headers_pseudo_headers(Stream, State,%=#http3_machine{local_settings=LocalSettin
|
|
{error, HumanReadable} ->
|
|
{error, HumanReadable} ->
|
|
headers_malformed(Stream, State, HumanReadable)
|
|
headers_malformed(Stream, State, HumanReadable)
|
|
end;
|
|
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) ->
|
|
headers_pseudo_headers(Stream, State, IsFin, Type=trailers, DecData, Headers) ->
|
|
case trailers_contain_pseudo_headers(Headers) of
|
|
case trailers_contain_pseudo_headers(Headers) of
|
|
false ->
|
|
false ->
|
|
@@ -306,6 +330,20 @@ request_pseudo_headers([{<<":", _/bits>>, _}|_], _) ->
|
|
request_pseudo_headers(Headers, PseudoHeaders) ->
|
|
request_pseudo_headers(Headers, PseudoHeaders) ->
|
|
{ok, PseudoHeaders, Headers}.
|
|
{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([]) ->
|
|
trailers_contain_pseudo_headers([]) ->
|
|
false;
|
|
false;
|
|
trailers_contain_pseudo_headers([{<<":", _/bits>>, _}|_]) ->
|
|
trailers_contain_pseudo_headers([{<<":", _/bits>>, _}|_]) ->
|
|
@@ -313,20 +351,20 @@ trailers_contain_pseudo_headers([{<<":", _/bits>>, _}|_]) ->
|
|
trailers_contain_pseudo_headers([_|Tail]) ->
|
|
trailers_contain_pseudo_headers([_|Tail]) ->
|
|
trailers_contain_pseudo_headers(Tail).
|
|
trailers_contain_pseudo_headers(Tail).
|
|
|
|
|
|
-headers_malformed(#stream{id=_StreamID}, State, HumanReadable) ->
|
|
|
|
|
|
+headers_malformed(#stream{}, State, HumanReadable) ->
|
|
%% @todo StreamID?
|
|
%% @todo StreamID?
|
|
{error, {stream_error, h3_message_error, HumanReadable}, State}.
|
|
{error, {stream_error, h3_message_error, HumanReadable}, State}.
|
|
|
|
|
|
%% Rejecting invalid regular headers might be a bit too strong for clients.
|
|
%% 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) ->
|
|
State, IsFin, Type, DecData, PseudoHeaders, Headers) ->
|
|
case regular_headers(Headers, Type) of
|
|
case regular_headers(Headers, Type) of
|
|
ok when Type =:= request ->
|
|
ok when Type =:= request ->
|
|
request_expected_size(Stream, State, IsFin, Type, DecData, PseudoHeaders, Headers);
|
|
request_expected_size(Stream, State, IsFin, Type, DecData, PseudoHeaders, Headers);
|
|
% ok when Type =:= push_promise ->
|
|
% ok when Type =:= push_promise ->
|
|
% push_promise_frame(Frame, State, Stream, PseudoHeaders, Headers);
|
|
% 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 ->
|
|
ok when Type =:= trailers ->
|
|
trailers_frame(Stream, State, DecData, Headers);
|
|
trailers_frame(Stream, State, DecData, Headers);
|
|
{error, HumanReadable} when Type =:= request ->
|
|
{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)')
|
|
'Multiple content-length headers were received. (RFC7230 3.3.2)')
|
|
end.
|
|
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
|
|
try cow_http_hd:parse_content_length(BinLen) of
|
|
Len ->
|
|
Len ->
|
|
headers_frame(Stream, State, IsFin, Type, DecData, PseudoHeaders, Headers, 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
|
|
false -> undefined
|
|
end,
|
|
end,
|
|
Stream0#stream{method=maps:get(method, PseudoHeaders),
|
|
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,
|
|
end,
|
|
State = stream_store(Stream, State0),
|
|
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
|
|
case DecData of
|
|
<<>> ->
|
|
<<>> ->
|
|
{ok, {headers, IsFin, Headers, PseudoHeaders, Len}, State};
|
|
{ok, {headers, IsFin, Headers, PseudoHeaders, Len}, State};
|
|
@@ -530,8 +606,6 @@ ignored_frame(StreamRef, State) ->
|
|
{ok, State}
|
|
{ok, State}
|
|
end.
|
|
end.
|
|
|
|
|
|
-
|
|
|
|
-
|
|
|
|
%% 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
|
|
%% a value that can then be used to send the frames.
|
|
%% 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},
|
|
prepare_headers(StreamRef, State=#http3_machine{encode_state=EncodeState0},
|
|
IsFin0, PseudoHeaders, Headers0) ->
|
|
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
|
|
IsFin = case {IsFin0, Method} of
|
|
{idle, _} -> nofin;
|
|
{idle, _} -> nofin;
|
|
{_, <<"HEAD">>} -> fin;
|
|
{_, <<"HEAD">>} -> fin;
|
|
@@ -551,6 +626,7 @@ prepare_headers(StreamRef, State=#http3_machine{encode_state=EncodeState0},
|
|
end,
|
|
end,
|
|
Headers = merge_pseudo_headers(PseudoHeaders, remove_http11_headers(Headers0)),
|
|
Headers = merge_pseudo_headers(PseudoHeaders, remove_http11_headers(Headers0)),
|
|
{ok, HeaderBlock, EncData, EncodeState} = cow_qpack:encode_field_section(Headers, StreamID, EncodeState0),
|
|
{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},
|
|
{ok, IsFin, HeaderBlock, EncData, stream_store(Stream#stream{local=IsFin0},
|
|
State#http3_machine{encode_state=EncodeState})}.
|
|
State#http3_machine{encode_state=EncodeState})}.
|
|
|
|
|