Skip to content

Commit f8bb8fc

Browse files
committed
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.
1 parent f8d0ad7 commit f8bb8fc

File tree

5 files changed

+364
-19
lines changed

5 files changed

+364
-19
lines changed

ebin/cowlib.app

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{application, 'cowlib', [
22
{description, "Support library for manipulating Web protocols."},
33
{vsn, "2.15.0"},
4-
{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']},
4+
{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']},
55
{registered, []},
66
{applications, [kernel,stdlib,crypto]},
77
{optional_applications, []},

src/cow_capsule.erl

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
%% Copyright (c) Loïc Hoguin <[email protected]>
2+
%%
3+
%% Permission to use, copy, modify, and/or distribute this software for any
4+
%% purpose with or without fee is hereby granted, provided that the above
5+
%% copyright notice and this permission notice appear in all copies.
6+
%%
7+
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8+
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9+
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10+
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11+
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12+
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13+
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14+
15+
-module(cow_capsule).
16+
17+
%% Parsing.
18+
-export([parse/1]).
19+
20+
%% Building.
21+
-export([wt_drain_session/0]).
22+
-export([wt_close_session/2]).
23+
24+
-type capsule() ::
25+
wt_drain_session |
26+
{wt_close_session, cow_http3:wt_app_error_code(), binary()}.
27+
28+
%% Parsing.
29+
30+
-spec parse(binary())
31+
-> {ok, capsule(), binary()}
32+
| {ok, binary()} %% Unknown capsule gets skipped.
33+
| more
34+
| {skip, non_neg_integer()} %% Unknown capsule; remaining length to skip.
35+
| error.
36+
37+
%% @todo Handle DATAGRAM capsules. {datagram, binary()}
38+
parse(<<2:2, 16#78ae:30, 0, Rest/bits>>) ->
39+
{ok, wt_drain_session, Rest};
40+
parse(<<1:2, 16#2843:14, Rest0/bits>>) when byte_size(Rest0) >= 5 ->
41+
LenOrError = case Rest0 of
42+
<<0:2, Len0:6, Rest1/bits>> ->
43+
{Len0, Rest1};
44+
<<1:2, Len0:14, Rest1/bits>> when Len0 =< 1028 ->
45+
{Len0, Rest1};
46+
%% AppCode is 4 bytes and AppMsg is up to 1024 bytes.
47+
_ ->
48+
error
49+
end,
50+
case LenOrError of
51+
{Len1, Rest2} ->
52+
AppMsgLen = Len1 - 4,
53+
case Rest2 of
54+
<<AppCode:32, AppMsg:AppMsgLen/binary, Rest/bits>> ->
55+
{ok, {wt_close_session, AppCode, AppMsg}, Rest};
56+
_ ->
57+
more
58+
end;
59+
error ->
60+
error
61+
end;
62+
parse(<<>>) ->
63+
more;
64+
%% Skip unknown capsules.
65+
parse(Data) ->
66+
%% @todo This can use maybe_expr in OTP-25+.
67+
case cow_http3:parse_int(Data) of
68+
more ->
69+
more;
70+
{_Type, Rest0} ->
71+
case cow_http3:parse_int(Rest0) of
72+
more ->
73+
more;
74+
{Len, Rest1} ->
75+
case Rest1 of
76+
<<_:Len/unit:8, Rest>> ->
77+
{ok, Rest};
78+
_ ->
79+
{skip, Len - byte_size(Rest1)}
80+
end
81+
end
82+
end.
83+
84+
%% Building.
85+
86+
-spec wt_drain_session() -> binary().
87+
88+
%% @todo Where should I put capsules?
89+
wt_drain_session() ->
90+
<<2:2, 16#78ae:30, 0>>.
91+
92+
-spec wt_close_session(cow_http3:wt_app_error_code(), iodata()) -> iodata().
93+
94+
wt_close_session(AppCode, <<>>) ->
95+
<<1:2, 16#2843:14, 4, AppCode:32>>;
96+
wt_close_session(AppCode, AppMsg) ->
97+
Len = 4 + iolist_size(AppMsg),
98+
[<<1:2, 16#2843:14>>, cow_http3:encode_int(Len), <<AppCode:32>>, AppMsg].

src/cow_http3.erl

Lines changed: 150 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,16 @@
1717
%% Parsing.
1818
-export([parse/1]).
1919
-export([parse_unidi_stream_header/1]).
20+
-export([parse_datagram/1]).
2021
-export([code_to_error/1]).
22+
-export([parse_int/1]).
2123

2224
%% Building.
2325
-export([data/1]).
2426
-export([headers/1]).
2527
-export([settings/1]).
28+
-export([webtransport_stream_header/2]).
29+
-export([datagram/2]).
2630
-export([error_to_code/1]).
2731
-export([encode_int/1]).
2832

@@ -32,14 +36,25 @@
3236
-type push_id() :: non_neg_integer().
3337
-export_type([push_id/0]).
3438

39+
-type h3_non_neg_integer() :: 0..16#3fffffffffffffff.
40+
3541
-type settings() :: #{
36-
qpack_max_table_capacity => 0..16#3fffffffffffffff,
37-
max_field_section_size => 0..16#3fffffffffffffff,
38-
qpack_blocked_streams => 0..16#3fffffffffffffff,
39-
enable_connect_protocol => boolean()
42+
qpack_max_table_capacity => h3_non_neg_integer(),
43+
max_field_section_size => h3_non_neg_integer(),
44+
qpack_blocked_streams => h3_non_neg_integer(),
45+
enable_connect_protocol => boolean(),
46+
%% Extensions.
47+
h3_datagram => boolean(),
48+
webtransport_max_sessions => h3_non_neg_integer(),
49+
webtransport_initial_max_streams_uni => h3_non_neg_integer(),
50+
webtransport_initial_max_streams_bidi => h3_non_neg_integer(),
51+
webtransport_initial_max_data => h3_non_neg_integer()
4052
}.
4153
-export_type([settings/0]).
4254

55+
-type wt_app_error_code() :: 0..16#ffffffff.
56+
-export_type([wt_app_error_code/0]).
57+
4358
-type error() :: h3_no_error
4459
| h3_general_protocol_error
4560
| h3_internal_error
@@ -56,7 +71,12 @@
5671
| h3_request_incomplete
5772
| h3_message_error
5873
| h3_connect_error
59-
| h3_version_fallback.
74+
| h3_version_fallback
75+
%% Extensions.
76+
| h3_datagram_error
77+
| webtransport_buffered_stream_rejected
78+
| webtransport_session_gone
79+
| {webtransport_application_error, wt_app_error_code()}.
6080
-export_type([error/0]).
6181

6282
-type frame() :: {data, binary()}
@@ -72,6 +92,7 @@
7292

7393
-spec parse(binary())
7494
-> {ok, frame(), binary()}
95+
| {webtransport_stream_header, stream_id(), binary()}
7596
| {more, {data, binary()} | ignore, non_neg_integer()}
7697
| {ignore, binary()}
7798
| {connection_error, h3_frame_error | h3_frame_unexpected | h3_settings_error, atom()}
@@ -191,6 +212,19 @@ parse(<<13, _/bits>>) ->
191212
{connection_error, h3_frame_error,
192213
'MAX_PUSH_ID frames payload MUST be 1, 2, 4 or 8 bytes wide. (RFC9114 7.1, RFC9114 7.2.6)'};
193214
%%
215+
%% WebTransport stream header.
216+
%%
217+
parse(<<1:2, 16#41:14, 0:2, SessionID:6, Rest/bits>>) ->
218+
{webtransport_stream_header, SessionID, Rest};
219+
parse(<<1:2, 16#41:14, 1:2, SessionID:14, Rest/bits>>) ->
220+
{webtransport_stream_header, SessionID, Rest};
221+
parse(<<1:2, 16#41:14, 2:2, SessionID:30, Rest/bits>>) ->
222+
{webtransport_stream_header, SessionID, Rest};
223+
parse(<<1:2, 16#41:14, 3:2, SessionID:62, Rest/bits>>) ->
224+
{webtransport_stream_header, SessionID, Rest};
225+
parse(<<16#41, _/bits>>) ->
226+
more;
227+
%%
194228
%% HTTP/2 frame types must be rejected.
195229
%%
196230
parse(<<2, _/bits>>) ->
@@ -294,6 +328,26 @@ parse_settings_id_val(Rest, Len, Settings, Identifier, Value) ->
294328
8 ->
295329
{connection_error, h3_settings_error,
296330
'The SETTINGS_ENABLE_CONNECT_PROTOCOL value MUST be 0 or 1. (RFC9220 3, RFC8441 3)'};
331+
%% SETTINGS_H3_DATAGRAM (RFC9297).
332+
16#33 when Value =:= 0 ->
333+
parse_settings_key_val(Rest, Len, Settings, h3_datagram, false);
334+
16#33 when Value =:= 1 ->
335+
parse_settings_key_val(Rest, Len, Settings, h3_datagram, true);
336+
16#33 ->
337+
{connection_error, h3_settings_error,
338+
'The SETTINGS_H3_DATAGRAM value MUST be 0 or 1. (RFC9297 2.1.1)'};
339+
%% SETTINGS_WEBTRANSPORT_MAX_SESSIONS (draft-ietf-webtrans-http3).
340+
16#c671706a ->
341+
parse_settings_key_val(Rest, Len, Settings, webtransport_max_sessions, Value);
342+
%% SETTINGS_WEBTRANSPORT_INITIAL_MAX_STREAMS_UNI (draft-ietf-webtrans-http3).
343+
16#2b64 ->
344+
parse_settings_key_val(Rest, Len, Settings, webtransport_initial_max_streams_uni, Value);
345+
%% SETTINGS_WEBTRANSPORT_INITIAL_MAX_STREAMS_BIDI (draft-ietf-webtrans-http3).
346+
16#2b65 ->
347+
parse_settings_key_val(Rest, Len, Settings, webtransport_initial_max_streams_bidi, Value);
348+
%% SETTINGS_WEBTRANSPORT_INITIAL_MAX_DATA (draft-ietf-webtrans-http3).
349+
16#2b61 ->
350+
parse_settings_key_val(Rest, Len, Settings, webtransport_initial_max_data, Value);
297351
_ when Identifier < 6 ->
298352
{connection_error, h3_settings_error,
299353
'HTTP/2 setting not defined for HTTP/3 must be rejected. (RFC9114 7.2.4.1)'};
@@ -335,8 +389,9 @@ parse_ignore(Data, Len) ->
335389
end.
336390

337391
-spec parse_unidi_stream_header(binary())
338-
-> {ok, control | push | encoder | decoder, binary()}
339-
| {undefined, binary()}.
392+
-> {ok, control | push | encoder | decoder | {webtransport, stream_id()}, binary()}
393+
| {undefined, binary()}
394+
| more.
340395

341396
parse_unidi_stream_header(<<0, Rest/bits>>) ->
342397
{ok, control, Rest};
@@ -346,6 +401,18 @@ parse_unidi_stream_header(<<2, Rest/bits>>) ->
346401
{ok, encoder, Rest};
347402
parse_unidi_stream_header(<<3, Rest/bits>>) ->
348403
{ok, decoder, Rest};
404+
%% WebTransport unidi streams.
405+
parse_unidi_stream_header(<<1:2, 16#54:14, 0:2, SessionID:6, Rest/bits>>) ->
406+
{ok, {webtransport, SessionID}, Rest};
407+
parse_unidi_stream_header(<<1:2, 16#54:14, 1:2, SessionID:14, Rest/bits>>) ->
408+
{ok, {webtransport, SessionID}, Rest};
409+
parse_unidi_stream_header(<<1:2, 16#54:14, 2:2, SessionID:30, Rest/bits>>) ->
410+
{ok, {webtransport, SessionID}, Rest};
411+
parse_unidi_stream_header(<<1:2, 16#54:14, 3:2, SessionID:62, Rest/bits>>) ->
412+
{ok, {webtransport, SessionID}, Rest};
413+
parse_unidi_stream_header(<<1:2, 16#54:14, _/bits>>) ->
414+
more;
415+
%% Unknown unidi streams.
349416
parse_unidi_stream_header(<<0:2, _:6, Rest/bits>>) ->
350417
{undefined, Rest};
351418
parse_unidi_stream_header(<<1:2, _:14, Rest/bits>>) ->
@@ -355,6 +422,13 @@ parse_unidi_stream_header(<<2:2, _:30, Rest/bits>>) ->
355422
parse_unidi_stream_header(<<3:2, _:62, Rest/bits>>) ->
356423
{undefined, Rest}.
357424

425+
-spec parse_datagram(binary()) -> {stream_id(), binary()}.
426+
427+
parse_datagram(Data) ->
428+
{QuarterID, Rest} = parse_int(Data),
429+
SessionID = QuarterID * 4,
430+
{SessionID, Rest}.
431+
358432
-spec code_to_error(non_neg_integer()) -> error().
359433

360434
code_to_error(16#0100) -> h3_no_error;
@@ -374,10 +448,36 @@ code_to_error(16#010d) -> h3_request_incomplete;
374448
code_to_error(16#010e) -> h3_message_error;
375449
code_to_error(16#010f) -> h3_connect_error;
376450
code_to_error(16#0110) -> h3_version_fallback;
451+
%% Extensions.
452+
code_to_error(16#33) -> h3_datagram_error;
453+
code_to_error(16#3994bd84) -> webtransport_buffered_stream_rejected;
454+
code_to_error(16#170d7b68) -> webtransport_session_gone;
455+
code_to_error(Code) when Code >= 16#52e4a40fa8db, Code =< 16#52e5ac983162 ->
456+
case (Code - 16#21) rem 16#1f of
457+
0 -> h3_no_error;
458+
_ ->
459+
%% @todo We need tests for this.
460+
Shifted = Code - 16#52e4a40fa8db,
461+
{webtransport_application_error,
462+
Shifted - Shifted div 16#1f}
463+
end;
377464
%% Unknown/reserved error codes must be treated
378465
%% as equivalent to H3_NO_ERROR.
379466
code_to_error(_) -> h3_no_error.
380467

468+
-spec parse_int(binary()) -> {non_neg_integer(), binary()} | more.
469+
470+
parse_int(<<0:2, Int:6, Rest/bits>>) ->
471+
{Int, Rest};
472+
parse_int(<<1:2, Int:14, Rest/bits>>) ->
473+
{Int, Rest};
474+
parse_int(<<2:2, Int:30, Rest/bits>>) ->
475+
{Int, Rest};
476+
parse_int(<<3:2, Int:62, Rest/bits>>) ->
477+
{Int, Rest};
478+
parse_int(_) ->
479+
more.
480+
381481
%% Building.
382482

383483
-spec data(iodata()) -> iolist().
@@ -414,12 +514,45 @@ settings_payload(Settings) ->
414514
qpack_blocked_streams -> [encode_int(1), encode_int(Value)];
415515
%% SETTINGS_ENABLE_CONNECT_PROTOCOL (RFC9220).
416516
enable_connect_protocol when Value -> [encode_int(8), encode_int(1)];
417-
enable_connect_protocol -> [encode_int(8), encode_int(0)]
517+
enable_connect_protocol -> [encode_int(8), encode_int(0)];
518+
%% SETTINGS_H3_DATAGRAM (RFC9297).
519+
h3_datagram when Value -> [encode_int(16#33), encode_int(1)];
520+
h3_datagram -> [encode_int(16#33), encode_int(0)];
521+
%% SETTINGS_ENABLE_WEBTRANSPORT (draft-ietf-webtrans-http3-02, for compatibility).
522+
enable_webtransport when Value -> [encode_int(16#2b603742), encode_int(1)];
523+
enable_webtransport -> [encode_int(16#2b603742), encode_int(0)];
524+
%% SETTINGS_WEBTRANSPORT_MAX_SESSIONS (draft-ietf-webtrans-http3).
525+
webtransport_max_sessions when Value =:= 0 -> <<>>;
526+
webtransport_max_sessions -> [encode_int(16#c671706a), encode_int(Value)];
527+
%% SETTINGS_WEBTRANSPORT_INITIAL_MAX_STREAMS_UNI (draft-ietf-webtrans-http3).
528+
webtransport_initial_max_streams_uni when Value =:= 0 -> <<>>;
529+
webtransport_initial_max_streams_uni -> [encode_int(16#2b64), encode_int(Value)];
530+
%% SETTINGS_WEBTRANSPORT_INITIAL_MAX_STREAMS_BIDI (draft-ietf-webtrans-http3).
531+
webtransport_initial_max_streams_bidi when Value =:= 0 -> <<>>;
532+
webtransport_initial_max_streams_bidi -> [encode_int(16#2b65), encode_int(Value)];
533+
%% SETTINGS_WEBTRANSPORT_INITIAL_MAX_DATA (draft-ietf-webtrans-http3).
534+
webtransport_initial_max_data when Value =:= 0 -> <<>>;
535+
webtransport_initial_max_data -> [encode_int(16#2b61), encode_int(Value)]
418536
end || {Key, Value} <- maps:to_list(Settings)],
419537
%% Include one reserved identifier in addition.
420538
ReservedType = 16#1f * (rand:uniform(148764065110560900) - 1) + 16#21,
421539
[encode_int(ReservedType), encode_int(rand:uniform(15384) - 1)|Payload].
422540

541+
-spec webtransport_stream_header(stream_id(), unidi | bidi) -> iolist().
542+
543+
webtransport_stream_header(SessionID, StreamType) ->
544+
Signal = case StreamType of
545+
unidi -> 16#54;
546+
bidi -> 16#41
547+
end,
548+
[encode_int(Signal), encode_int(SessionID)].
549+
550+
-spec datagram(stream_id(), iodata()) -> iolist().
551+
552+
datagram(SessionID, Data) ->
553+
QuarterID = SessionID div 4,
554+
[encode_int(QuarterID), Data].
555+
423556
-spec error_to_code(error()) -> non_neg_integer().
424557

425558
error_to_code(h3_no_error) ->
@@ -444,9 +577,15 @@ error_to_code(h3_request_cancelled) -> 16#010c;
444577
error_to_code(h3_request_incomplete) -> 16#010d;
445578
error_to_code(h3_message_error) -> 16#010e;
446579
error_to_code(h3_connect_error) -> 16#010f;
447-
error_to_code(h3_version_fallback) -> 16#0110.
448-
449-
-spec encode_int(0..16#3fffffffffffffff) -> binary().
580+
error_to_code(h3_version_fallback) -> 16#0110;
581+
%% Extensions.
582+
error_to_code(h3_datagram_error) -> 16#33;
583+
error_to_code(webtransport_buffered_stream_rejected) -> 16#3994bd84;
584+
error_to_code(webtransport_session_gone) -> 16#170d7b68;
585+
error_to_code({webtransport_application_error, AppErrorCode}) ->
586+
16#52e4a40fa8db + AppErrorCode + AppErrorCode div 16#1e.
587+
588+
-spec encode_int(h3_non_neg_integer()) -> binary().
450589

451590
encode_int(I) when I < 64 ->
452591
<<0:2, I:6>>;

0 commit comments

Comments
 (0)