You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by rn...@apache.org on 2025/07/16 19:34:02 UTC

(couchdb-cowlib) branch master created (now e2d7749)

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

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


      at e2d7749  Update to most recent WebTransport draft

This branch includes the following new commits:

     new f8bb8fc  Initial implementation of HTTP/3 WebTransport
     new e2d7749  Update to most recent WebTransport draft

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(couchdb-cowlib) 01/02: Initial implementation of HTTP/3 WebTransport

Posted by rn...@apache.org.
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 <es...@ninenines.eu>
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 <es...@ninenines.eu>
+%%
+%% 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


(couchdb-cowlib) 02/02: Update to most recent WebTransport draft

Posted by rn...@apache.org.
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 e2d7749f61b89cc6f8779ba66a5a8ab0fe85c827
Author: Loïc Hoguin <es...@ninenines.eu>
AuthorDate: Wed Jun 25 11:44:09 2025 +0200

    Update to most recent WebTransport draft
    
    WEBTRANSPORT -> WT in most places. Only ENABLE_WEBTRANSPORT
    remains (since it is a draft-02 setting that was later removed)
    as well as internal functions and atoms about stream headers,
    for greater clarity.
---
 src/cow_http3.erl         | 66 +++++++++++++++++++++++------------------------
 src/cow_http3_machine.erl |  6 ++---
 2 files changed, 36 insertions(+), 36 deletions(-)

diff --git a/src/cow_http3.erl b/src/cow_http3.erl
index f1cb0f7..d950500 100644
--- a/src/cow_http3.erl
+++ b/src/cow_http3.erl
@@ -45,10 +45,10 @@
 	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()
+	wt_max_sessions => h3_non_neg_integer(),
+	wt_initial_max_streams_uni => h3_non_neg_integer(),
+	wt_initial_max_streams_bidi => h3_non_neg_integer(),
+	wt_initial_max_data => h3_non_neg_integer()
 }.
 -export_type([settings/0]).
 
@@ -74,9 +74,9 @@
 	| h3_version_fallback
 	%% Extensions.
 	| h3_datagram_error
-	| webtransport_buffered_stream_rejected
-	| webtransport_session_gone
-	| {webtransport_application_error, wt_app_error_code()}.
+	| wt_buffered_stream_rejected
+	| wt_session_gone
+	| {wt_application_error, wt_app_error_code()}.
 -export_type([error/0]).
 
 -type frame() :: {data, binary()}
@@ -336,18 +336,18 @@ parse_settings_id_val(Rest, Len, Settings, Identifier, Value) ->
 		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).
+		%% SETTINGS_WT_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).
+			parse_settings_key_val(Rest, Len, Settings, wt_max_sessions, Value);
+		%% SETTINGS_WT_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).
+			parse_settings_key_val(Rest, Len, Settings, wt_initial_max_streams_uni, Value);
+		%% SETTINGS_WT_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).
+			parse_settings_key_val(Rest, Len, Settings, wt_initial_max_streams_bidi, Value);
+		%% SETTINGS_WT_INITIAL_MAX_DATA (draft-ietf-webtrans-http3).
 		16#2b61 ->
-			parse_settings_key_val(Rest, Len, Settings, webtransport_initial_max_data, Value);
+			parse_settings_key_val(Rest, Len, Settings, wt_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)'};
@@ -450,15 +450,15 @@ 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(16#3994bd84) -> wt_buffered_stream_rejected;
+code_to_error(16#170d7b68) -> wt_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,
+			{wt_application_error,
 				Shifted - Shifted div 16#1f}
 	end;
 %% Unknown/reserved error codes must be treated
@@ -521,18 +521,18 @@ settings_payload(Settings) ->
 		%% 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)]
+		%% SETTINGS_WT_MAX_SESSIONS (draft-ietf-webtrans-http3).
+		wt_max_sessions when Value =:= 0 -> <<>>;
+		wt_max_sessions -> [encode_int(16#c671706a), encode_int(Value)];
+		%% SETTINGS_WT_INITIAL_MAX_STREAMS_UNI (draft-ietf-webtrans-http3).
+		wt_initial_max_streams_uni when Value =:= 0 -> <<>>;
+		wt_initial_max_streams_uni -> [encode_int(16#2b64), encode_int(Value)];
+		%% SETTINGS_WT_INITIAL_MAX_STREAMS_BIDI (draft-ietf-webtrans-http3).
+		wt_initial_max_streams_bidi when Value =:= 0 -> <<>>;
+		wt_initial_max_streams_bidi -> [encode_int(16#2b65), encode_int(Value)];
+		%% SETTINGS_WT_INITIAL_MAX_DATA (draft-ietf-webtrans-http3).
+		wt_initial_max_data when Value =:= 0 -> <<>>;
+		wt_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,
@@ -580,9 +580,9 @@ error_to_code(h3_connect_error) -> 16#010f;
 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}) ->
+error_to_code(wt_buffered_stream_rejected) -> 16#3994bd84;
+error_to_code(wt_session_gone) -> 16#170d7b68;
+error_to_code({wt_application_error, AppErrorCode}) ->
 	16#52e4a40fa8db + AppErrorCode + AppErrorCode div 16#1e.
 
 -spec encode_int(h3_non_neg_integer()) -> binary().
diff --git a/src/cow_http3_machine.erl b/src/cow_http3_machine.erl
index 7a01703..515f87a 100644
--- a/src/cow_http3_machine.erl
+++ b/src/cow_http3_machine.erl
@@ -159,8 +159,8 @@ init_settings(Opts) ->
 	%% 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(S4, Opts, wt_max_sessions,
+		wt_max_sessions, 0).
 
 setting_from_opt(Settings, Opts, OptName, SettingName, Default) ->
 	case maps:get(OptName, Opts, Default) of
@@ -469,7 +469,7 @@ 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
+		%% @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} ->