mirror of
https://github.com/ninenines/cowboy.git
synced 2025-07-14 12:20:24 +00:00
WIP
This commit is contained in:
parent
c43384c8de
commit
8c2bdb1e22
7 changed files with 318 additions and 27 deletions
|
@ -198,6 +198,8 @@ handle_quic_msg(State0=#state{opts=Opts}, Msg) ->
|
||||||
case cowboy_quicer:handle(Msg) of
|
case cowboy_quicer:handle(Msg) of
|
||||||
{data, StreamID, IsFin, Data} ->
|
{data, StreamID, IsFin, Data} ->
|
||||||
parse(State0, StreamID, Data, IsFin);
|
parse(State0, StreamID, Data, IsFin);
|
||||||
|
{datagram, Data} ->
|
||||||
|
parse_datagram(State0, Data);
|
||||||
{stream_started, StreamID, StreamType} ->
|
{stream_started, StreamID, StreamType} ->
|
||||||
State = stream_new_remote(State0, StreamID, StreamType),
|
State = stream_new_remote(State0, StreamID, StreamType),
|
||||||
loop(State);
|
loop(State);
|
||||||
|
@ -551,6 +553,36 @@ early_error(State0=#state{ref=Ref, opts=Opts, peer=Peer},
|
||||||
send_headers(State0, Stream, fin, StatusCode0, RespHeaders0)
|
send_headers(State0, Stream, fin, StatusCode0, RespHeaders0)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%% Datagrams.
|
||||||
|
|
||||||
|
parse_datagram(State, Data) ->
|
||||||
|
case parse_var_int(Data) of
|
||||||
|
{ok, QuarterID, Rest} ->
|
||||||
|
SessionID = QuarterID * 4,
|
||||||
|
case stream_get(State, SessionID) of
|
||||||
|
#stream{status=webtransport_session} ->
|
||||||
|
webtransport_event(State, SessionID, {datagram, Rest}),
|
||||||
|
loop(State);
|
||||||
|
_ ->
|
||||||
|
error(todo) %% @todo Might be a future WT session or an error.
|
||||||
|
end;
|
||||||
|
%% Ignore invalid datagrams. @todo Is that behavior correct?
|
||||||
|
more ->
|
||||||
|
loop(State)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @todo Move to Cowlib and use in cow_http3.
|
||||||
|
parse_var_int(<<0:2, Int:6, Rest/bits>>) ->
|
||||||
|
{ok, Int, Rest};
|
||||||
|
parse_var_int(<<1:2, Int:14, Rest/bits>>) ->
|
||||||
|
{ok, Int, Rest};
|
||||||
|
parse_var_int(<<2:2, Int:30, Rest/bits>>) ->
|
||||||
|
{ok, Int, Rest};
|
||||||
|
parse_var_int(<<3:2, Int:62, Rest/bits>>) ->
|
||||||
|
{ok, Int, Rest};
|
||||||
|
parse_var_int(_) ->
|
||||||
|
more.
|
||||||
|
|
||||||
%% Erlang messages.
|
%% Erlang messages.
|
||||||
|
|
||||||
down(State0=#state{opts=Opts, children=Children0}, Pid, Msg) ->
|
down(State0=#state{opts=Opts, children=Children0}, Pid, Msg) ->
|
||||||
|
@ -874,7 +906,7 @@ webtransport_commands(State, SessionID, Commands) ->
|
||||||
|
|
||||||
wt_commands(State, _, []) ->
|
wt_commands(State, _, []) ->
|
||||||
State;
|
State;
|
||||||
wt_commands(State=#state{conn=Conn}, Session=#stream{id=SessionID},
|
wt_commands(State0=#state{conn=Conn}, Session=#stream{id=SessionID},
|
||||||
[{open_stream, OpenStreamRef, StreamType, InitialData}|Tail]) ->
|
[{open_stream, OpenStreamRef, StreamType, InitialData}|Tail]) ->
|
||||||
%% Because opening the stream involves sending a short header
|
%% Because opening the stream involves sending a short header
|
||||||
%% we necessarily write data. The InitialData variable allows
|
%% we necessarily write data. The InitialData variable allows
|
||||||
|
@ -887,17 +919,25 @@ wt_commands(State=#state{conn=Conn}, Session=#stream{id=SessionID},
|
||||||
case cowboy_quicer:StartF(Conn, [Header, InitialData]) of
|
case cowboy_quicer:StartF(Conn, [Header, InitialData]) of
|
||||||
{ok, StreamID} ->
|
{ok, StreamID} ->
|
||||||
%% @todo Pass Session directly?
|
%% @todo Pass Session directly?
|
||||||
webtransport_event(State, SessionID,
|
webtransport_event(State0, SessionID,
|
||||||
{opened_stream_id, OpenStreamRef, StreamID}),
|
{opened_stream_id, OpenStreamRef, StreamID}),
|
||||||
%% @todo Save the WT stream in cow_http3_machine AND here.
|
State = stream_new_local(State0, StreamID, StreamType,
|
||||||
|
{webtransport_stream, SessionID, StreamType}),
|
||||||
wt_commands(State, Session, Tail)
|
wt_commands(State, Session, Tail)
|
||||||
%% @todo Handle errors.
|
%% @todo Handle errors.
|
||||||
end;
|
end;
|
||||||
wt_commands(State, Session, [{close_stream, StreamID, Code}|Tail]) ->
|
wt_commands(State, Session, [{close_stream, StreamID, Code}|Tail]) ->
|
||||||
%% @todo Check that StreamID belongs to Session.
|
%% @todo Check that StreamID belongs to Session.
|
||||||
error({todo, State, Session, [{close_stream, StreamID, Code}|Tail]});
|
error({todo, State, Session, [{close_stream, StreamID, Code}|Tail]});
|
||||||
wt_commands(State, Session, [{send, datagram, Data}|Tail]) ->
|
wt_commands(State=#state{conn=Conn}, Session=#stream{id=SessionID},
|
||||||
error({todo, State, Session, [{send, datagram, Data}|Tail]});
|
[{send, datagram, Data}|Tail]) ->
|
||||||
|
QuarterID = SessionID div 4,
|
||||||
|
%% @todo Add a function to cowboy_quicer.
|
||||||
|
case quicer:send_dgram(Conn, [cow_http3:encode_int(QuarterID), Data]) of
|
||||||
|
{ok, _} ->
|
||||||
|
wt_commands(State, Session, Tail)
|
||||||
|
%% @todo Handle errors.
|
||||||
|
end;
|
||||||
wt_commands(State=#state{conn=Conn}, Session, [{send, StreamID, Data}|Tail]) ->
|
wt_commands(State=#state{conn=Conn}, Session, [{send, StreamID, Data}|Tail]) ->
|
||||||
%% @todo Check that StreamID belongs to Session.
|
%% @todo Check that StreamID belongs to Session.
|
||||||
case cowboy_quicer:send(Conn, StreamID, Data, nofin) of
|
case cowboy_quicer:send(Conn, StreamID, Data, nofin) of
|
||||||
|
@ -1058,15 +1098,25 @@ terminate_all_streams(State, [{StreamID, #stream{state=StreamState}}|Tail], Reas
|
||||||
stream_get(#state{streams=Streams}, StreamID) ->
|
stream_get(#state{streams=Streams}, StreamID) ->
|
||||||
maps:get(StreamID, Streams, error).
|
maps:get(StreamID, Streams, error).
|
||||||
|
|
||||||
stream_new_remote(State=#state{http3_machine=HTTP3Machine0, streams=Streams},
|
stream_new_local(State, StreamID, StreamType, Status) ->
|
||||||
StreamID, StreamType) ->
|
stream_new(State, StreamID, StreamType, unidi_local, Status).
|
||||||
|
|
||||||
|
stream_new_remote(State, StreamID, StreamType) ->
|
||||||
|
Status = case StreamType of
|
||||||
|
unidi -> header;
|
||||||
|
bidi -> normal
|
||||||
|
end,
|
||||||
|
stream_new(State, StreamID, StreamType, unidi_remote, Status).
|
||||||
|
|
||||||
|
stream_new(State=#state{http3_machine=HTTP3Machine0, streams=Streams},
|
||||||
|
StreamID, StreamType, UnidiType, Status) ->
|
||||||
{HTTP3Machine, Status} = case StreamType of
|
{HTTP3Machine, Status} = case StreamType of
|
||||||
unidi ->
|
unidi ->
|
||||||
{cow_http3_machine:init_unidi_stream(StreamID, unidi_remote, HTTP3Machine0),
|
{cow_http3_machine:init_unidi_stream(StreamID, UnidiType, HTTP3Machine0),
|
||||||
header};
|
Status};
|
||||||
bidi ->
|
bidi ->
|
||||||
{cow_http3_machine:init_bidi_stream(StreamID, HTTP3Machine0),
|
{cow_http3_machine:init_bidi_stream(StreamID, HTTP3Machine0),
|
||||||
normal}
|
Status}
|
||||||
end,
|
end,
|
||||||
Stream = #stream{id=StreamID, status=Status},
|
Stream = #stream{id=StreamID, status=Status},
|
||||||
State#state{http3_machine=HTTP3Machine, streams=Streams#{StreamID => Stream}}.
|
State#state{http3_machine=HTTP3Machine, streams=Streams#{StreamID => Stream}}.
|
||||||
|
|
|
@ -201,6 +201,9 @@ handle({quic, Data, StreamRef, #{flags := Flags}}) when is_binary(Data) ->
|
||||||
_ -> nofin
|
_ -> nofin
|
||||||
end,
|
end,
|
||||||
{data, StreamID, IsFin, Data};
|
{data, StreamID, IsFin, Data};
|
||||||
|
%% @todo Match on Conn.
|
||||||
|
handle({quic, Data, Conn, Flags}) when is_integer(Flags) ->
|
||||||
|
{datagram, Data};
|
||||||
%% QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED.
|
%% QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED.
|
||||||
handle({quic, new_stream, StreamRef, #{flags := Flags}}) ->
|
handle({quic, new_stream, StreamRef, #{flags := Flags}}) ->
|
||||||
case quicer:setopt(StreamRef, active, true) of
|
case quicer:setopt(StreamRef, active, true) of
|
||||||
|
|
|
@ -445,6 +445,7 @@ parse_header_fun(<<"sec-websocket-protocol">>) -> fun cow_http_hd:parse_sec_webs
|
||||||
parse_header_fun(<<"sec-websocket-version">>) -> fun cow_http_hd:parse_sec_websocket_version_req/1;
|
parse_header_fun(<<"sec-websocket-version">>) -> fun cow_http_hd:parse_sec_websocket_version_req/1;
|
||||||
parse_header_fun(<<"trailer">>) -> fun cow_http_hd:parse_trailer/1;
|
parse_header_fun(<<"trailer">>) -> fun cow_http_hd:parse_trailer/1;
|
||||||
parse_header_fun(<<"upgrade">>) -> fun cow_http_hd:parse_upgrade/1;
|
parse_header_fun(<<"upgrade">>) -> fun cow_http_hd:parse_upgrade/1;
|
||||||
|
parse_header_fun(<<"wt-available-protocols">>) -> fun cow_http_hd:parse_wt_available_protocols/1;
|
||||||
parse_header_fun(<<"x-forwarded-for">>) -> fun cow_http_hd:parse_x_forwarded_for/1.
|
parse_header_fun(<<"x-forwarded-for">>) -> fun cow_http_hd:parse_x_forwarded_for/1.
|
||||||
|
|
||||||
parse_header(Name, Req, Default, ParseFun) ->
|
parse_header(Name, Req, Default, ParseFun) ->
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
|
|
||||||
-export([upgrade/4]).
|
-export([upgrade/4]).
|
||||||
-export([upgrade/5]).
|
-export([upgrade/5]).
|
||||||
|
-export([terminate/3]).
|
||||||
|
|
||||||
-type opts() :: #{
|
-type opts() :: #{
|
||||||
%% @todo
|
%% @todo
|
||||||
|
@ -201,8 +202,19 @@ commands([Command={send, _, _, _}|Tail], State, Acc) ->
|
||||||
%% @todo set_options (to increase number of streams? data amounts? or a flow command?)
|
%% @todo set_options (to increase number of streams? data amounts? or a flow command?)
|
||||||
%% @todo shutdown_reason if useful.
|
%% @todo shutdown_reason if useful.
|
||||||
|
|
||||||
terminate(State, HandlerState, Error) ->
|
terminate(State, HandlerState, Reason) ->
|
||||||
error({todo, State, HandlerState, Error}).
|
%cowboy_stream:terminate(StreamID, Reason, StreamState)
|
||||||
|
%% @todo This terminate is at the connection level.
|
||||||
|
% handler_terminate(State, HandlerState, Reason),
|
||||||
|
% case Shutdown of
|
||||||
|
% normal -> exit(normal);
|
||||||
|
% _ -> exit({shutdown, Shutdown})
|
||||||
|
% end.
|
||||||
|
% exit(normal).
|
||||||
|
%handler_terminate(#state{handler=Handler, req=Req}, HandlerState, Reason) ->
|
||||||
|
% cowboy_handler:terminate(Reason, Req, HandlerState, Handler).
|
||||||
|
ok.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,10 @@
|
||||||
-import(ct_helper, [config/2]).
|
-import(ct_helper, [config/2]).
|
||||||
-import(ct_helper, [doc/1]).
|
-import(ct_helper, [doc/1]).
|
||||||
|
|
||||||
|
%% @todo -ifdef(COWBOY_QUICER).
|
||||||
|
|
||||||
|
-include_lib("quicer/include/quicer.hrl").
|
||||||
|
|
||||||
all() ->
|
all() ->
|
||||||
[{group, enabled}].
|
[{group, enabled}].
|
||||||
|
|
||||||
|
@ -58,29 +62,38 @@ init_routes(_) -> [
|
||||||
%run(_Config) ->
|
%run(_Config) ->
|
||||||
% timer:sleep(infinity).
|
% timer:sleep(infinity).
|
||||||
|
|
||||||
%% @todo Write tests!!
|
|
||||||
|
|
||||||
%% 3. Session Establishment
|
%% 3. Session Establishment
|
||||||
|
|
||||||
%% 3.1. Establishing a WebTransport-Capable HTTP/3 Connection
|
%% 3.1. Establishing a WebTransport-Capable HTTP/3 Connection
|
||||||
|
|
||||||
%% In order to indicate support for WebTransport, the server MUST send a SETTINGS_WEBTRANSPORT_MAX_SESSIONS value greater than "0" in its SETTINGS frame. (3.1)
|
%% In order to indicate support for WebTransport, the server MUST send a SETTINGS_WEBTRANSPORT_MAX_SESSIONS value greater than "0" in its SETTINGS frame. (3.1)
|
||||||
|
%% @todo reject_session_disabled
|
||||||
|
%% @todo accept_session_below
|
||||||
|
%% @todo accept_session_equal
|
||||||
|
%% @todo reject_session_above
|
||||||
|
|
||||||
%% The client MUST NOT send a WebTransport request until it has received the setting indicating WebTransport support from the server. (3.1)
|
%% The client MUST NOT send a WebTransport request until it has received the setting indicating WebTransport support from the server. (3.1)
|
||||||
|
|
||||||
%% For draft verisons of WebTransport only, the server MUST NOT process any incoming WebTransport requests until the client settings have been received, as the client may be using a version of the WebTransport extension that is different from the one used by the server. (3.1)
|
%% For draft verisons of WebTransport only, the server MUST NOT process any incoming WebTransport requests until the client settings have been received, as the client may be using a version of the WebTransport extension that is different from the one used by the server. (3.1)
|
||||||
|
|
||||||
%% Because WebTransport over HTTP/3 requires support for HTTP/3 datagrams and the Capsule Protocol, both the client and the server MUST indicate support for HTTP/3 datagrams by sending a SETTINGS_H3_DATAGRAM value set to 1 in their SETTINGS frame (see Section 2.1.1 of [HTTP-DATAGRAM]). (3.1)
|
%% Because WebTransport over HTTP/3 requires support for HTTP/3 datagrams and the Capsule Protocol, both the client and the server MUST indicate support for HTTP/3 datagrams by sending a SETTINGS_H3_DATAGRAM value set to 1 in their SETTINGS frame (see Section 2.1.1 of [HTTP-DATAGRAM]). (3.1)
|
||||||
|
%% @todo settings_h3_datagram_enabled
|
||||||
|
|
||||||
%% WebTransport over HTTP/3 also requires support for QUIC datagrams. To indicate support, both the client and the server MUST send a max_datagram_frame_size transport parameter with a value greater than 0 (see Section 3 of [QUIC-DATAGRAM]). (3.1)
|
%% WebTransport over HTTP/3 also requires support for QUIC datagrams. To indicate support, both the client and the server MUST send a max_datagram_frame_size transport parameter with a value greater than 0 (see Section 3 of [QUIC-DATAGRAM]). (3.1)
|
||||||
|
%% @todo quic_datagram_enabled (if size is too low the CONNECT stream can be used for capsules)
|
||||||
|
|
||||||
%% Any WebTransport requests sent by the client without enabling QUIC and HTTP datagrams MUST be treated as malformed by the server, as described in Section 4.1.2 of [HTTP3]. (3.1)
|
%% Any WebTransport requests sent by the client without enabling QUIC and HTTP datagrams MUST be treated as malformed by the server, as described in Section 4.1.2 of [HTTP3]. (3.1)
|
||||||
|
%% @todo reject_h3_datagram_disabled
|
||||||
|
%% @todo reject_quic_datagram_disabled
|
||||||
|
|
||||||
%% WebTransport over HTTP/3 relies on the RESET_STREAM_AT frame defined in [RESET-STREAM-AT]. To indicate support, both the client and the server MUST enable the extension as described in Section 3 of [RESET-STREAM-AT]. (3.1)
|
%% WebTransport over HTTP/3 relies on the RESET_STREAM_AT frame defined in [RESET-STREAM-AT]. To indicate support, both the client and the server MUST enable the extension as described in Section 3 of [RESET-STREAM-AT]. (3.1)
|
||||||
|
%% @todo reset_stream_at_enabled
|
||||||
|
|
||||||
%% 3.2. Extended CONNECT in HTTP/3
|
%% 3.2. Extended CONNECT in HTTP/3
|
||||||
|
|
||||||
%% [RFC8441] defines an extended CONNECT method in Section 4, enabled by the SETTINGS_ENABLE_CONNECT_PROTOCOL setting. That setting is defined for HTTP/3 by [RFC9220]. A server supporting WebTransport over HTTP/3 MUST send both the SETTINGS_WEBTRANSPORT_MAX_SESSIONS setting with a value greater than "0" and the SETTINGS_ENABLE_CONNECT_PROTOCOL setting with a value of "1". (3.2)
|
%% [RFC8441] defines an extended CONNECT method in Section 4, enabled by the SETTINGS_ENABLE_CONNECT_PROTOCOL setting. That setting is defined for HTTP/3 by [RFC9220]. A server supporting WebTransport over HTTP/3 MUST send both the SETTINGS_WEBTRANSPORT_MAX_SESSIONS setting with a value greater than "0" and the SETTINGS_ENABLE_CONNECT_PROTOCOL setting with a value of "1". (3.2)
|
||||||
|
%% @todo settings_enable_connect_protocol_enabled
|
||||||
|
%% @todo reject_settings_enable_connect_protocol_disabled
|
||||||
|
|
||||||
%% 3.3. Creating a New Session
|
%% 3.3. Creating a New Session
|
||||||
|
|
||||||
|
@ -92,7 +105,19 @@ init_routes(_) -> [
|
||||||
|
|
||||||
%% When the request contains the Origin header, the WebTransport server MUST verify the Origin header to ensure that the specified origin is allowed to access the server in question. If the verification fails, the WebTransport server SHOULD reply with status code 403 (Section 15.5.4 of [HTTP]). (3.3)
|
%% When the request contains the Origin header, the WebTransport server MUST verify the Origin header to ensure that the specified origin is allowed to access the server in question. If the verification fails, the WebTransport server SHOULD reply with status code 403 (Section 15.5.4 of [HTTP]). (3.3)
|
||||||
|
|
||||||
%% If all checks pass, the WebTransport server MAY accept the session by replying with a 2xx series status code, as defined in Section 15.3 of [HTTP]. (3.3)
|
accept_session_when_enabled(Config) ->
|
||||||
|
doc("Confirm that a WebTransport session can be established over HTTP/3. "
|
||||||
|
"(draft_webtrans_http3 3.3, RFC9220)"),
|
||||||
|
%% Connect to the WebTransport server.
|
||||||
|
#{
|
||||||
|
conn := Conn,
|
||||||
|
session_id := SessionID
|
||||||
|
} = do_webtransport_connect(Config),
|
||||||
|
%% Create a bidi stream, send Hello, get Hello back.
|
||||||
|
{ok, BidiStreamRef} = quicer:start_stream(Conn, #{}),
|
||||||
|
{ok, _} = quicer:send(BidiStreamRef, <<16#41, 0:2, SessionID:6, "Hello">>),
|
||||||
|
{ok, <<"Hello">>} = rfc9114_SUITE:do_receive_data(BidiStreamRef),
|
||||||
|
ok.
|
||||||
|
|
||||||
%% If the server accepts 0-RTT, the server MUST NOT reduce the limit of maximum open WebTransport sessions from the one negotiated during the previous session; such change would be deemed incompatible, and MUST result in a H3_SETTINGS_ERROR connection error. (3.3)
|
%% If the server accepts 0-RTT, the server MUST NOT reduce the limit of maximum open WebTransport sessions from the one negotiated during the previous session; such change would be deemed incompatible, and MUST result in a H3_SETTINGS_ERROR connection error. (3.3)
|
||||||
|
|
||||||
|
@ -102,6 +127,16 @@ init_routes(_) -> [
|
||||||
|
|
||||||
%% The user agent MAY include a WT-Available-Protocols header field in the CONNECT request. The WT-Available-Protocols enumerates the possible protocols in preference order. If the server receives such a header, it MAY include a WT-Protocol field in a successful (2xx) response. If it does, the server SHALL include a single choice from the client's list in that field. Servers MAY reject the request if the client did not include a suitable protocol. (3.4)
|
%% The user agent MAY include a WT-Available-Protocols header field in the CONNECT request. The WT-Available-Protocols enumerates the possible protocols in preference order. If the server receives such a header, it MAY include a WT-Protocol field in a successful (2xx) response. If it does, the server SHALL include a single choice from the client's list in that field. Servers MAY reject the request if the client did not include a suitable protocol. (3.4)
|
||||||
|
|
||||||
|
application_protocol_negotiation(Config) ->
|
||||||
|
doc("Applications can negotiate a protocol to use via WebTransport. "
|
||||||
|
"(draft_webtrans_http3 3.4)"),
|
||||||
|
%% Connect to the WebTransport server.
|
||||||
|
#{
|
||||||
|
resp_headers := RespHeaders
|
||||||
|
} = do_webtransport_connect(Config, [{<<"wt-available-protocols">>, <<"foo, bar">>}]),
|
||||||
|
{<<"wt-protocol">>, <<"foo">>} = lists:keyfind(<<"wt-protocol">>, 1, RespHeaders),
|
||||||
|
ok.
|
||||||
|
|
||||||
%% Both WT-Available-Protocols and WT-Protocol are Structured Fields [RFC8941]. WT-Available-Protocols is a List of Tokens, and WT-Protocol is a Token. The token in the WT-Protocol response header field MUST be one of the tokens listed in WT-Available-Protocols of the request. (3.4)
|
%% Both WT-Available-Protocols and WT-Protocol are Structured Fields [RFC8941]. WT-Available-Protocols is a List of Tokens, and WT-Protocol is a Token. The token in the WT-Protocol response header field MUST be one of the tokens listed in WT-Available-Protocols of the request. (3.4)
|
||||||
|
|
||||||
%% @todo 3.5 Prioritization
|
%% @todo 3.5 Prioritization
|
||||||
|
@ -111,14 +146,65 @@ init_routes(_) -> [
|
||||||
%% The client MAY optimistically open unidirectional and bidirectional streams, as well as send datagrams, for a session that it has sent the CONNECT request for, even if it has not yet received the server's response to the request. (4)
|
%% The client MAY optimistically open unidirectional and bidirectional streams, as well as send datagrams, for a session that it has sent the CONNECT request for, even if it has not yet received the server's response to the request. (4)
|
||||||
|
|
||||||
%% If at any point a session ID is received that cannot be a valid ID for a client-initiated bidirectional stream, the recipient MUST close the connection with an H3_ID_ERROR error code. (4)
|
%% If at any point a session ID is received that cannot be a valid ID for a client-initiated bidirectional stream, the recipient MUST close the connection with an H3_ID_ERROR error code. (4)
|
||||||
|
%% @todo Open bidi with Session ID 0, then do the CONNECT request.
|
||||||
|
|
||||||
%% 4.1. Unidirectional streams
|
%% 4.1. Unidirectional streams
|
||||||
|
|
||||||
%% WebTransport endpoints can initiate unidirectional streams. (4.1)
|
unidirectional_streams(Config) ->
|
||||||
|
doc("Both endpoints can open and use unidirectional streams. "
|
||||||
|
"(draft_webtrans_http3 4.1)"),
|
||||||
|
%% Connect to the WebTransport server.
|
||||||
|
#{
|
||||||
|
conn := Conn,
|
||||||
|
session_id := SessionID
|
||||||
|
} = do_webtransport_connect(Config),
|
||||||
|
%% Create a unidi stream, send Hello with a Fin flag.
|
||||||
|
{ok, LocalStreamRef} = quicer:start_stream(Conn,
|
||||||
|
#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
|
||||||
|
{ok, _} = quicer:send(LocalStreamRef,
|
||||||
|
<<16#54, 0:2, SessionID:6, "Hello">>,
|
||||||
|
?QUIC_SEND_FLAG_FIN),
|
||||||
|
%% Accept an identical unidi stream.
|
||||||
|
{unidi, RemoteStreamRef} = do_receive_new_stream(),
|
||||||
|
{nofin, <<16#54, 0:2, SessionID:6>>} = do_receive_data(RemoteStreamRef),
|
||||||
|
{fin, <<"Hello">>} = do_receive_data(RemoteStreamRef),
|
||||||
|
ok.
|
||||||
|
|
||||||
%% 4.2. Bidirectional Streams
|
%% 4.2. Bidirectional Streams
|
||||||
|
|
||||||
%% WebTransport endpoints can initiate bidirectional streams. (4.2)
|
bidirectional_streams_client(Config) ->
|
||||||
|
doc("The WT client can open and use bidirectional streams. "
|
||||||
|
"(draft_webtrans_http3 4.2)"),
|
||||||
|
%% Connect to the WebTransport server.
|
||||||
|
#{
|
||||||
|
conn := Conn,
|
||||||
|
session_id := SessionID
|
||||||
|
} = do_webtransport_connect(Config),
|
||||||
|
%% Create a bidi stream, send Hello, get Hello back.
|
||||||
|
{ok, LocalStreamRef} = quicer:start_stream(Conn, #{}),
|
||||||
|
{ok, _} = quicer:send(LocalStreamRef, <<16#41, 0:2, SessionID:6, "Hello">>),
|
||||||
|
%% @todo Use the local do_receive_data instead to have the fin flag.
|
||||||
|
{ok, <<"Hello">>} = rfc9114_SUITE:do_receive_data(LocalStreamRef),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
bidirectional_streams_server(Config) ->
|
||||||
|
doc("The WT server can open and use bidirectional streams. "
|
||||||
|
"(draft_webtrans_http3 4.2)"),
|
||||||
|
%% Connect to the WebTransport server.
|
||||||
|
#{
|
||||||
|
conn := Conn,
|
||||||
|
session_id := SessionID
|
||||||
|
} = do_webtransport_connect(Config),
|
||||||
|
%% Create a bidi stream, send a special instruction to make it create a bidi stream.
|
||||||
|
{ok, LocalStreamRef} = quicer:start_stream(Conn, #{}),
|
||||||
|
{ok, _} = quicer:send(LocalStreamRef, <<16#41, 0:2, SessionID:6, "TEST:open_bidi">>),
|
||||||
|
%% Accept the bidi stream and receive the data.
|
||||||
|
{bidi, RemoteStreamRef} = do_receive_new_stream(),
|
||||||
|
{nofin, <<16#41, 0:2, SessionID:6>>} = do_receive_data(RemoteStreamRef),
|
||||||
|
{ok, _} = quicer:send(RemoteStreamRef, <<"Hello">>,
|
||||||
|
?QUIC_SEND_FLAG_FIN),
|
||||||
|
{fin, <<"Hello">>} = do_receive_data(RemoteStreamRef),
|
||||||
|
ok.
|
||||||
|
|
||||||
%% Endpoints MUST NOT send WEBTRANSPORT_STREAM as a frame type on HTTP/3 streams other than the very first bytes of a request stream. Receiving this frame type in any other circumstances MUST be treated as a connection error of type H3_FRAME_ERROR. (4.2)
|
%% Endpoints MUST NOT send WEBTRANSPORT_STREAM as a frame type on HTTP/3 streams other than the very first bytes of a request stream. Receiving this frame type in any other circumstances MUST be treated as a connection error of type H3_FRAME_ERROR. (4.2)
|
||||||
|
|
||||||
|
@ -134,7 +220,21 @@ init_routes(_) -> [
|
||||||
|
|
||||||
%% 4.4. Datagrams
|
%% 4.4. Datagrams
|
||||||
|
|
||||||
%% Datagrams can be sent using HTTP Datagrams. The WebTransport datagram payload is sent unmodified in the "HTTP Datagram Payload" field of an HTTP Datagram (Section 2.1 of [HTTP-DATAGRAM]). Note that the payload field directly follows the Quarter Stream ID field, which is at the start of the QUIC DATAGRAM frame payload and refers to the CONNECT stream that established the WebTransport session. (4.4)
|
datagrams(Config) ->
|
||||||
|
doc("Both endpoints can send and receive datagrams. (draft_webtrans_http3 4.4)"),
|
||||||
|
%% Connect to the WebTransport server.
|
||||||
|
#{
|
||||||
|
conn := Conn,
|
||||||
|
session_id := SessionID
|
||||||
|
} = do_webtransport_connect(Config),
|
||||||
|
QuarterID = SessionID div 4,
|
||||||
|
%% Send a Hello datagram.
|
||||||
|
{ok, _} = quicer:send_dgram(Conn, <<0:2, QuarterID:6, "Hello">>),
|
||||||
|
%% Receive a Hello datagram back.
|
||||||
|
{datagram, SessionID, <<"Hello">>} = do_receive_datagram(Conn),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% @todo datagrams_via_capsule?
|
||||||
|
|
||||||
%% 4.5. Buffering Incoming Streams and Datagrams
|
%% 4.5. Buffering Incoming Streams and Datagrams
|
||||||
|
|
||||||
|
@ -248,3 +348,105 @@ init_routes(_) -> [
|
||||||
%% Cleanly terminating a CONNECT stream without a CLOSE_WEBTRANSPORT_SESSION capsule SHALL be semantically equivalent to terminating it with a CLOSE_WEBTRANSPORT_SESSION capsule that has an error code of 0 and an empty error string. (6)
|
%% Cleanly terminating a CONNECT stream without a CLOSE_WEBTRANSPORT_SESSION capsule SHALL be semantically equivalent to terminating it with a CLOSE_WEBTRANSPORT_SESSION capsule that has an error code of 0 and an empty error string. (6)
|
||||||
|
|
||||||
%% the endpoint SHOULD wait until all CONNECT streams have been closed by the peer before sending the CONNECTION_CLOSE (6)
|
%% the endpoint SHOULD wait until all CONNECT streams have been closed by the peer before sending the CONNECTION_CLOSE (6)
|
||||||
|
|
||||||
|
%% Helpers.
|
||||||
|
|
||||||
|
do_webtransport_connect(Config) ->
|
||||||
|
do_webtransport_connect(Config, []).
|
||||||
|
|
||||||
|
do_webtransport_connect(Config, ExtraHeaders) ->
|
||||||
|
%% Connect to server.
|
||||||
|
#{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config, #{
|
||||||
|
peer_unidi_stream_count => 100,
|
||||||
|
datagram_send_enabled => 1,
|
||||||
|
datagram_receive_enabled => 1
|
||||||
|
}),
|
||||||
|
%% Confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||||
|
#{enable_connect_protocol := true} = Settings,
|
||||||
|
%% Confirm that SETTINGS_WEBTRANSPORT_MAX_SESSIONS >= 1.
|
||||||
|
#{webtransport_max_sessions := WTMaxSessions} = Settings,
|
||||||
|
true = WTMaxSessions >= 1,
|
||||||
|
%% Confirm that SETTINGS_H3_DATAGRAM = 1.
|
||||||
|
#{h3_datagram := true} = Settings,
|
||||||
|
%% Confirm that QUIC's max_datagram_size > 0.
|
||||||
|
receive {quic, dgram_state_changed, Conn, DatagramState} ->
|
||||||
|
#{
|
||||||
|
dgram_max_len := DatagramMaxLen,
|
||||||
|
dgram_send_enabled := DatagramSendEnabled
|
||||||
|
} = DatagramState,
|
||||||
|
true = DatagramMaxLen > 0,
|
||||||
|
true = DatagramSendEnabled,
|
||||||
|
ok
|
||||||
|
after 5000 ->
|
||||||
|
error({timeout, waiting_for_datagram_state_change})
|
||||||
|
end,
|
||||||
|
%% Send a CONNECT :protocol request to upgrade the stream to Websocket.
|
||||||
|
{ok, ConnectStreamRef} = quicer:start_stream(Conn, #{}),
|
||||||
|
{ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([
|
||||||
|
{<<":method">>, <<"CONNECT">>},
|
||||||
|
{<<":protocol">>, <<"webtransport">>},
|
||||||
|
{<<":scheme">>, <<"https">>},
|
||||||
|
{<<":path">>, <<"/wt">>},
|
||||||
|
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
|
||||||
|
{<<"origin">>, <<"https://localhost">>}
|
||||||
|
|ExtraHeaders], 0, cow_qpack:init(encoder)),
|
||||||
|
{ok, _} = quicer:send(ConnectStreamRef, [
|
||||||
|
<<1>>, %% HEADERS frame.
|
||||||
|
cow_http3:encode_int(iolist_size(EncodedRequest)),
|
||||||
|
EncodedRequest
|
||||||
|
]),
|
||||||
|
%% Receive a 200 response.
|
||||||
|
{ok, Data} = rfc9114_SUITE:do_receive_data(ConnectStreamRef),
|
||||||
|
{HLenEnc, HLenBits} = rfc9114_SUITE:do_guess_int_encoding(Data),
|
||||||
|
<<
|
||||||
|
1, %% HEADERS frame.
|
||||||
|
HLenEnc:2, HLen:HLenBits,
|
||||||
|
EncodedResponse:HLen/bytes
|
||||||
|
>> = Data,
|
||||||
|
{ok, DecodedResponse, _DecData, _DecSt}
|
||||||
|
= cow_qpack:decode_field_section(EncodedResponse, 0, cow_qpack:init(decoder)),
|
||||||
|
#{<<":status">> := <<"200">>} = maps:from_list(DecodedResponse),
|
||||||
|
%% Retrieve the Session ID.
|
||||||
|
{ok, SessionID} = quicer:get_stream_id(ConnectStreamRef),
|
||||||
|
%% Accept QPACK streams to avoid conflicts with unidi streams from tests.
|
||||||
|
Unidi1 = rfc9114_SUITE:do_accept_qpack_stream(Conn),
|
||||||
|
Unidi2 = rfc9114_SUITE:do_accept_qpack_stream(Conn),
|
||||||
|
%% Done.
|
||||||
|
#{
|
||||||
|
conn => Conn,
|
||||||
|
session_id => SessionID,
|
||||||
|
resp_headers => DecodedResponse,
|
||||||
|
enc_or_dec1 => Unidi1,
|
||||||
|
enc_or_dec2 => Unidi2
|
||||||
|
}.
|
||||||
|
|
||||||
|
do_receive_new_stream() ->
|
||||||
|
receive
|
||||||
|
{quic, new_stream, StreamRef, #{flags := Flags}} ->
|
||||||
|
ok = quicer:setopt(StreamRef, active, true),
|
||||||
|
case quicer:is_unidirectional(Flags) of
|
||||||
|
true -> {unidi, StreamRef};
|
||||||
|
false -> {bidi, StreamRef}
|
||||||
|
end
|
||||||
|
after 5000 ->
|
||||||
|
error({timeout, waiting_for_stream})
|
||||||
|
end.
|
||||||
|
|
||||||
|
do_receive_data(StreamRef) ->
|
||||||
|
receive {quic, Data, StreamRef, #{flags := Flags}} ->
|
||||||
|
IsFin = case Flags band ?QUIC_RECEIVE_FLAG_FIN of
|
||||||
|
?QUIC_RECEIVE_FLAG_FIN -> fin;
|
||||||
|
_ -> nofin
|
||||||
|
end,
|
||||||
|
{IsFin, Data}
|
||||||
|
after 5000 ->
|
||||||
|
error({timeout, waiting_for_data})
|
||||||
|
end.
|
||||||
|
|
||||||
|
do_receive_datagram(Conn) ->
|
||||||
|
receive {quic, <<0:2, QuarterID:6, Data/bits>>, Conn, Flags} when is_integer(Flags) ->
|
||||||
|
{datagram, QuarterID * 4, Data}
|
||||||
|
after 5000 ->
|
||||||
|
ct:pal("~p", [process_info(self(), messages)]),
|
||||||
|
error({timeout, waiting_for_datagram})
|
||||||
|
end.
|
||||||
|
|
|
@ -8,7 +8,13 @@
|
||||||
-export([webtransport_handle/2]).
|
-export([webtransport_handle/2]).
|
||||||
-export([webtransport_info/2]).
|
-export([webtransport_info/2]).
|
||||||
|
|
||||||
init(Req, _) ->
|
init(Req0, _) ->
|
||||||
|
Req = case cowboy_req:parse_header(<<"wt-available-protocols">>, Req0) of
|
||||||
|
undefined ->
|
||||||
|
Req0;
|
||||||
|
[Protocol|_] ->
|
||||||
|
cowboy_req:set_resp_header(<<"wt-protocol">>, Protocol, Req0)
|
||||||
|
end,
|
||||||
{cowboy_webtransport, Req, #{}}.
|
{cowboy_webtransport, Req, #{}}.
|
||||||
|
|
||||||
%% @todo WT handle {stream_open,4,bidi}
|
%% @todo WT handle {stream_open,4,bidi}
|
||||||
|
@ -24,12 +30,26 @@ webtransport_handle(Event = {stream_open, StreamID, unidi}, Streams) ->
|
||||||
OpenStreamRef => {unidi_local, StreamID}}};
|
OpenStreamRef => {unidi_local, StreamID}}};
|
||||||
webtransport_handle(Event = {opened_stream_id, OpenStreamRef, OpenStreamID}, Streams) ->
|
webtransport_handle(Event = {opened_stream_id, OpenStreamRef, OpenStreamID}, Streams) ->
|
||||||
ct:pal("WT handle ~p~n", [Event]),
|
ct:pal("WT handle ~p~n", [Event]),
|
||||||
#{OpenStreamRef := {unidi_local, RemoteStreamID}} = Streams,
|
case Streams of
|
||||||
|
#{OpenStreamRef := bidi} ->
|
||||||
|
{[], maps:remove(OpenStreamRef, Streams#{
|
||||||
|
OpenStreamID => bidi
|
||||||
|
})};
|
||||||
|
#{OpenStreamRef := {unidi_local, RemoteStreamID}} ->
|
||||||
#{RemoteStreamID := {unidi_remote, OpenStreamRef}} = Streams,
|
#{RemoteStreamID := {unidi_remote, OpenStreamRef}} = Streams,
|
||||||
{[], maps:remove(OpenStreamRef, Streams#{
|
{[], maps:remove(OpenStreamRef, Streams#{
|
||||||
RemoteStreamID => {unidi_remote, OpenStreamID},
|
RemoteStreamID => {unidi_remote, OpenStreamID},
|
||||||
OpenStreamID => {unidi_local, RemoteStreamID}
|
OpenStreamID => {unidi_local, RemoteStreamID}
|
||||||
})};
|
})}
|
||||||
|
end;
|
||||||
|
webtransport_handle(Event = {stream_data, StreamID, IsFin, <<"TEST:", Test/bits>>}, Streams) ->
|
||||||
|
ct:pal("WT handle ~p~n", [Event]),
|
||||||
|
case Test of
|
||||||
|
<<"open_bidi">> ->
|
||||||
|
OpenStreamRef = make_ref(),
|
||||||
|
{[{open_stream, OpenStreamRef, bidi, <<>>}],
|
||||||
|
Streams#{OpenStreamRef => bidi}}
|
||||||
|
end;
|
||||||
webtransport_handle(Event = {stream_data, StreamID, IsFin, Data}, Streams) ->
|
webtransport_handle(Event = {stream_data, StreamID, IsFin, Data}, Streams) ->
|
||||||
ct:pal("WT handle ~p~n", [Event]),
|
ct:pal("WT handle ~p~n", [Event]),
|
||||||
case Streams of
|
case Streams of
|
||||||
|
@ -42,8 +62,11 @@ webtransport_handle(Event = {stream_data, StreamID, IsFin, Data}, Streams) ->
|
||||||
#{StreamID := {unidi_remote, LocalStreamID}} ->
|
#{StreamID := {unidi_remote, LocalStreamID}} ->
|
||||||
{[{send, LocalStreamID, IsFin, Data}], Streams}
|
{[{send, LocalStreamID, IsFin, Data}], Streams}
|
||||||
end;
|
end;
|
||||||
webtransport_handle(Event, Streams) ->
|
webtransport_handle(Event = {datagram, Data}, Streams) ->
|
||||||
ct:pal("WT handle ~p~n", [Event]),
|
ct:pal("WT handle ~p~n", [Event]),
|
||||||
|
{[{send, datagram, Data}], Streams};
|
||||||
|
webtransport_handle(Event, Streams) ->
|
||||||
|
ct:pal("WT handle ignore ~p~n", [Event]),
|
||||||
{[], Streams}.
|
{[], Streams}.
|
||||||
|
|
||||||
webtransport_info({try_again, Event}, Streams) ->
|
webtransport_info({try_again, Event}, Streams) ->
|
||||||
|
|
|
@ -426,7 +426,7 @@ reject_upgrade_header(Config) ->
|
||||||
% Examples.
|
% Examples.
|
||||||
|
|
||||||
accept_handshake_when_enabled(Config) ->
|
accept_handshake_when_enabled(Config) ->
|
||||||
doc("Confirm the example for Websocket over HTTP/2 works. (RFC9220, RFC8441 5.1)"),
|
doc("Confirm the example for Websocket over HTTP/3 works. (RFC9220, RFC8441 5.1)"),
|
||||||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||||
#{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config),
|
#{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config),
|
||||||
#{enable_connect_protocol := true} = Settings,
|
#{enable_connect_protocol := true} = Settings,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue