aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ebin/cowlib.app2
-rw-r--r--src/cow_capsule.erl98
-rw-r--r--src/cow_http3.erl161
-rw-r--r--src/cow_http3_machine.erl84
-rw-r--r--src/cow_http_hd.erl38
5 files changed, 364 insertions, 19 deletions
diff --git a/ebin/cowlib.app b/ebin/cowlib.app
index 4c6d3fd..9745cf0 100644
--- a/ebin/cowlib.app
+++ b/ebin/cowlib.app
@@ -1,7 +1,7 @@
{application, 'cowlib', [
{description, "Support library for manipulating Web protocols."},
{vsn, "2.15.0"},
- {modules, ['cow_base64url','cow_cookie','cow_date','cow_deflate','cow_hpack','cow_http','cow_http1','cow_http2','cow_http2_machine','cow_http3','cow_http3_machine','cow_http_hd','cow_http_struct_hd','cow_http_te','cow_iolists','cow_link','cow_mimetypes','cow_multipart','cow_qpack','cow_qs','cow_spdy','cow_sse','cow_uri','cow_uri_template','cow_ws']},
+ {modules, ['cow_base64url','cow_capsule','cow_cookie','cow_date','cow_deflate','cow_hpack','cow_http','cow_http1','cow_http2','cow_http2_machine','cow_http3','cow_http3_machine','cow_http_hd','cow_http_struct_hd','cow_http_te','cow_iolists','cow_link','cow_mimetypes','cow_multipart','cow_qpack','cow_qs','cow_spdy','cow_sse','cow_uri','cow_uri_template','cow_ws']},
{registered, []},
{applications, [kernel,stdlib,crypto]},
{optional_applications, []},
diff --git a/src/cow_capsule.erl b/src/cow_capsule.erl
new file mode 100644
index 0000000..542262f
--- /dev/null
+++ b/src/cow_capsule.erl
@@ -0,0 +1,98 @@
+%% Copyright (c) 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_capsule).
+
+%% Parsing.
+-export([parse/1]).
+
+%% Building.
+-export([wt_drain_session/0]).
+-export([wt_close_session/2]).
+
+-type capsule() ::
+ wt_drain_session |
+ {wt_close_session, cow_http3:wt_app_error_code(), binary()}.
+
+%% Parsing.
+
+-spec parse(binary())
+ -> {ok, capsule(), binary()}
+ | {ok, binary()} %% Unknown capsule gets skipped.
+ | more
+ | {skip, non_neg_integer()} %% Unknown capsule; remaining length to skip.
+ | error.
+
+%% @todo Handle DATAGRAM capsules. {datagram, binary()}
+parse(<<2:2, 16#78ae:30, 0, Rest/bits>>) ->
+ {ok, wt_drain_session, Rest};
+parse(<<1:2, 16#2843:14, Rest0/bits>>) when byte_size(Rest0) >= 5 ->
+ LenOrError = case Rest0 of
+ <<0:2, Len0:6, Rest1/bits>> ->
+ {Len0, Rest1};
+ <<1:2, Len0:14, Rest1/bits>> when Len0 =< 1028 ->
+ {Len0, Rest1};
+ %% AppCode is 4 bytes and AppMsg is up to 1024 bytes.
+ _ ->
+ error
+ end,
+ case LenOrError of
+ {Len1, Rest2} ->
+ AppMsgLen = Len1 - 4,
+ case Rest2 of
+ <<AppCode:32, AppMsg:AppMsgLen/binary, Rest/bits>> ->
+ {ok, {wt_close_session, AppCode, AppMsg}, Rest};
+ _ ->
+ more
+ end;
+ error ->
+ error
+ end;
+parse(<<>>) ->
+ more;
+%% Skip unknown capsules.
+parse(Data) ->
+ %% @todo This can use maybe_expr in OTP-25+.
+ case cow_http3:parse_int(Data) of
+ more ->
+ more;
+ {_Type, Rest0} ->
+ case cow_http3:parse_int(Rest0) of
+ more ->
+ more;
+ {Len, Rest1} ->
+ case Rest1 of
+ <<_:Len/unit:8, Rest>> ->
+ {ok, Rest};
+ _ ->
+ {skip, Len - byte_size(Rest1)}
+ end
+ end
+ end.
+
+%% Building.
+
+-spec wt_drain_session() -> binary().
+
+%% @todo Where should I put capsules?
+wt_drain_session() ->
+ <<2:2, 16#78ae:30, 0>>.
+
+-spec wt_close_session(cow_http3:wt_app_error_code(), iodata()) -> iodata().
+
+wt_close_session(AppCode, <<>>) ->
+ <<1:2, 16#2843:14, 4, AppCode:32>>;
+wt_close_session(AppCode, AppMsg) ->
+ Len = 4 + iolist_size(AppMsg),
+ [<<1:2, 16#2843:14>>, cow_http3:encode_int(Len), <<AppCode:32>>, AppMsg].
diff --git a/src/cow_http3.erl b/src/cow_http3.erl
index 4e9c984..f1cb0f7 100644
--- a/src/cow_http3.erl
+++ b/src/cow_http3.erl
@@ -17,12 +17,16 @@
%% Parsing.
-export([parse/1]).
-export([parse_unidi_stream_header/1]).
+-export([parse_datagram/1]).
-export([code_to_error/1]).
+-export([parse_int/1]).
%% Building.
-export([data/1]).
-export([headers/1]).
-export([settings/1]).
+-export([webtransport_stream_header/2]).
+-export([datagram/2]).
-export([error_to_code/1]).
-export([encode_int/1]).
@@ -32,14 +36,25 @@
-type push_id() :: non_neg_integer().
-export_type([push_id/0]).
+-type h3_non_neg_integer() :: 0..16#3fffffffffffffff.
+
-type settings() :: #{
- qpack_max_table_capacity => 0..16#3fffffffffffffff,
- max_field_section_size => 0..16#3fffffffffffffff,
- qpack_blocked_streams => 0..16#3fffffffffffffff,
- enable_connect_protocol => boolean()
+ qpack_max_table_capacity => h3_non_neg_integer(),
+ max_field_section_size => h3_non_neg_integer(),
+ qpack_blocked_streams => h3_non_neg_integer(),
+ enable_connect_protocol => boolean(),
+ %% Extensions.
+ h3_datagram => boolean(),
+ webtransport_max_sessions => h3_non_neg_integer(),
+ webtransport_initial_max_streams_uni => h3_non_neg_integer(),
+ webtransport_initial_max_streams_bidi => h3_non_neg_integer(),
+ webtransport_initial_max_data => h3_non_neg_integer()
}.
-export_type([settings/0]).
+-type wt_app_error_code() :: 0..16#ffffffff.
+-export_type([wt_app_error_code/0]).
+
-type error() :: h3_no_error
| h3_general_protocol_error
| h3_internal_error
@@ -56,7 +71,12 @@
| h3_request_incomplete
| h3_message_error
| h3_connect_error
- | h3_version_fallback.
+ | h3_version_fallback
+ %% Extensions.
+ | h3_datagram_error
+ | webtransport_buffered_stream_rejected
+ | webtransport_session_gone
+ | {webtransport_application_error, wt_app_error_code()}.
-export_type([error/0]).
-type frame() :: {data, binary()}
@@ -72,6 +92,7 @@
-spec parse(binary())
-> {ok, frame(), binary()}
+ | {webtransport_stream_header, stream_id(), binary()}
| {more, {data, binary()} | ignore, non_neg_integer()}
| {ignore, binary()}
| {connection_error, h3_frame_error | h3_frame_unexpected | h3_settings_error, atom()}
@@ -191,6 +212,19 @@ parse(<<13, _/bits>>) ->
{connection_error, h3_frame_error,
'MAX_PUSH_ID frames payload MUST be 1, 2, 4 or 8 bytes wide. (RFC9114 7.1, RFC9114 7.2.6)'};
%%
+%% WebTransport stream header.
+%%
+parse(<<1:2, 16#41:14, 0:2, SessionID:6, Rest/bits>>) ->
+ {webtransport_stream_header, SessionID, Rest};
+parse(<<1:2, 16#41:14, 1:2, SessionID:14, Rest/bits>>) ->
+ {webtransport_stream_header, SessionID, Rest};
+parse(<<1:2, 16#41:14, 2:2, SessionID:30, Rest/bits>>) ->
+ {webtransport_stream_header, SessionID, Rest};
+parse(<<1:2, 16#41:14, 3:2, SessionID:62, Rest/bits>>) ->
+ {webtransport_stream_header, SessionID, Rest};
+parse(<<16#41, _/bits>>) ->
+ more;
+%%
%% HTTP/2 frame types must be rejected.
%%
parse(<<2, _/bits>>) ->
@@ -294,6 +328,26 @@ parse_settings_id_val(Rest, Len, Settings, Identifier, Value) ->
8 ->
{connection_error, h3_settings_error,
'The SETTINGS_ENABLE_CONNECT_PROTOCOL value MUST be 0 or 1. (RFC9220 3, RFC8441 3)'};
+ %% SETTINGS_H3_DATAGRAM (RFC9297).
+ 16#33 when Value =:= 0 ->
+ parse_settings_key_val(Rest, Len, Settings, h3_datagram, false);
+ 16#33 when Value =:= 1 ->
+ parse_settings_key_val(Rest, Len, Settings, h3_datagram, true);
+ 16#33 ->
+ {connection_error, h3_settings_error,
+ 'The SETTINGS_H3_DATAGRAM value MUST be 0 or 1. (RFC9297 2.1.1)'};
+ %% SETTINGS_WEBTRANSPORT_MAX_SESSIONS (draft-ietf-webtrans-http3).
+ 16#c671706a ->
+ parse_settings_key_val(Rest, Len, Settings, webtransport_max_sessions, Value);
+ %% SETTINGS_WEBTRANSPORT_INITIAL_MAX_STREAMS_UNI (draft-ietf-webtrans-http3).
+ 16#2b64 ->
+ parse_settings_key_val(Rest, Len, Settings, webtransport_initial_max_streams_uni, Value);
+ %% SETTINGS_WEBTRANSPORT_INITIAL_MAX_STREAMS_BIDI (draft-ietf-webtrans-http3).
+ 16#2b65 ->
+ parse_settings_key_val(Rest, Len, Settings, webtransport_initial_max_streams_bidi, Value);
+ %% SETTINGS_WEBTRANSPORT_INITIAL_MAX_DATA (draft-ietf-webtrans-http3).
+ 16#2b61 ->
+ parse_settings_key_val(Rest, Len, Settings, webtransport_initial_max_data, Value);
_ when Identifier < 6 ->
{connection_error, h3_settings_error,
'HTTP/2 setting not defined for HTTP/3 must be rejected. (RFC9114 7.2.4.1)'};
@@ -335,8 +389,9 @@ parse_ignore(Data, Len) ->
end.
-spec parse_unidi_stream_header(binary())
- -> {ok, control | push | encoder | decoder, binary()}
- | {undefined, binary()}.
+ -> {ok, control | push | encoder | decoder | {webtransport, stream_id()}, binary()}
+ | {undefined, binary()}
+ | more.
parse_unidi_stream_header(<<0, Rest/bits>>) ->
{ok, control, Rest};
@@ -346,6 +401,18 @@ parse_unidi_stream_header(<<2, Rest/bits>>) ->
{ok, encoder, Rest};
parse_unidi_stream_header(<<3, Rest/bits>>) ->
{ok, decoder, Rest};
+%% WebTransport unidi streams.
+parse_unidi_stream_header(<<1:2, 16#54:14, 0:2, SessionID:6, Rest/bits>>) ->
+ {ok, {webtransport, SessionID}, Rest};
+parse_unidi_stream_header(<<1:2, 16#54:14, 1:2, SessionID:14, Rest/bits>>) ->
+ {ok, {webtransport, SessionID}, Rest};
+parse_unidi_stream_header(<<1:2, 16#54:14, 2:2, SessionID:30, Rest/bits>>) ->
+ {ok, {webtransport, SessionID}, Rest};
+parse_unidi_stream_header(<<1:2, 16#54:14, 3:2, SessionID:62, Rest/bits>>) ->
+ {ok, {webtransport, SessionID}, Rest};
+parse_unidi_stream_header(<<1:2, 16#54:14, _/bits>>) ->
+ more;
+%% Unknown unidi streams.
parse_unidi_stream_header(<<0:2, _:6, Rest/bits>>) ->
{undefined, Rest};
parse_unidi_stream_header(<<1:2, _:14, Rest/bits>>) ->
@@ -355,6 +422,13 @@ parse_unidi_stream_header(<<2:2, _:30, Rest/bits>>) ->
parse_unidi_stream_header(<<3:2, _:62, Rest/bits>>) ->
{undefined, Rest}.
+-spec parse_datagram(binary()) -> {stream_id(), binary()}.
+
+parse_datagram(Data) ->
+ {QuarterID, Rest} = parse_int(Data),
+ SessionID = QuarterID * 4,
+ {SessionID, Rest}.
+
-spec code_to_error(non_neg_integer()) -> error().
code_to_error(16#0100) -> h3_no_error;
@@ -374,10 +448,36 @@ code_to_error(16#010d) -> h3_request_incomplete;
code_to_error(16#010e) -> h3_message_error;
code_to_error(16#010f) -> h3_connect_error;
code_to_error(16#0110) -> h3_version_fallback;
+%% Extensions.
+code_to_error(16#33) -> h3_datagram_error;
+code_to_error(16#3994bd84) -> webtransport_buffered_stream_rejected;
+code_to_error(16#170d7b68) -> webtransport_session_gone;
+code_to_error(Code) when Code >= 16#52e4a40fa8db, Code =< 16#52e5ac983162 ->
+ case (Code - 16#21) rem 16#1f of
+ 0 -> h3_no_error;
+ _ ->
+ %% @todo We need tests for this.
+ Shifted = Code - 16#52e4a40fa8db,
+ {webtransport_application_error,
+ Shifted - Shifted div 16#1f}
+ end;
%% Unknown/reserved error codes must be treated
%% as equivalent to H3_NO_ERROR.
code_to_error(_) -> h3_no_error.
+-spec parse_int(binary()) -> {non_neg_integer(), binary()} | more.
+
+parse_int(<<0:2, Int:6, Rest/bits>>) ->
+ {Int, Rest};
+parse_int(<<1:2, Int:14, Rest/bits>>) ->
+ {Int, Rest};
+parse_int(<<2:2, Int:30, Rest/bits>>) ->
+ {Int, Rest};
+parse_int(<<3:2, Int:62, Rest/bits>>) ->
+ {Int, Rest};
+parse_int(_) ->
+ more.
+
%% Building.
-spec data(iodata()) -> iolist().
@@ -414,12 +514,45 @@ settings_payload(Settings) ->
qpack_blocked_streams -> [encode_int(1), encode_int(Value)];
%% SETTINGS_ENABLE_CONNECT_PROTOCOL (RFC9220).
enable_connect_protocol when Value -> [encode_int(8), encode_int(1)];
- enable_connect_protocol -> [encode_int(8), encode_int(0)]
+ enable_connect_protocol -> [encode_int(8), encode_int(0)];
+ %% SETTINGS_H3_DATAGRAM (RFC9297).
+ h3_datagram when Value -> [encode_int(16#33), encode_int(1)];
+ h3_datagram -> [encode_int(16#33), encode_int(0)];
+ %% SETTINGS_ENABLE_WEBTRANSPORT (draft-ietf-webtrans-http3-02, for compatibility).
+ enable_webtransport when Value -> [encode_int(16#2b603742), encode_int(1)];
+ enable_webtransport -> [encode_int(16#2b603742), encode_int(0)];
+ %% SETTINGS_WEBTRANSPORT_MAX_SESSIONS (draft-ietf-webtrans-http3).
+ webtransport_max_sessions when Value =:= 0 -> <<>>;
+ webtransport_max_sessions -> [encode_int(16#c671706a), encode_int(Value)];
+ %% SETTINGS_WEBTRANSPORT_INITIAL_MAX_STREAMS_UNI (draft-ietf-webtrans-http3).
+ webtransport_initial_max_streams_uni when Value =:= 0 -> <<>>;
+ webtransport_initial_max_streams_uni -> [encode_int(16#2b64), encode_int(Value)];
+ %% SETTINGS_WEBTRANSPORT_INITIAL_MAX_STREAMS_BIDI (draft-ietf-webtrans-http3).
+ webtransport_initial_max_streams_bidi when Value =:= 0 -> <<>>;
+ webtransport_initial_max_streams_bidi -> [encode_int(16#2b65), encode_int(Value)];
+ %% SETTINGS_WEBTRANSPORT_INITIAL_MAX_DATA (draft-ietf-webtrans-http3).
+ webtransport_initial_max_data when Value =:= 0 -> <<>>;
+ webtransport_initial_max_data -> [encode_int(16#2b61), encode_int(Value)]
end || {Key, Value} <- maps:to_list(Settings)],
%% Include one reserved identifier in addition.
ReservedType = 16#1f * (rand:uniform(148764065110560900) - 1) + 16#21,
[encode_int(ReservedType), encode_int(rand:uniform(15384) - 1)|Payload].
+-spec webtransport_stream_header(stream_id(), unidi | bidi) -> iolist().
+
+webtransport_stream_header(SessionID, StreamType) ->
+ Signal = case StreamType of
+ unidi -> 16#54;
+ bidi -> 16#41
+ end,
+ [encode_int(Signal), encode_int(SessionID)].
+
+-spec datagram(stream_id(), iodata()) -> iolist().
+
+datagram(SessionID, Data) ->
+ QuarterID = SessionID div 4,
+ [encode_int(QuarterID), Data].
+
-spec error_to_code(error()) -> non_neg_integer().
error_to_code(h3_no_error) ->
@@ -444,9 +577,15 @@ error_to_code(h3_request_cancelled) -> 16#010c;
error_to_code(h3_request_incomplete) -> 16#010d;
error_to_code(h3_message_error) -> 16#010e;
error_to_code(h3_connect_error) -> 16#010f;
-error_to_code(h3_version_fallback) -> 16#0110.
-
--spec encode_int(0..16#3fffffffffffffff) -> binary().
+error_to_code(h3_version_fallback) -> 16#0110;
+%% Extensions.
+error_to_code(h3_datagram_error) -> 16#33;
+error_to_code(webtransport_buffered_stream_rejected) -> 16#3994bd84;
+error_to_code(webtransport_session_gone) -> 16#170d7b68;
+error_to_code({webtransport_application_error, AppErrorCode}) ->
+ 16#52e4a40fa8db + AppErrorCode + AppErrorCode div 16#1e.
+
+-spec encode_int(h3_non_neg_integer()) -> binary().
encode_int(I) when I < 64 ->
<<0:2, I:6>>;
diff --git a/src/cow_http3_machine.erl b/src/cow_http3_machine.erl
index 9cd83d6..7a01703 100644
--- a/src/cow_http3_machine.erl
+++ b/src/cow_http3_machine.erl
@@ -20,6 +20,9 @@
-export([set_unidi_remote_stream_type/3]).
-export([init_bidi_stream/2]).
-export([init_bidi_stream/3]).
+-export([become_webtransport_session/2]).
+-export([become_webtransport_stream/3]).
+-export([close_webtransport_session/2]).
-export([close_bidi_stream_for_sending/2]).
-export([close_stream/2]).
-export([unidi_data/4]).
@@ -43,6 +46,9 @@
-type unidi_stream_dir() :: unidi_local | unidi_remote.
-type unidi_stream_type() :: control | push | encoder | decoder.
+%% All stream types must have `id` as the first element
+%% of the record as the more general functions require it there.
+
-record(unidi_stream, {
id :: cow_http3:stream_id(),
@@ -74,7 +80,21 @@
te :: undefined | binary()
}).
--type stream() :: #unidi_stream{} | #bidi_stream{}.
+-record(wt_session, {
+ id :: cow_http3:stream_id()
+}).
+
+-record(wt_stream, {
+ id :: cow_http3:stream_id(),
+
+ %% All WT streams belong to a single WT session.
+ session_id :: cow_http3:stream_id(),
+
+ %% Unidi stream direction (local = we initiated) or bidi.
+ dir :: unidi_stream_dir() | bidi
+}).
+
+-type stream() :: #unidi_stream{} | #bidi_stream{} | #wt_session{} | #wt_stream{}.
-record(http3_machine, {
%% Whether the HTTP/3 endpoint is a client or a server.
@@ -132,8 +152,15 @@ init_settings(Opts) ->
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).
+ S2 = setting_from_opt(S1, Opts, enable_connect_protocol,
+ enable_connect_protocol, false),
+ S3 = setting_from_opt(S2, Opts, h3_datagram,
+ h3_datagram, false),
+ %% For compatibility with draft-02.
+ S4 = setting_from_opt(S3, Opts, enable_webtransport,
+ enable_webtransport, false),
+ setting_from_opt(S4, Opts, webtransport_max_sessions,
+ webtransport_max_sessions, 0).
setting_from_opt(Settings, Opts, OptName, SettingName, Default) ->
case maps:get(OptName, Opts, Default) of
@@ -227,6 +254,49 @@ init_bidi_stream(StreamID, Method, State=#http3_machine{streams=Streams}) ->
StreamID => #bidi_stream{id=StreamID, method=Method}
}}.
+-spec become_webtransport_session(cow_http3:stream_id(), State)
+ -> State when State::http3_machine().
+
+become_webtransport_session(StreamID, State=#http3_machine{streams=Streams}) ->
+ #{StreamID := #bidi_stream{}} = Streams,
+ stream_store(#wt_session{id=StreamID}, State).
+
+-spec become_webtransport_stream(cow_http3:stream_id(), cow_http3:stream_id(), State)
+ -> {ok, State} when State::http3_machine().
+
+become_webtransport_stream(StreamID, SessionID, State0) ->
+ %% First we check whether SessionID really exists and is a WT session.
+ case stream_get(SessionID, State0) of
+ #wt_session{} ->
+ %% The stream becomes a WT stream tied to SessionID.
+ Dir = case stream_get(StreamID, State0) of
+ #unidi_stream{dir=Dir0} -> Dir0;
+ %% @todo The bidi stream must be in idle state.
+ #bidi_stream{} -> bidi
+ end,
+ State = stream_store(#wt_stream{
+ id=StreamID, session_id=SessionID, dir=Dir},
+ State0),
+ {ok, State}
+ %% @todo Error conditions.
+ end.
+
+-spec close_webtransport_session(cow_http3:stream_id(), State)
+ -> State when State::http3_machine().
+
+close_webtransport_session(SessionID, State=#http3_machine{streams=Streams0}) ->
+ #{SessionID := #wt_session{}} = Streams0,
+ %% Remove all streams belonging to the session.
+ Streams = maps:filtermap(fun
+ (_, #wt_session{id=StreamID}) when StreamID =:= SessionID ->
+ false;
+ (_, #wt_stream{session_id=StreamID}) when StreamID =:= SessionID ->
+ false;
+ (_, _) ->
+ true
+ end, Streams0),
+ State#http3_machine{streams=Streams}.
+
-spec close_bidi_stream_for_sending(cow_http3:stream_id(), State)
-> State when State::http3_machine().
@@ -399,6 +469,9 @@ 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
+ %% @todo If this is a webtransport request we also need to check a few
+ %% other things such as h3_datagram, max_sessions and QUIC's max_datagram_size options.
+ %% @todo So cow_http3_machine needs to know about at least some QUIC options.
{headers, Headers, PseudoHeaders, Len} ->
headers_frame(Stream, State, IsFin, Type, DecData, Headers, PseudoHeaders, Len);
% {push_promise, Headers, PseudoHeaders} -> %% @todo Implement push promises.
@@ -714,8 +787,5 @@ 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,
+ StreamID = element(2, Stream),
State#http3_machine{streams=Streams#{StreamID => Stream}}.
diff --git a/src/cow_http_hd.erl b/src/cow_http_hd.erl
index 2d5bbc2..1ff9bca 100644
--- a/src/cow_http_hd.erl
+++ b/src/cow_http_hd.erl
@@ -118,6 +118,10 @@
% @todo -export([parse_via/1]). RFC7230
% @todo -export([parse_want_digest/1]). RFC3230
% @todo -export([parse_warning/1]). RFC7234
+-export([parse_wt_available_protocols/1]).
+-export([wt_available_protocols/1]).
+-export([parse_wt_protocol/1]).
+-export([wt_protocol/1]).
-export([parse_www_authenticate/1]).
% @todo -export([parse_x_content_duration/1]). Gecko/MDN (value: float)
% @todo -export([parse_x_dns_prefetch_control/1]). Various (value: "on"|"off")
@@ -3393,6 +3397,40 @@ parse_vary_error_test_() ->
[{V, fun() -> {'EXIT', _} = (catch parse_vary(V)) end} || V <- Tests].
-endif.
+%% WT-Available-Protocols header.
+
+-spec parse_wt_available_protocols(binary()) -> [binary()].
+
+parse_wt_available_protocols(Protocols) ->
+ List = cow_http_struct_hd:parse_list(Protocols),
+ [case Item of
+ {item, {string, Value}, _} -> Value
+ end || Item <- List].
+
+-spec wt_available_protocols([binary()]) -> iolist().
+
+wt_available_protocols(Protocols) ->
+ cow_http_struct_hd:list([
+ {item, {string, Value}, []}
+ || Value <- Protocols]).
+
+%% @todo Tests.
+
+%% WT-Protocol header.
+
+-spec parse_wt_protocol(binary()) -> binary().
+
+parse_wt_protocol(WTProtocol) ->
+ {item, {string, Value}, _} = cow_http_struct_hd:parse_item(WTProtocol),
+ Value.
+
+-spec wt_protocol(iodata()) -> iolist().
+
+wt_protocol(WTProtocol) ->
+ cow_http_struct_hd:item({item, {string, WTProtocol}, []}).
+
+%% @todo Tests.
+
%% WWW-Authenticate header.
%%
%% Unknown schemes are represented as the lowercase binary