diff options
Diffstat (limited to 'src/cow_http3.erl')
-rw-r--r-- | src/cow_http3.erl | 161 |
1 files changed, 150 insertions, 11 deletions
diff --git a/src/cow_http3.erl b/src/cow_http3.erl index 4e9c984..d950500 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(), + 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]). +-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 + | wt_buffered_stream_rejected + | wt_session_gone + | {wt_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_WT_MAX_SESSIONS (draft-ietf-webtrans-http3). + 16#c671706a -> + 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, 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, wt_initial_max_streams_bidi, Value); + %% SETTINGS_WT_INITIAL_MAX_DATA (draft-ietf-webtrans-http3). + 16#2b61 -> + 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)'}; @@ -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) -> 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, + {wt_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_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, [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(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(). encode_int(I) when I < 64 -> <<0:2, I:6>>; |