diff options
-rw-r--r-- | ebin/cowlib.app | 2 | ||||
-rw-r--r-- | src/cow_capsule.erl | 98 | ||||
-rw-r--r-- | src/cow_http3.erl | 161 | ||||
-rw-r--r-- | src/cow_http3_machine.erl | 84 | ||||
-rw-r--r-- | 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 |