%% Copyright (c) 2023-2024, Loïc Hoguin %% %% 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}}.