aboutsummaryrefslogtreecommitdiffstats
path: root/src/cow_http3_machine.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/cow_http3_machine.erl')
-rw-r--r--src/cow_http3_machine.erl721
1 files changed, 721 insertions, 0 deletions
diff --git a/src/cow_http3_machine.erl b/src/cow_http3_machine.erl
new file mode 100644
index 0000000..b1b4a68
--- /dev/null
+++ b/src/cow_http3_machine.erl
@@ -0,0 +1,721 @@
+%% 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.
+
+-module(cow_http3_machine).
+
+-export([init/2]).
+-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_bidi_stream_for_sending/2]).
+-export([close_stream/2]).
+-export([unidi_data/4]).
+-export([frame/4]).
+-export([ignored_frame/2]).
+-export([prepare_headers/5]).
+-export([prepare_trailers/3]).
+-export([reset_stream/2]).
+-export([get_bidi_stream_local_state/2]).
+-export([get_bidi_stream_remote_state/2]).
+
+-type opts() :: #{
+ enable_connect_protocol => boolean(),
+ 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
+}.
+-export_type([opts/0]).
+
+-type unidi_stream_dir() :: unidi_local | unidi_remote.
+-type unidi_stream_type() :: control | push | encoder | decoder.
+
+-record(unidi_stream, {
+ id :: cow_http3:stream_id(),
+
+ %% Unidi stream direction (local = we initiated).
+ dir :: unidi_stream_dir(),
+
+ %% Unidi stream type.
+ type :: undefined | unidi_stream_type()
+}).
+
+-record(bidi_stream, {
+ id :: cow_http3:stream_id(),
+
+ %% Request method.
+ method = undefined :: undefined | binary(),
+
+ %% Whether we finished sending data.
+ local = idle :: idle | cow_http:fin(),
+
+ %% Whether we finished receiving data.
+ remote = idle :: idle | cow_http:fin(),
+
+ %% Size expected and read from the request body.
+ remote_expected_size = undefined :: undefined | non_neg_integer(),
+ remote_read_size = 0 :: non_neg_integer(),
+
+ %% Unparsed te header. Used to know if we can send trailers.
+ %% Note that we can always send trailers to the server.
+ te :: undefined | binary()
+}).
+
+-type stream() :: #unidi_stream{} | #bidi_stream{}.
+
+-record(http3_machine, {
+ %% Whether the HTTP/3 endpoint is a client or a server.
+ mode :: client | server,
+
+ %% Current state of the supported unidi streams:
+ %% * the control stream must send SETTINGS as its first frame
+ %% * none of these streams can be closed once they are open
+ peer_control_state = no_stream :: no_stream | no_settings | ready,
+ peer_decode_state = no_stream :: no_stream | ready,
+ peer_encode_state = no_stream :: no_stream | ready,
+
+ %% Maximum Push ID.
+ max_push_id = -1 :: -1 | cow_http3:push_id(),
+
+ %% Settings are separate for each endpoint. They are sent once
+ %% at the beginning of the control stream.
+ local_settings = #{
+% enable_connect_protocol => false
+% max_decode_blocked_streams => 0,
+% max_decode_table_size => 0,
+% max_encode_blocked_streams => 0,
+% max_encode_table_size => 4096
+ } :: map(),
+
+ %% Currently active HTTP/3 streams. Streams may be initiated either
+ %% by the client or by the server through PUSH_PROMISE frames.
+ streams = #{} :: #{cow_http3:stream_id() => stream()},
+
+ %% QPACK decoding and encoding state.
+ decode_state :: cow_qpack:state(),
+ encode_state :: cow_qpack:state()
+}).
+
+-opaque http3_machine() :: #http3_machine{}.
+-export_type([http3_machine/0]).
+
+-type instructions() :: undefined
+ | {decoder_instructions | encoder_instructions, iodata()}.
+
+-spec init(client | server, opts())
+ -> {ok, iolist(), http3_machine()}.
+
+init(Mode, Opts) ->
+ Settings = init_settings(Opts),
+ {ok, cow_http3:settings(Settings), #http3_machine{
+ mode=Mode, local_settings=Settings,
+ decode_state=init_decode_state(Opts),
+ encode_state=init_encode_state(Opts)
+ }}.
+
+init_settings(Opts) ->
+ S0 = setting_from_opt(#{}, Opts, max_decode_table_size,
+ qpack_max_table_capacity, 0),
+ S1 = setting_from_opt(S0, Opts, max_decode_blocked_streams,
+ qpack_blocked_streams, 0),
+ %% @todo max_field_section_size
+ setting_from_opt(S1, Opts, enable_connect_protocol,
+ enable_connect_protocol, false).
+
+setting_from_opt(Settings, Opts, OptName, SettingName, Default) ->
+ case maps:get(OptName, Opts, Default) of
+ Default -> Settings;
+ Value -> Settings#{SettingName => Value}
+ end.
+
+%% Note that only the decoder sends them as SETTINGS.
+init_decode_state(Opts) ->
+ MaxTableCapacity = maps:get(max_decode_table_size, Opts, 0),
+ MaxBlockedStreams = maps:get(max_decode_blocked_streams, Opts, 0),
+ cow_qpack:init(decoder, MaxTableCapacity, MaxBlockedStreams).
+
+%% We want to use the dynamic table by default to improve
+%% compression ratio, but we do not allow blocked streams
+%% by default because they could lead to the latency being
+%% worse than otherwise.
+init_encode_state(Opts) ->
+ MaxTableCapacity = maps:get(max_encode_table_size, Opts, 4096),
+ MaxBlockedStreams = maps:get(max_encode_blocked_streams, Opts, 0),
+ cow_qpack:init(encoder, MaxTableCapacity, MaxBlockedStreams).
+
+-spec init_unidi_local_streams(cow_http3:stream_id(), cow_http3:stream_id(),
+ cow_http3:stream_id(), State) -> State when State::http3_machine().
+
+init_unidi_local_streams(ControlID, EncoderID, DecoderID,
+ State=#http3_machine{streams=Streams}) ->
+ State#http3_machine{
+ streams=Streams#{
+ ControlID => #unidi_stream{id=ControlID, dir=unidi_local, type=control},
+ EncoderID => #unidi_stream{id=EncoderID, dir=unidi_local, type=encoder},
+ DecoderID => #unidi_stream{id=DecoderID, dir=unidi_local, type=decoder}
+ }}.
+
+-spec init_unidi_stream(cow_http3:stream_id(), unidi_stream_dir(), State)
+ -> State when State::http3_machine().
+
+init_unidi_stream(StreamID, StreamDir, State=#http3_machine{streams=Streams}) ->
+ State#http3_machine{streams=Streams#{StreamID => #unidi_stream{
+ id=StreamID, dir=StreamDir, type=undefined}}}.
+
+-spec set_unidi_remote_stream_type(cow_http3:stream_id(), unidi_stream_type(), State)
+ -> {ok, State}
+ | {error, {connection_error, h3_stream_creation_error, atom()}, State}
+ when State::http3_machine().
+
+set_unidi_remote_stream_type(StreamID, Type=control,
+ State=#http3_machine{peer_control_state=no_stream}) ->
+ Stream = stream_get(StreamID, State),
+ {ok, stream_store(Stream#unidi_stream{type=Type},
+ State#http3_machine{peer_control_state=no_settings})};
+set_unidi_remote_stream_type(_, control, State) ->
+ {error, {connection_error, h3_stream_creation_error,
+ 'A peer cannot open two control streams. (RFC9114 6.2.1)'},
+ State};
+set_unidi_remote_stream_type(StreamID, Type=decoder,
+ State=#http3_machine{peer_decode_state=no_stream}) ->
+ Stream = stream_get(StreamID, State),
+ {ok, stream_store(Stream#unidi_stream{type=Type},
+ State#http3_machine{peer_decode_state=ready})};
+set_unidi_remote_stream_type(StreamID, Type=encoder,
+ State=#http3_machine{peer_encode_state=no_stream}) ->
+ Stream = stream_get(StreamID, State),
+ {ok, stream_store(Stream#unidi_stream{type=Type},
+ State#http3_machine{peer_encode_state=ready})};
+set_unidi_remote_stream_type(_, decoder, State) ->
+ {error, {connection_error, h3_stream_creation_error,
+ 'A peer cannot open two decoder streams. (RFC9204 4.2)'},
+ State};
+set_unidi_remote_stream_type(_, encoder, State) ->
+ {error, {connection_error, h3_stream_creation_error,
+ 'A peer cannot open two encoder streams. (RFC9204 4.2)'},
+ State}.
+
+%% All bidi streams are request/response.
+%% We only need to know the method when in client mode.
+
+-spec init_bidi_stream(cow_http3:stream_id(), State)
+ -> State when State::http3_machine().
+
+init_bidi_stream(StreamID, State=#http3_machine{streams=Streams}) ->
+ State#http3_machine{streams=Streams#{
+ StreamID => #bidi_stream{id=StreamID}
+ }}.
+
+-spec init_bidi_stream(cow_http3:stream_id(), binary(), State)
+ -> State when State::http3_machine().
+
+init_bidi_stream(StreamID, Method, State=#http3_machine{streams=Streams}) ->
+ State#http3_machine{streams=Streams#{
+ StreamID => #bidi_stream{id=StreamID, method=Method}
+ }}.
+
+-spec close_bidi_stream_for_sending(cow_http3:stream_id(), State)
+ -> State when State::http3_machine().
+
+close_bidi_stream_for_sending(StreamID, State=#http3_machine{streams=Streams}) ->
+ #{StreamID := Stream} = Streams,
+ stream_store(Stream#bidi_stream{local=fin}, State).
+
+-spec close_stream(cow_http3:stream_id(), State)
+ -> {ok, State}
+ | {error, {connection_error, h3_closed_critical_stream, atom()}, State}
+ when State::http3_machine().
+
+close_stream(StreamID, State=#http3_machine{streams=Streams0}) ->
+ case maps:take(StreamID, Streams0) of
+ {#unidi_stream{type=control}, Streams} ->
+ {error, {connection_error, h3_closed_critical_stream,
+ 'A control stream was closed. (RFC9114 6.2.1)'},
+ State#http3_machine{streams=Streams}};
+ {#unidi_stream{type=decoder}, Streams} ->
+ {error, {connection_error, h3_closed_critical_stream,
+ 'A decoder stream was closed. (RFC9204 4.2)'},
+ State#http3_machine{streams=Streams}};
+ {#unidi_stream{type=encoder}, Streams} ->
+ {error, {connection_error, h3_closed_critical_stream,
+ 'An encoder stream was closed. (RFC9204 4.2)'},
+ State#http3_machine{streams=Streams}};
+ {_, Streams} ->
+ {ok, State#http3_machine{streams=Streams}}
+ end.
+
+-spec unidi_data(binary(), cow_http:fin(), cow_http3:stream_id(), State)
+ -> {ok, instructions(), State}
+ | {error, {connection_error, cow_qpack:error(), atom()}, State}
+ when State::http3_machine().
+
+%% All currently supported unidi streams are critical.
+unidi_data(_, fin, _, State) ->
+ {error, {connection_error, h3_closed_critical_stream,
+ 'The FIN flag was set on an encoder or decoder stream. (RFC9204 4.2)'},
+ State};
+unidi_data(Data, nofin, StreamID, State=#http3_machine{
+ decode_state=DecState0, encode_state=EncState0}) ->
+ case stream_get(StreamID, State) of
+ #unidi_stream{type=decoder} ->
+ case cow_qpack:execute_decoder_instructions(Data, EncState0) of
+ {ok, EncState} ->
+ {ok, undefined, State#http3_machine{encode_state=EncState}};
+ Error = {connection_error, _, _} ->
+ {error, Error, State}
+ end;
+ #unidi_stream{type=encoder} ->
+ case cow_qpack:execute_encoder_instructions(Data, DecState0) of
+ {ok, <<>>, DecState} ->
+ {ok, undefined, State#http3_machine{decode_state=DecState}};
+ {ok, DecData, DecState} ->
+ {ok, {decoder_instructions, DecData},
+ State#http3_machine{decode_state=DecState}};
+ Error = {connection_error, _, _} ->
+ {error, Error, State}
+ end
+ end.
+
+-spec frame(cow_http3:frame(), cow_http:fin(), cow_http3:stream_id(), State)
+ -> {ok, State}
+ | {ok, {data, binary()}, State}
+ | {ok, {headers, cow_http:headers(), cow_http:pseudo_headers(),
+ non_neg_integer() | undefined}, instructions(), State}
+ | {ok, {trailers, cow_http:headers()}, instructions(), State}
+ | {ok, {goaway, cow_http3:stream_id() | cow_http3:push_id()}, State}
+ | {error, {stream_error, h3_message_error, atom()}, instructions(), State}
+ | {error, {connection_error, cow_http3:error() | cow_qpack:error(), atom()}, State}
+ when State::http3_machine().
+
+frame(Frame, IsFin, StreamID, State) ->
+ case element(1, Frame) of
+ data -> data_frame(Frame, IsFin, StreamID, State);
+ headers -> headers_frame(Frame, IsFin, StreamID, State);
+ cancel_push -> cancel_push_frame(Frame, IsFin, StreamID, State);
+ settings -> settings_frame(Frame, IsFin, StreamID, State);
+ push_promise -> push_promise_frame(Frame, IsFin, StreamID, State);
+ goaway -> goaway_frame(Frame, IsFin, StreamID, State);
+ max_push_id -> max_push_id_frame(Frame, IsFin, StreamID, State)
+ end.
+
+%% DATA frame.
+
+data_frame(Frame={data, Data}, IsFin, StreamID, State) ->
+ DataLen = byte_size(Data),
+ case stream_get(StreamID, State) of
+ Stream = #bidi_stream{remote=nofin} ->
+ data_frame(Frame, IsFin, Stream, State, DataLen);
+ #bidi_stream{remote=idle} ->
+ {error, {connection_error, h3_frame_unexpected,
+ 'DATA frame received before a HEADERS frame. (RFC9114 4.1)'},
+ State};
+ #bidi_stream{remote=fin} ->
+ {error, {connection_error, h3_frame_unexpected,
+ 'DATA frame received after trailer HEADERS frame. (RFC9114 4.1)'},
+ State};
+ #unidi_stream{type=control} ->
+ control_frame(Frame, State)
+ end.
+
+data_frame(Frame, IsFin, Stream0=#bidi_stream{remote_read_size=StreamRead}, State0, DataLen) ->
+ Stream = Stream0#bidi_stream{remote=IsFin,
+ remote_read_size=StreamRead + DataLen},
+ State = stream_store(Stream, State0),
+ case is_body_size_valid(Stream) of
+ true ->
+ {ok, Frame, State}%;
+%% @todo Implement and update error type/message.
+% false ->
+% stream_reset(StreamID, State, protocol_error,
+% 'The total size of DATA frames is different than the content-length. (RFC7540 8.1.2.6)')
+ end.
+
+%% It's always valid when no content-length header was specified.
+is_body_size_valid(#bidi_stream{remote_expected_size=undefined}) ->
+ true;
+%% We didn't finish reading the body but the size is already larger than expected.
+is_body_size_valid(#bidi_stream{remote=nofin, remote_expected_size=Expected,
+ remote_read_size=Read}) when Read > Expected ->
+ false;
+is_body_size_valid(#bidi_stream{remote=nofin}) ->
+ true;
+is_body_size_valid(#bidi_stream{remote=fin, remote_expected_size=Expected,
+ remote_read_size=Expected}) ->
+ true;
+%% We finished reading the body and the size read is not the one expected.
+is_body_size_valid(_) ->
+ false.
+
+%% HEADERS frame.
+
+headers_frame(Frame, IsFin, StreamID, State=#http3_machine{mode=Mode}) ->
+ case stream_get(StreamID, State) of
+ %% Headers.
+ Stream=#bidi_stream{remote=idle} ->
+ headers_decode(Frame, IsFin, Stream, State, case Mode of
+ server -> request;
+ client -> response
+ end);
+ %% Trailers.
+ Stream=#bidi_stream{remote=nofin} ->
+ headers_decode(Frame, IsFin, Stream, State, trailers);
+ %% Additional frame received after trailers.
+ #bidi_stream{remote=fin} ->
+ {error, {connection_error, h3_frame_unexpected,
+ 'HEADERS frame received after trailer HEADERS frame. (RFC9114 4.1)'},
+ State};
+ #unidi_stream{type=control} ->
+ control_frame(Frame, State)
+ end.
+
+headers_decode({headers, EncodedFieldSection}, IsFin, Stream=#bidi_stream{id=StreamID},
+ State=#http3_machine{decode_state=DecodeState0}, Type) ->
+ try cow_qpack:decode_field_section(EncodedFieldSection, StreamID, DecodeState0) of
+ {ok, Headers, DecData, DecodeState} ->
+ headers_process(Stream,
+ State#http3_machine{decode_state=DecodeState}, IsFin, Type, DecData, Headers);
+ Error = {connection_error, _, _} ->
+ {error, Error, State}
+ catch _:_ ->
+ {error, {connection_error, qpack_decompression_failed,
+ 'Exception while trying to decode QPACK-encoded header block. (RFC9204 2.2)'},
+ State}
+ end.
+
+headers_process(Stream=#bidi_stream{method=ReqMethod},
+ State=#http3_machine{local_settings=LocalSettings},
+ IsFin, Type, DecData, Headers0) ->
+ case cow_http:process_headers(Headers0, Type, ReqMethod, IsFin, LocalSettings) of
+ {headers, Headers, PseudoHeaders, Len} ->
+ headers_frame(Stream, State, IsFin, Type, DecData, Headers, PseudoHeaders, Len);
+% {push_promise, Headers, PseudoHeaders} -> %% @todo Implement push promises.
+ {trailers, Headers} ->
+ trailers_frame(Stream, State, DecData, Headers);
+ {error, Reason} ->
+ {error, {stream_error, h3_message_error, format_error(Reason)},
+ %% We decoded the headers so must send the instructions if any.
+ case DecData of
+ <<>> -> undefined;
+ _ -> {decoder_instructions, DecData}
+ end,
+ State}
+ end.
+
+headers_frame(Stream0, State0, IsFin, Type, DecData, Headers, PseudoHeaders, Len) ->
+ Stream = case Type of
+ request ->
+ TE = case lists:keyfind(<<"te">>, 1, Headers) of
+ {_, TE0} -> TE0;
+ false -> undefined
+ end,
+ Stream0#bidi_stream{method=maps:get(method, PseudoHeaders),
+ remote=IsFin, remote_expected_size=Len, te=TE};
+ response ->
+ case PseudoHeaders of
+ #{status := Status} when Status >= 100, Status =< 199 -> Stream0;
+ _ -> Stream0#bidi_stream{remote=IsFin, remote_expected_size=Len}
+ end
+ end,
+ State = stream_store(Stream, State0),
+ {ok, {headers, Headers, PseudoHeaders, Len},
+ case DecData of
+ <<>> -> undefined;
+ _ -> {decoder_instructions, DecData}
+ end,
+ State}.
+
+trailers_frame(Stream0, State0, DecData, Headers) ->
+ Stream = Stream0#bidi_stream{remote=fin},
+ State = stream_store(Stream, State0),
+ %% @todo Error out if we didn't get the full body.
+ case is_body_size_valid(Stream) of
+ true ->
+ {ok, {trailers, Headers},
+ case DecData of
+ <<>> -> undefined;
+ _ -> {decoder_instructions, DecData}
+ end,
+ State}%;
+%% @todo Implement and update error type/message.
+% false ->
+% stream_reset(StreamID, State, protocol_error,
+% 'The total size of DATA frames is different than the content-length. (RFC7540 8.1.2.6)')
+ end.
+
+format_error(connect_invalid_pseudo_header) ->
+ 'CONNECT requests only use the :method and :authority pseudo-headers. (RFC9114 4.4)';
+format_error(connect_missing_authority) ->
+ 'CONNECT requests must include the :authority pseudo-header. (RFC9114 4.4)';
+format_error(empty_header_name) ->
+ 'Empty header names are not valid regular headers. (CVE-2019-9516)';
+format_error(extended_connect_missing_protocol) ->
+ 'Extended CONNECT requests must include the :protocol pseudo-header. (RFC9220, RFC8441 4)';
+format_error(invalid_connection_header) ->
+ 'The connection header is not allowed. (RFC9114 4.2)';
+format_error(invalid_keep_alive_header) ->
+ 'The keep-alive header is not allowed. (RFC9114 4.2)';
+format_error(invalid_protocol_pseudo_header) ->
+ 'The :protocol pseudo-header is only defined for the extended CONNECT. (RFC9220, RFC8441 4)';
+format_error(invalid_proxy_authenticate_header) ->
+ 'The proxy-authenticate header is not allowed. (RFC9114 4.2)';
+format_error(invalid_proxy_authorization_header) ->
+ 'The proxy-authorization header is not allowed. (RFC9114 4.2)';
+format_error(invalid_pseudo_header) ->
+ 'An unknown or invalid pseudo-header was found. (RFC9114 4.3)';
+format_error(invalid_status_pseudo_header) ->
+ 'The :status pseudo-header value is invalid. (RFC9114 4.3, RFC9114 4.3.2)';
+format_error(invalid_te_header) ->
+ 'The te header is only allowed in request headers. (RFC9114 4.2)';
+format_error(invalid_te_value) ->
+ 'The te header with a value other than "trailers" is not allowed. (RFC9114 4.2)';
+format_error(invalid_transfer_encoding_header) ->
+ 'The transfer-encoding header is not allowed. (RFC9114 4.1)';
+format_error(invalid_upgrade_header) ->
+ 'The upgrade header is not allowed. (RFC9114 4.2)';
+format_error(missing_pseudo_header) ->
+ 'A required pseudo-header was not found. (RFC9114 4.3.1, RFC9114 4.3.2)';
+format_error(multiple_authority_pseudo_headers) ->
+ 'Multiple :authority pseudo-headers were found. (RFC9114 4.3.1)';
+format_error(multiple_method_pseudo_headers) ->
+ 'Multiple :method pseudo-headers were found. (RFC9114 4.3.1)';
+format_error(multiple_path_pseudo_headers) ->
+ 'Multiple :path pseudo-headers were found. (RFC9114 4.3.1)';
+format_error(multiple_protocol_pseudo_headers) ->
+ 'Multiple :protocol pseudo-headers were found. (RFC9114 4.3.1)';
+format_error(multiple_scheme_pseudo_headers) ->
+ 'Multiple :scheme pseudo-headers were found. (RFC9114 4.3.1)';
+format_error(multiple_status_pseudo_headers) ->
+ 'Multiple :status pseudo-headers were found. (RFC9114 4.3.2)';
+format_error(non_zero_length_with_fin_flag) ->
+ 'HEADERS frame with the FIN flag contains a non-zero content-length. (RFC9114 4.1.2)';
+format_error(pseudo_header_after_regular) ->
+ 'Pseudo-headers were found after regular headers. (RFC9114 4.3)';
+format_error(trailer_invalid_pseudo_header) ->
+ 'Trailer header blocks must not contain pseudo-headers. (RFC9114 4.3)';
+format_error(uppercase_header_name) ->
+ 'Header names must be lowercase. (RFC9114 4.1.2, RFC9114 4.2)';
+format_error(Reason) ->
+ cow_http:format_semantic_error(Reason).
+
+cancel_push_frame(Frame, _IsFin, StreamID, State) ->
+ case stream_get(StreamID, State) of
+ #unidi_stream{type=control} ->
+ control_frame(Frame, State)
+ end.
+
+settings_frame(Frame, _IsFin, StreamID, State) ->
+ case stream_get(StreamID, State) of
+ #unidi_stream{type=control} ->
+ control_frame(Frame, State);
+ #bidi_stream{} ->
+ {error, {connection_error, h3_frame_unexpected,
+ 'The SETTINGS frame is not allowed on a bidi stream. (RFC9114 7.2.4)'},
+ State}
+ end.
+
+push_promise_frame(Frame, _IsFin, StreamID, State) ->
+ case stream_get(StreamID, State) of
+ #unidi_stream{type=control} ->
+ control_frame(Frame, State)
+ end.
+
+goaway_frame(Frame, _IsFin, StreamID, State) ->
+ case stream_get(StreamID, State) of
+ #unidi_stream{type=control} ->
+ control_frame(Frame, State);
+ #bidi_stream{} ->
+ {error, {connection_error, h3_frame_unexpected,
+ 'The GOAWAY frame is not allowed on a bidi stream. (RFC9114 7.2.6)'},
+ State}
+ end.
+
+max_push_id_frame(Frame, _IsFin, StreamID, State) ->
+ case stream_get(StreamID, State) of
+ #unidi_stream{type=control} ->
+ control_frame(Frame, State);
+ #bidi_stream{} ->
+ {error, {connection_error, h3_frame_unexpected,
+ 'The MAX_PUSH_ID frame is not allowed on a bidi stream. (RFC9114 7.2.7)'},
+ State}
+ end.
+
+control_frame({settings, Settings}, State=#http3_machine{
+ peer_control_state=no_settings, encode_state=EncState0}) ->
+ %% @todo max_field_section_size
+ %% Send the QPACK values to the encoder.
+ MaxTableCapacity = maps:get(qpack_max_table_capacity, Settings, 0),
+ MaxBlockedStreams = maps:get(qpack_blocked_streams, Settings, 0),
+ EncState = cow_qpack:encoder_set_settings(MaxTableCapacity, MaxBlockedStreams, EncState0),
+ {ok, State#http3_machine{peer_control_state=ready, encode_state=EncState}};
+control_frame({settings, _}, State) ->
+ {error, {connection_error, h3_frame_unexpected,
+ 'The SETTINGS frame cannot be sent more than once. (RFC9114 7.2.4)'},
+ State};
+control_frame(_Frame, State=#http3_machine{peer_control_state=no_settings}) ->
+ {error, {connection_error, h3_missing_settings,
+ 'The first frame on the control stream must be a SETTINGS frame. (RFC9114 6.2.1)'},
+ State};
+control_frame(Frame = {goaway, _}, State) ->
+ {ok, Frame, State};
+%% @todo Implement server push.
+control_frame({max_push_id, PushID}, State=#http3_machine{max_push_id=MaxPushID}) ->
+ if
+ PushID >= MaxPushID ->
+ {ok, State#http3_machine{max_push_id=PushID}};
+ true ->
+ {error, {connection_error, h3_id_error,
+ 'MAX_PUSH_ID must not be lower than previously received. (RFC9114 7.2.7)'},
+ State}
+ end;
+control_frame(ignored_frame, State) ->
+ {ok, State};
+control_frame(_Frame, State) ->
+ {error, {connection_error, h3_frame_unexpected,
+ 'DATA and HEADERS frames are not allowed on the control stream. (RFC9114 7.2.1, RFC9114 7.2.2)'},
+ State}.
+
+%% Ignored frames.
+
+-spec ignored_frame(cow_http3:stream_id(), State)
+ -> {ok, State}
+ | {error, {connection_error, cow_http3:error(), atom()}, State}
+ when State::http3_machine().
+
+ignored_frame(StreamID, State) ->
+ case stream_get(StreamID, State) of
+ #unidi_stream{type=control} ->
+ control_frame(ignored_frame, 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.
+
+-spec prepare_headers(cow_http3:stream_id(), State,
+ idle | cow_http:fin(), cow_http:pseudo_headers(), cow_http:headers())
+ -> {ok, cow_http:fin(), iodata(), instructions(), State} when State::http3_machine().
+
+prepare_headers(StreamID, State=#http3_machine{encode_state=EncodeState0},
+ IsFin0, PseudoHeaders, Headers0) ->
+ Stream = #bidi_stream{method=Method, local=idle} = stream_get(StreamID, State),
+ IsFin = case {IsFin0, Method} of
+ {idle, _} -> nofin;
+ {_, <<"HEAD">>} -> fin;
+ _ -> IsFin0
+ end,
+ %% With QUIC we don't have a data queue so the local state
+ %% can be updated immediately.
+ LocalIsFin = case IsFin0 of
+ idle -> idle;
+ _ -> IsFin
+ end,
+ Headers = cow_http:merge_pseudo_headers(PseudoHeaders,
+ cow_http:remove_http1_headers(Headers0)),
+ {ok, HeaderBlock, EncData, EncodeState}
+ = cow_qpack:encode_field_section(Headers, StreamID, EncodeState0),
+ {ok, IsFin, HeaderBlock,
+ case EncData of
+ [] -> undefined;
+ _ -> {encoder_instructions, EncData}
+ end,
+ stream_store(Stream#bidi_stream{local=LocalIsFin},
+ State#http3_machine{encode_state=EncodeState})}.
+
+-spec prepare_trailers(cow_http3:stream_id(), State, cow_http:headers())
+ -> {trailers, iodata(), instructions(), State}
+ | {no_trailers, State}
+ when State::http3_machine().
+
+prepare_trailers(StreamID, State=#http3_machine{encode_state=EncodeState0}, Trailers) ->
+ Stream = #bidi_stream{local=nofin, te=TE0} = stream_get(StreamID, State),
+ TE = try cow_http_hd:parse_te(TE0) of
+ {trailers, []} -> trailers;
+ _ -> no_trailers
+ catch _:_ ->
+ %% If we can't parse the TE header, assume we can't send trailers.
+ no_trailers
+ end,
+ case TE of
+ trailers ->
+ {ok, HeaderBlock, EncData, EncodeState}
+ = cow_qpack:encode_field_section(Trailers, StreamID, EncodeState0),
+ {trailers, HeaderBlock,
+ case EncData of
+ [] -> undefined;
+ _ -> {encoder_instructions, EncData}
+ end,
+ stream_store(Stream#bidi_stream{local=fin},
+ State#http3_machine{encode_state=EncodeState})};
+ no_trailers ->
+ {no_trailers, stream_store(Stream#bidi_stream{local=fin}, State)}
+ end.
+
+%% Public interface to reset streams.
+
+-spec reset_stream(cow_http3:stream_id(), State)
+ -> {ok, State} | {error, not_found} when State::http3_machine().
+
+reset_stream(StreamID, State=#http3_machine{streams=Streams0}) ->
+ case maps:take(StreamID, Streams0) of
+ {_, Streams} ->
+ {ok, State#http3_machine{streams=Streams}};
+ error ->
+ {error, not_found}
+ end.
+
+%% Retrieve the local state for a bidi stream.
+
+-spec get_bidi_stream_local_state(cow_http3:stream_id(), http3_machine())
+ -> {ok, idle | cow_http:fin()} | {error, not_found}.
+
+get_bidi_stream_local_state(StreamID, State) ->
+ case stream_get(StreamID, State) of
+ #bidi_stream{local=IsFin} ->
+ {ok, IsFin};
+ %% Stream may never have been opened, or could have
+ %% already been closed.
+ undefined ->
+ {error, not_found}
+ end.
+
+%% Retrieve the remote state for a bidi stream.
+
+-spec get_bidi_stream_remote_state(cow_http3:stream_id(), http3_machine())
+ -> {ok, idle | cow_http:fin()} | {error, not_found}.
+
+get_bidi_stream_remote_state(StreamID, State) ->
+ case stream_get(StreamID, State) of
+ #bidi_stream{remote=IsFin} ->
+ {ok, IsFin};
+ %% Stream may never have been opened, or could have
+ %% already been closed.
+ undefined ->
+ {error, not_found}
+ end.
+
+%% Stream-related functions.
+
+stream_get(StreamID, #http3_machine{streams=Streams}) ->
+ maps:get(StreamID, Streams, undefined).
+
+stream_store(Stream, State=#http3_machine{streams=Streams}) ->
+ StreamID = case Stream of
+ #bidi_stream{id=StreamID0} -> StreamID0;
+ #unidi_stream{id=StreamID0} -> StreamID0
+ end,
+ State#http3_machine{streams=Streams#{StreamID => Stream}}.