This is an automated email from the ASF dual-hosted git repository.

rnewson pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/couchdb-cowlib.git

commit f8bb8fc23658b6e9decc2b9ed1d33fa31656fe49
Author: Loïc Hoguin <[email protected]>
AuthorDate: Tue Apr 15 15:26:43 2025 +0200

    Initial implementation of HTTP/3 WebTransport
    
    This adds support for WebTransport HTTP/3 settings, error codes,
    headers, stream types as well as capsules, with an initial
    implementation of the Capsule protocol.
---
 ebin/cowlib.app           |   2 +-
 src/cow_capsule.erl       |  98 ++++++++++++++++++++++++++++
 src/cow_http3.erl         | 161 ++++++++++++++++++++++++++++++++++++++++++----
 src/cow_http3_machine.erl |  84 ++++++++++++++++++++++--
 src/cow_http_hd.erl       |  38 +++++++++++
 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

Reply via email to