diff options
Diffstat (limited to 'test')
45 files changed, 1690 insertions, 134 deletions
diff --git a/test/compress_SUITE.erl b/test/compress_SUITE.erl index a6a100c..9da9769 100644 --- a/test/compress_SUITE.erl +++ b/test/compress_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]> +%% 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 diff --git a/test/cowboy_ct_hook.erl b/test/cowboy_ct_hook.erl index e76ec21..46e56a2 100644 --- a/test/cowboy_ct_hook.erl +++ b/test/cowboy_ct_hook.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2014-2024, Loïc Hoguin <[email protected]> +%% 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 diff --git a/test/cowboy_test.erl b/test/cowboy_test.erl index 670da18..541e8f9 100644 --- a/test/cowboy_test.erl +++ b/test/cowboy_test.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2014-2024, Loïc Hoguin <[email protected]> +%% 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 @@ -120,50 +120,53 @@ common_groups(Tests, Parallel) -> Groups end. -init_common_groups(Name = http, Config, Mod) -> - init_http(Name, #{ +init_common_groups(Name, Config, Mod) -> + init_common_groups(Name, Config, Mod, #{}). + +init_common_groups(Name = http, Config, Mod, ProtoOpts) -> + init_http(Name, ProtoOpts#{ env => #{dispatch => Mod:init_dispatch(Config)} }, [{flavor, vanilla}|Config]); -init_common_groups(Name = https, Config, Mod) -> - init_https(Name, #{ +init_common_groups(Name = https, Config, Mod, ProtoOpts) -> + init_https(Name, ProtoOpts#{ env => #{dispatch => Mod:init_dispatch(Config)} }, [{flavor, vanilla}|Config]); -init_common_groups(Name = h2, Config, Mod) -> - init_http2(Name, #{ +init_common_groups(Name = h2, Config, Mod, ProtoOpts) -> + init_http2(Name, ProtoOpts#{ env => #{dispatch => Mod:init_dispatch(Config)} }, [{flavor, vanilla}|Config]); -init_common_groups(Name = h2c, Config, Mod) -> - Config1 = init_http(Name, #{ +init_common_groups(Name = h2c, Config, Mod, ProtoOpts) -> + Config1 = init_http(Name, ProtoOpts#{ env => #{dispatch => Mod:init_dispatch(Config)} }, [{flavor, vanilla}|Config]), lists:keyreplace(protocol, 1, Config1, {protocol, http2}); -init_common_groups(Name = h3, Config, Mod) -> - init_http3(Name, #{ +init_common_groups(Name = h3, Config, Mod, ProtoOpts) -> + init_http3(Name, ProtoOpts#{ env => #{dispatch => Mod:init_dispatch(Config)} }, [{flavor, vanilla}|Config]); -init_common_groups(Name = http_compress, Config, Mod) -> - init_http(Name, #{ +init_common_groups(Name = http_compress, Config, Mod, ProtoOpts) -> + init_http(Name, ProtoOpts#{ env => #{dispatch => Mod:init_dispatch(Config)}, stream_handlers => [cowboy_compress_h, cowboy_stream_h] }, [{flavor, compress}|Config]); -init_common_groups(Name = https_compress, Config, Mod) -> - init_https(Name, #{ +init_common_groups(Name = https_compress, Config, Mod, ProtoOpts) -> + init_https(Name, ProtoOpts#{ env => #{dispatch => Mod:init_dispatch(Config)}, stream_handlers => [cowboy_compress_h, cowboy_stream_h] }, [{flavor, compress}|Config]); -init_common_groups(Name = h2_compress, Config, Mod) -> - init_http2(Name, #{ +init_common_groups(Name = h2_compress, Config, Mod, ProtoOpts) -> + init_http2(Name, ProtoOpts#{ env => #{dispatch => Mod:init_dispatch(Config)}, stream_handlers => [cowboy_compress_h, cowboy_stream_h] }, [{flavor, compress}|Config]); -init_common_groups(Name = h2c_compress, Config, Mod) -> - Config1 = init_http(Name, #{ +init_common_groups(Name = h2c_compress, Config, Mod, ProtoOpts) -> + Config1 = init_http(Name, ProtoOpts#{ env => #{dispatch => Mod:init_dispatch(Config)}, stream_handlers => [cowboy_compress_h, cowboy_stream_h] }, [{flavor, compress}|Config]), lists:keyreplace(protocol, 1, Config1, {protocol, http2}); -init_common_groups(Name = h3_compress, Config, Mod) -> - init_http3(Name, #{ +init_common_groups(Name = h3_compress, Config, Mod, ProtoOpts) -> + init_http3(Name, ProtoOpts#{ env => #{dispatch => Mod:init_dispatch(Config)}, stream_handlers => [cowboy_compress_h, cowboy_stream_h] }, [{flavor, compress}|Config]). diff --git a/test/decompress_SUITE.erl b/test/decompress_SUITE.erl index f61bb5d..f1eb13a 100644 --- a/test/decompress_SUITE.erl +++ b/test/decompress_SUITE.erl @@ -1,5 +1,5 @@ -%% Copyright (c) 2024, jdamanalo <[email protected]> -%% Copyright (c) 2024, Loïc Hoguin <[email protected]> +%% Copyright (c) jdamanalo <[email protected]> +%% 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 diff --git a/test/draft_h3_webtransport_SUITE.erl b/test/draft_h3_webtransport_SUITE.erl new file mode 100644 index 0000000..05a6c17 --- /dev/null +++ b/test/draft_h3_webtransport_SUITE.erl @@ -0,0 +1,814 @@ +%% 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(draft_h3_webtransport_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [config/2]). +-import(ct_helper, [doc/1]). +-import(rfc9114_SUITE, [do_wait_stream_aborted/1]). + +-ifdef(COWBOY_QUICER). + +-include_lib("quicer/include/quicer.hrl"). + +all() -> + [{group, enabled}]. + +groups() -> + Tests = ct_helper:all(?MODULE), + [{enabled, [], Tests}]. %% @todo Enable parallel when all is better. + +init_per_group(Name = enabled, Config) -> + cowboy_test:init_http3(Name, #{ + enable_connect_protocol => true, + h3_datagram => true, + enable_webtransport => true, %% For compatibility with draft-02. + wt_max_sessions => 10, + env => #{dispatch => cowboy_router:compile(init_routes(Config))} + }, Config). + +end_per_group(Name, _) -> + cowboy_test:stop_group(Name). + +init_routes(_) -> [ + {"localhost", [ + {"/wt", wt_echo_h, []} + ]} +]. + +%% Temporary. + +%% To start Chromium the command line is roughly: +%% chromium --ignore-certificate-errors-spki-list=LeLykt63i2FRAm+XO91yBoSjKfrXnAFygqe5xt0zgDA= --ignore-certificate-errors --user-data-dir=/tmp/chromium-wt --allow-insecure-localhost --webtransport-developer-mode --enable-quic https://googlechrome.github.io/samples/webtransport/client.html +%% +%% To find the SPKI the command is roughly: +%% openssl x509 -in ~/ninenines/cowboy/test/rfc9114_SUITE_data/server.pem -pubkey -noout | \ +%% openssl pkey -pubin -outform der | \ +%% openssl dgst -sha256 -binary | \ +%% openssl enc -base64 + +%run(Config) -> +% ct:pal("port ~p", [config(port, Config)]), +% timer:sleep(infinity). + +%% 3. Session Establishment + +%% 3.1. Establishing a WebTransport-Capable HTTP/3 Connection + +%% In order to indicate support for WebTransport, the server MUST send a SETTINGS_WT_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) + +%% 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) +%% @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) +%% @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) +%% @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) +%% @todo reset_stream_at_enabled + +%% 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_WT_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 + +%% As WebTransport sessions are established over HTTP/3, they are identified using the https URI scheme ([HTTP], Section 4.2.2). (3.3) + +%% In order to create a new WebTransport session, a client can send an HTTP CONNECT request. The :protocol pseudo-header field ([RFC8441]) MUST be set to webtransport. The :scheme field MUST be https. Both the :authority and the :path value MUST be set; those fields indicate the desired WebTransport server. If the WebTransport session is coming from a browser client, an Origin header [RFC6454] MUST be provided within the request; otherwise, the header is OPTIONAL. (3.3) + +%% If it does not (have a WT server), it SHOULD reply with status code 404 (Section 15.5.5 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) + +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, <<1:2, 16#41:14, 0:2, SessionID:6, "Hello">>), + {nofin, <<"Hello">>} = 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) + +%% The capsule-protocol header field Section 3.4 of [HTTP-DATAGRAM] is not required by WebTransport and can safely be ignored by WebTransport endpoints. (3.3) + +%% 3.4. Application Protocol Negotiation + +application_protocol_negotiation(Config) -> + doc("Applications can negotiate a protocol to use via WebTransport. " + "(draft_webtrans_http3 3.4)"), + %% Connect to the WebTransport server. + WTAvailableProtocols = cow_http_hd:wt_available_protocols([<<"foo">>, <<"bar">>]), + #{ + resp_headers := RespHeaders + } = do_webtransport_connect(Config, [{<<"wt-available-protocols">>, WTAvailableProtocols}]), + {<<"wt-protocol">>, WTProtocol} = lists:keyfind(<<"wt-protocol">>, 1, RespHeaders), + <<"foo">> = iolist_to_binary(cow_http_hd:parse_wt_protocol(WTProtocol)), + 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) + +%% @todo 3.5 Prioritization + +%% 4. WebTransport Features + +%% 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) +%% @todo Open bidi with Session ID 0, then do the CONNECT request. + +%% 4.1. Unidirectional streams + +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, + <<1:2, 16#54:14, 0:2, SessionID:6, "Hello">>, + ?QUIC_SEND_FLAG_FIN), + %% Accept an identical unidi stream. + {unidi, RemoteStreamRef} = do_receive_new_stream(), + {nofin, <<1:2, 16#54:14, 0:2, SessionID:6>>} = do_receive_data(RemoteStreamRef), + {fin, <<"Hello">>} = do_receive_data(RemoteStreamRef), + ok. + +%% 4.2. Bidirectional Streams + +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, <<1:2, 16#41:14, 0:2, SessionID:6, "Hello">>), + {nofin, <<"Hello">>} = 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 the server create another bidi stream. + {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}), + {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:open_bidi">>), + %% Accept the bidi stream and receive the data. + {bidi, RemoteStreamRef} = do_receive_new_stream(), + {nofin, <<1:2, 16#41:14, 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 WT_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) + +%% 4.3. Resetting Data Streams + +%% A WebTransport endpoint may send a RESET_STREAM or a STOP_SENDING frame for a WebTransport data stream. Those signals are propagated by the WebTransport implementation to the application. (4.3) + +%% A WebTransport application SHALL provide an error code for those operations. (4.3) + +%% WebTransport implementations MUST use the RESET_STREAM_AT frame [RESET-STREAM-AT] with a Reliable Size set to at least the size of the WebTransport header when resetting a WebTransport data stream. This ensures that the ID field associating the data stream with a WebTransport session is always delivered. (4.3) + +%% WebTransport implementations SHALL forward the error code for a stream associated with a known session to the application that owns that session (4.3) + +%% 4.4. Datagrams + +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 + +%% To handle this case (out of order stream_open/CONNECT), WebTransport endpoints SHOULD buffer streams and datagrams until those can be associated with an established session. (4.5) + +%% To avoid resource exhaustion, the endpoints MUST limit the number of buffered streams and datagrams. When the number of buffered streams is exceeded, a stream SHALL be closed by sending a RESET_STREAM and/or STOP_SENDING with the WT_BUFFERED_STREAM_REJECTED error code. When the number of buffered datagrams is exceeded, a datagram SHALL be dropped. It is up to an implementation to choose what stream or datagram to discard. (4.5) + +%% 4.6. Interaction with HTTP/3 GOAWAY frame + +%% A client receiving GOAWAY cannot initiate CONNECT requests for new WebTransport sessions on that HTTP/3 connection; it must open a new HTTP/3 connection to initiate new WebTransport sessions with the same peer. (4.6) + +%% An HTTP/3 GOAWAY frame is also a signal to applications to initiate shutdown for all WebTransport sessions. (4.6) + +%% @todo Currently receipt of a GOAWAY frame immediately ends the connection. +%% We want to allow WT sessions to gracefully shut down before that. +%goaway_client(Config) -> +% doc("The HTTP/3 client can initiate the close of all WT sessions " +% "by sending a GOAWAY frame. (draft_webtrans_http3 4.6)"), +% %% Connect to the WebTransport server. +% #{ +% conn := Conn, +% connect_stream_ref := ConnectStreamRef, +% session_id := SessionID +% } = do_webtransport_connect(Config), +% %% Open a control stream and send a GOAWAY frame. +% {ok, ControlRef} = quicer:start_stream(Conn, +% #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), +% {ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}), +% {ok, _} = quicer:send(ControlRef, [ +% <<0>>, %% CONTROL stream. +% SettingsBin, +% <<7>>, %% GOAWAY frame. +% cow_http3:encode_int(1), +% cow_http3:encode_int(0) +% ]), +% %% Receive a datagram indicating processing by the WT handler. +% {datagram, SessionID, <<"TEST:close_initiated">>} = do_receive_datagram(Conn), +% ok. + +wt_drain_session_client(Config) -> + doc("The WT client can initiate the close of a single session. " + "(draft_webtrans_http3 4.6)"), + %% Connect to the WebTransport server. + #{ + conn := Conn, + connect_stream_ref := ConnectStreamRef, + session_id := SessionID + } = do_webtransport_connect(Config), + %% Send the WT_DRAIN_SESSION capsule on the CONNECT stream. + {ok, _} = quicer:send(ConnectStreamRef, cow_capsule:wt_drain_session()), + %% Receive a datagram indicating processing by the WT handler. + {datagram, SessionID, <<"TEST:close_initiated">>} = do_receive_datagram(Conn), + ok. + +wt_drain_session_server(Config) -> + doc("The WT server can initiate the close of a single session. " + "(draft_webtrans_http3 4.6)"), + %% Connect to the WebTransport server. + #{ + conn := Conn, + connect_stream_ref := ConnectStreamRef, + session_id := SessionID + } = do_webtransport_connect(Config), + %% Create a bidi stream, send a special instruction to make it initiate the close. + {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}), + {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:initiate_close">>), + %% Receive the WT_DRAIN_SESSION capsule on the CONNECT stream. + DrainWTSessionCapsule = cow_capsule:wt_drain_session(), + {nofin, DrainWTSessionCapsule} = do_receive_data(ConnectStreamRef), + ok. + +wt_drain_session_continue_client(Config) -> + doc("After the WT client has initiated the close of the session, " + "both client and server can continue using the session and " + "open new streams. (draft_webtrans_http3 4.6)"), + %% Connect to the WebTransport server. + #{ + conn := Conn, + connect_stream_ref := ConnectStreamRef, + session_id := SessionID + } = do_webtransport_connect(Config), + %% Send the WT_DRAIN_SESSION capsule on the CONNECT stream. + {ok, _} = quicer:send(ConnectStreamRef, cow_capsule:wt_drain_session()), + %% Receive a datagram indicating processing by the WT handler. + {datagram, SessionID, <<"TEST:close_initiated">>} = do_receive_datagram(Conn), + %% Create a new bidi stream, send Hello, get Hello back. + {ok, ContinueStreamRef} = quicer:start_stream(Conn, #{}), + {ok, _} = quicer:send(ContinueStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "Hello">>), + {nofin, <<"Hello">>} = do_receive_data(ContinueStreamRef), + ok. + +wt_drain_session_continue_server(Config) -> + doc("After the WT server has initiated the close of the session, " + "both client and server can continue using the session and " + "open new streams. (draft_webtrans_http3 4.6)"), + %% Connect to the WebTransport server. + #{ + conn := Conn, + connect_stream_ref := ConnectStreamRef, + session_id := SessionID + } = do_webtransport_connect(Config), + %% Create a bidi stream, send a special instruction to make it initiate the close. + {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}), + {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:initiate_close">>), + %% Receive the WT_DRAIN_SESSION capsule on the CONNECT stream. + DrainWTSessionCapsule = cow_capsule:wt_drain_session(), + {nofin, DrainWTSessionCapsule} = do_receive_data(ConnectStreamRef), + %% Create a new bidi stream, send Hello, get Hello back. + {ok, ContinueStreamRef} = quicer:start_stream(Conn, #{}), + {ok, _} = quicer:send(ContinueStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "Hello">>), + {nofin, <<"Hello">>} = do_receive_data(ContinueStreamRef), + ok. + +%% @todo 4.7. Use of Keying Material Exporters + +%% 5. Flow Control + +%% 5.1. Limiting the Number of Simultaneous Sessions + +%% This document defines a SETTINGS_WT_MAX_SESSIONS parameter that allows the server to limit the maximum number of concurrent WebTransport sessions on a single HTTP/3 connection. The client MUST NOT open more simultaneous sessions than indicated in the server SETTINGS parameter. The server MUST NOT close the connection if the client opens sessions exceeding this limit, as the client and the server do not have a consistent view of how many sessions are open due to the asynchronous nature of the protocol; instead, it MUST reset all of the CONNECT streams it is not willing to process with the H3_REQUEST_REJECTED status defined in [HTTP3]. (5.1) + +%% 5.2. Limiting the Number of Streams Within a Session + +%% The WT_MAX_STREAMS capsule (Section 5.6.1) establishes a limit on the number of streams within a WebTransport session. (5.2) + +%% Note that the CONNECT stream for the session is not included in either the bidirectional or the unidirectional stream limits (5.2) + +%% The session-level stream limit applies in addition to the QUIC MAX_STREAMS frame, which provides a connection-level stream limit. New streams can only be created within the session if both the stream- and the connection-level limit permit (5.2) + +%% The WT_STREAMS_BLOCKED capsule (Section 5.7) can be sent to indicate that an endpoint was unable to create a stream due to the session-level stream limit. (5.2) + +%% Note that enforcing this limit requires reliable resets for stream headers so that both endpoints can agree on the number of streams that are open. (5.2) + +%% 5.3. Data Limits + +%% The WT_MAX_DATA capsule (Section 5.8) establishes a limit on the amount of data that can be sent within a WebTransport session. This limit counts all data that is sent on streams of the corresponding type, excluding the stream header (see Section 4.1 and Section 4.2). (5.3) + +%% Implementing WT_MAX_DATA requires that the QUIC stack provide the WebTransport implementation with information about the final size of streams; see { {Section 4.5 of !RFC9000}}. This allows both endpoints to agree on how much data was consumed by that stream, although the stream header exclusion above applies. (5.3) + +%% The WT_DATA_BLOCKED capsule (Section 5.9) can be sent to indicate that an endpoint was unable to send data due to a limit set by the WT_MAX_DATA capsule. (5.3) + +%% The WT_MAX_STREAM_DATA and WT_STREAM_DATA_BLOCKED capsules (Part XX of [I-D.ietf-webtrans-http2]) are not used and so are prohibited. Endpoints MUST treat receipt of a WT_MAX_STREAM_DATA or a WT_STREAM_DATA_BLOCKED capsule as a session error. (5.3) + +%% 5.4. Flow Control and Intermediaries + +%% In practice, an intermediary that translates flow control signals between similar WebTransport protocols, such as between two HTTP/3 connections, can often simply reexpress the same limits received on one connection directly on the other connection. (5.4) + +%% 5.5. Flow Control SETTINGS + +%% WT_MAX_STREAMS via SETTINGS_WT_INITIAL_MAX_STREAMS_UNI and SETTINGS_WT_INITIAL_MAX_STREAMS_BIDI (5.5) + +%% WT_MAX_DATA via SETTINGS_WT_INITIAL_MAX_DATA (5.5) + +%% 5.6. Flow Control Capsules + +%% 5.6.1. WT_MAX_STREAMS Capsule + +%% An HTTP capsule [HTTP-DATAGRAM] called WT_MAX_STREAMS is introduced to inform the peer of the cumulative number of streams of a given type it is permitted to open. A WT_MAX_STREAMS capsule with a type of 0x190B4D3F applies to bidirectional streams, and a WT_MAX_STREAMS capsule with a type of 0x190B4D40 applies to unidirectional streams. (5.6.1) + +%% Note that, because Maximum Streams is a cumulative value representing the total allowed number of streams, including previously closed streams, endpoints repeatedly send new WT_MAX_STREAMS capsules with increasing Maximum Streams values as streams are opened. (5.6.1) + +%% Maximum Streams: A count of the cumulative number of streams of the corresponding type that can be opened over the lifetime of the session. This value cannot exceed 260, as it is not possible to encode stream IDs larger than 262-1. (5.6.1) + +%% An endpoint MUST NOT open more streams than permitted by the current stream limit set by its peer. (5.6.1) + +%% Note that this limit includes streams that have been closed as well as those that are open. (5.6.1) + +%% Initial values for these limits MAY be communicated by sending non-zero values for SETTINGS_WT_INITIAL_MAX_STREAMS_UNI and SETTINGS_WT_INITIAL_MAX_STREAMS_BIDI. (5.6.1) + +%% 5.7. WT_STREAMS_BLOCKED Capsule + +%% A sender SHOULD send a WT_STREAMS_BLOCKED capsule (type=0x190B4D43 for bidi or 0x190B4D44 for unidi) when it wishes to open a stream but is unable to do so due to the maximum stream limit set by its peer. (5.7) + +%% 5.8. WT_MAX_DATA Capsule + +%% An HTTP capsule [HTTP-DATAGRAM] called WT_MAX_DATA (type=0x190B4D3D) is introduced to inform the peer of the maximum amount of data that can be sent on the WebTransport session as a whole. (5.8) + +%% This limit counts all data that is sent on streams of the corresponding type, excluding the stream header (see Section 4.1 and Section 4.2). Implementing WT_MAX_DATA requires that the QUIC stack provide the WebTransport implementation with information about the final size of streams; see Section 4.5 of [RFC9000]. (5.8) + +%% All data sent in WT_STREAM capsules counts toward this limit. The sum of the lengths of Stream Data fields in WT_STREAM capsules MUST NOT exceed the value advertised by a receiver. (5.8) + +%% The initial value for this limit MAY be communicated by sending a non-zero value for SETTINGS_WT_INITIAL_MAX_DATA. (5.8) + +%% 5.9. WT_DATA_BLOCKED Capsule + +%% A sender SHOULD send a WT_DATA_BLOCKED capsule (type=0x190B4D41) when it wishes to send data but is unable to do so due to WebTransport session-level flow control. (5.9) + +%% WT_DATA_BLOCKED capsules can be used as input to tuning of flow control algorithms. (5.9) + +%% 6. Session Termination + +%% A WebTransport session over HTTP/3 is considered terminated when either of the following conditions is met: +%% * the CONNECT stream is closed, either cleanly or abruptly, on either side; or +%% * a WT_CLOSE_SESSION capsule is either sent or received. +%% (6) + +wt_close_session_client(Config) -> + doc("The WT client can close a single session. (draft_webtrans_http3 4.6)"), + %% Connect to the WebTransport server. + #{ + connect_stream_ref := ConnectStreamRef + } = do_webtransport_connect(Config), + %% Send the WT_CLOSE_SESSION capsule on the CONNECT stream. + {ok, _} = quicer:send(ConnectStreamRef, + cow_capsule:wt_close_session(0, <<>>), + ?QUIC_SEND_FLAG_FIN), + %% Normally we should also stop reading but in order to detect + %% that the server stops the stream we must not otherwise the + %% stream will be de facto closed on our end. + %% + %% The recipient must close or reset the stream in response. + receive + {quic, stream_closed, ConnectStreamRef, _} -> + ok + after 1000 -> + error({timeout, waiting_for_stream_closed}) + end. + +wt_close_session_server(Config) -> + doc("The WT server can close a single session. (draft_webtrans_http3 4.6)"), + %% Connect to the WebTransport server. + #{ + conn := Conn, + connect_stream_ref := ConnectStreamRef, + session_id := SessionID + } = do_webtransport_connect(Config), + %% Create a bidi stream, send a special instruction to make it initiate the close. + {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}), + {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:close">>), + %% Receive the WT_CLOSE_SESSION capsule on the CONNECT stream. + CloseWTSessionCapsule = cow_capsule:wt_close_session(0, <<>>), + {fin, CloseWTSessionCapsule} = do_receive_data(ConnectStreamRef), + ok. + +wt_session_gone_client(Config) -> + doc("Upon learning that the session has been terminated, " + "the WT server must reset associated streams with the " + "WEBTRANSPORT_SESSION_GONE error code. (draft_webtrans_http3 4.6)"), + %% Connect to the WebTransport server. + #{ + conn := Conn, + connect_stream_ref := ConnectStreamRef, + session_id := SessionID + } = do_webtransport_connect(Config), + %% Create a unidi stream. + {ok, LocalUnidiStreamRef} = quicer:start_stream(Conn, + #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), + {ok, _} = quicer:send(LocalUnidiStreamRef, + <<1:2, 16#54:14, 0:2, SessionID:6, "Hello">>), + %% Accept an identical unidi stream. + {unidi, RemoteUnidiStreamRef} = do_receive_new_stream(), + {nofin, <<1:2, 16#54:14, 0:2, SessionID:6>>} = do_receive_data(RemoteUnidiStreamRef), + {nofin, <<"Hello">>} = do_receive_data(RemoteUnidiStreamRef), + %% Create a bidi stream, send a special instruction + %% to make the server create another bidi stream. + {ok, LocalBidiStreamRef} = quicer:start_stream(Conn, #{}), + {ok, _} = quicer:send(LocalBidiStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:open_bidi">>), + %% Accept the bidi stream and receive the data. + {bidi, RemoteBidiStreamRef} = do_receive_new_stream(), + {nofin, <<1:2, 16#41:14, 0:2, SessionID:6>>} = do_receive_data(RemoteBidiStreamRef), + {ok, _} = quicer:send(RemoteBidiStreamRef, <<"Hello">>), + {nofin, <<"Hello">>} = do_receive_data(RemoteBidiStreamRef), + %% Send the WT_CLOSE_SESSION capsule on the CONNECT stream. + {ok, _} = quicer:send(ConnectStreamRef, + cow_capsule:wt_close_session(0, <<>>), + ?QUIC_SEND_FLAG_FIN), + %% All streams from that WT session have been aborted. + #{reason := wt_session_gone} = do_wait_stream_aborted(LocalUnidiStreamRef), + #{reason := wt_session_gone} = do_wait_stream_aborted(RemoteUnidiStreamRef), + #{reason := wt_session_gone} = do_wait_stream_aborted(LocalBidiStreamRef), + #{reason := wt_session_gone} = do_wait_stream_aborted(RemoteBidiStreamRef), + ok. + +wt_session_gone_server(Config) -> + doc("After the session has been terminated by the WT server, " + "the WT server must reset associated streams with the " + "WT_SESSION_GONE error code. (draft_webtrans_http3 4.6)"), + %% Connect to the WebTransport server. + #{ + conn := Conn, + connect_stream_ref := ConnectStreamRef, + session_id := SessionID + } = do_webtransport_connect(Config), + %% Create a unidi stream. + {ok, LocalUnidiStreamRef} = quicer:start_stream(Conn, + #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), + {ok, _} = quicer:send(LocalUnidiStreamRef, + <<1:2, 16#54:14, 0:2, SessionID:6, "Hello">>), + %% Accept an identical unidi stream. + {unidi, RemoteUnidiStreamRef} = do_receive_new_stream(), + {nofin, <<1:2, 16#54:14, 0:2, SessionID:6>>} = do_receive_data(RemoteUnidiStreamRef), + {nofin, <<"Hello">>} = do_receive_data(RemoteUnidiStreamRef), + %% Create a bidi stream, send a special instruction + %% to make the server create another bidi stream. + {ok, LocalBidiStreamRef} = quicer:start_stream(Conn, #{}), + {ok, _} = quicer:send(LocalBidiStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:open_bidi">>), + %% Accept the bidi stream and receive the data. + {bidi, RemoteBidiStreamRef} = do_receive_new_stream(), + {nofin, <<1:2, 16#41:14, 0:2, SessionID:6>>} = do_receive_data(RemoteBidiStreamRef), + {ok, _} = quicer:send(RemoteBidiStreamRef, <<"Hello">>), + {nofin, <<"Hello">>} = do_receive_data(RemoteBidiStreamRef), + + %% Send a special instruction to make the server initiate the close. + {ok, _} = quicer:send(LocalBidiStreamRef, <<"TEST:close">>), + %% Receive the WT_CLOSE_SESSION capsule on the CONNECT stream. + CloseWTSessionCapsule = cow_capsule:wt_close_session(0, <<>>), + {fin, CloseWTSessionCapsule} = do_receive_data(ConnectStreamRef), + %% All streams from that WT session have been aborted. + #{reason := wt_session_gone} = do_wait_stream_aborted(LocalUnidiStreamRef), + #{reason := wt_session_gone} = do_wait_stream_aborted(RemoteUnidiStreamRef), + #{reason := wt_session_gone} = do_wait_stream_aborted(LocalBidiStreamRef), + #{reason := wt_session_gone} = do_wait_stream_aborted(RemoteBidiStreamRef), + ok. + +%% Application Error Message: A UTF-8 encoded error message string provided by the application closing the session. The message takes up the remainder of the capsule, and its length MUST NOT exceed 1024 bytes. (6) +%% @todo What if it's larger? + +wt_close_session_app_code_msg_client(Config) -> + doc("The WT client can close a single session with an application error code " + "and an application error message. (draft_webtrans_http3 4.6)"), + %% Connect to the WebTransport server. + #{ + conn := Conn, + connect_stream_ref := ConnectStreamRef, + session_id := SessionID + } = do_webtransport_connect(Config), + %% Create a bidi stream, send a special instruction to make it propagate events. + {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}), + EventPidBin = term_to_binary(self()), + {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, + "TEST:event_pid:", EventPidBin/binary>>), + %% Send the WT_CLOSE_SESSION capsule on the CONNECT stream. + {ok, _} = quicer:send(ConnectStreamRef, + cow_capsule:wt_close_session(17, <<"seventeen">>), + ?QUIC_SEND_FLAG_FIN), + %% @todo Stop reading from the CONNECt stream too. (STOP_SENDING) + %% Receive the terminate event from the WT handler. + receive + {'$wt_echo_h', terminate, {closed, 17, <<"seventeen">>}, _, _} -> + ok + after 1000 -> + error({timeout, waiting_for_terminate_event}) + end. + +wt_close_session_app_code_server(Config) -> + doc("The WT server can close a single session with an application error code. " + "(draft_webtrans_http3 4.6)"), + %% Connect to the WebTransport server. + #{ + conn := Conn, + connect_stream_ref := ConnectStreamRef, + session_id := SessionID + } = do_webtransport_connect(Config), + %% Create a bidi stream, send a special instruction to make it initiate the close. + {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}), + {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, + "TEST:close_app_code">>), + %% Receive the WT_CLOSE_SESSION capsule on the CONNECT stream. + CloseWTSessionCapsule = cow_capsule:wt_close_session(1234567890, <<>>), + {fin, CloseWTSessionCapsule} = do_receive_data(ConnectStreamRef), + ok. + +wt_close_session_app_code_msg_server(Config) -> + doc("The WT server can close a single session with an application error code " + "and an application error message. (draft_webtrans_http3 4.6)"), + %% Connect to the WebTransport server. + #{ + conn := Conn, + connect_stream_ref := ConnectStreamRef, + session_id := SessionID + } = do_webtransport_connect(Config), + %% Create a bidi stream, send a special instruction to make it initiate the close. + {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}), + {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, + "TEST:close_app_code_msg">>), + %% Receive the WT_CLOSE_SESSION capsule on the CONNECT stream. + CloseWTSessionCapsule = iolist_to_binary(cow_capsule:wt_close_session(1234567890, + <<"onetwothreefourfivesixseveneightnineten">>)), + {fin, CloseWTSessionCapsule} = do_receive_data(ConnectStreamRef), + ok. + +%% An endpoint that sends a WT_CLOSE_SESSION capsule MUST immediately send a FIN. The endpoint MAY send a STOP_SENDING to indicate it is no longer reading from the CONNECT stream. The recipient MUST either close or reset the stream in response. (6) +%% @todo wt_close_session_server_fin +%% @todo The part about close/reset should be tested in wt_close_session_client. + +%% If any additional stream data is received on the CONNECT stream after receiving a WT_CLOSE_SESSION capsule, the stream MUST be reset with code H3_MESSAGE_ERROR. (6) +%% @todo wt_close_session_followed_by_data + +connect_stream_closed_cleanly_fin(Config) -> + doc("The WT client closing the CONNECT stream cleanly " + "is equivalent to a capsule with an application error code of 0 " + "and an empty error string. (draft_webtrans_http3 4.6)"), + %% Connect to the WebTransport server. + #{ + conn := Conn, + connect_stream_ref := ConnectStreamRef, + session_id := SessionID + } = do_webtransport_connect(Config), + %% Create a bidi stream, send a special instruction to make it propagate events. + {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}), + EventPidBin = term_to_binary(self()), + {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, + "TEST:event_pid:", EventPidBin/binary>>), + {nofin, <<"event_pid_received">>} = do_receive_data(LocalStreamRef), + %% Cleanly terminate the CONNECT stream. + {ok, _} = quicer:send(ConnectStreamRef, <<>>, ?QUIC_SEND_FLAG_FIN), + %% Receive the terminate event from the WT handler. + receive + {'$wt_echo_h', terminate, {closed, 0, <<>>}, _, _} -> + ok + after 1000 -> + error({timeout, waiting_for_terminate_event}) + end. + +connect_stream_closed_cleanly_shutdown(Config) -> + doc("The WT client closing the CONNECT stream cleanly " + "is equivalent to a capsule with an application error code of 0 " + "and an empty error string. (draft_webtrans_http3 4.6)"), + %% Connect to the WebTransport server. + #{ + conn := Conn, + connect_stream_ref := ConnectStreamRef, + session_id := SessionID + } = do_webtransport_connect(Config), + %% Create a bidi stream, send a special instruction to make it propagate events. + {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}), + EventPidBin = term_to_binary(self()), + {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, + "TEST:event_pid:", EventPidBin/binary>>), + {nofin, <<"event_pid_received">>} = do_receive_data(LocalStreamRef), + %% Cleanly terminate the CONNECT stream. + _ = quicer:shutdown_stream(ConnectStreamRef), + %% Receive the terminate event from the WT handler. + receive + {'$wt_echo_h', terminate, {closed, 0, <<>>}, _, _} -> + ok + after 1000 -> + error({timeout, waiting_for_terminate_event}) + end. + +connect_stream_closed_abruptly(Config) -> + doc("The WT client may close the CONNECT stream abruptly. " + "(draft_webtrans_http3 4.6)"), + %% Connect to the WebTransport server. + #{ + conn := Conn, + connect_stream_ref := ConnectStreamRef, + session_id := SessionID + } = do_webtransport_connect(Config), + %% Create a bidi stream, send a special instruction to make it propagate events. + {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}), + EventPidBin = term_to_binary(self()), + {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, + "TEST:event_pid:", EventPidBin/binary>>), + {nofin, <<"event_pid_received">>} = do_receive_data(LocalStreamRef), + %% Abruptly terminate the CONNECT stream. + _ = quicer:shutdown_stream(ConnectStreamRef, ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT, + 0, infinity), + %% Receive the terminate event from the WT handler. + receive + %% @todo It would be good to forward a stream error as well + %% so that a WT error can be sent, but I have been unsuccessful. + {'$wt_echo_h', terminate, closed_abruptly, _, _} -> + ok + after 1000 -> + error({timeout, waiting_for_terminate_event}) + end. + +%% @todo This one is about gracefully closing HTTP/3 connection with WT sessions. +%% 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_WT_MAX_SESSIONS >= 1. + #{wt_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. + {nofin, Data} = 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, + connect_stream_ref => ConnectStreamRef, + 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. + +-endif. diff --git a/test/examples_SUITE.erl b/test/examples_SUITE.erl index e2327bc..3d7c48b 100644 --- a/test/examples_SUITE.erl +++ b/test/examples_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2016-2024, Loïc Hoguin <[email protected]> +%% 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 @@ -35,10 +35,10 @@ init_per_suite(Config) -> %% reuse the same build across all tests. Make = do_find_make_cmd(), CommonDir = config(priv_dir, Config), - ct:log("~s~n", [os:cmd("git clone --depth 1 https://github.com/ninenines/cowboy " + ct:log("~ts~n", [os:cmd("git clone --depth 1 https://github.com/ninenines/cowboy " ++ CommonDir ++ "cowboy")]), - ct:log("~s~n", [os:cmd(Make ++ " -C " ++ CommonDir ++ "cowboy distclean")]), - ct:log("~s~n", [os:cmd(Make ++ " -C " ++ CommonDir ++ "cowboy DEPS_DIR=" ++ CommonDir)]), + ct:log("~ts~n", [os:cmd(Make ++ " -C " ++ CommonDir ++ "cowboy distclean")]), + ct:log("~ts~n", [os:cmd(Make ++ " -C " ++ CommonDir ++ "cowboy DEPS_DIR=" ++ CommonDir)]), Config. end_per_suite(_) -> @@ -70,24 +70,24 @@ do_get_paths(Example0) -> do_compile_and_start(Example, Config) -> Make = do_find_make_cmd(), {Dir, Rel, _} = do_get_paths(Example), - ct:log("~s~n", [os:cmd(Make ++ " -C " ++ Dir ++ " distclean")]), + ct:log("~ts~n", [os:cmd(Make ++ " -C " ++ Dir ++ " distclean")]), %% We use a common build for Cowboy, Cowlib and Ranch to speed things up. CommonDir = config(priv_dir, Config), - ct:log("~s~n", [os:cmd("mkdir " ++ Dir ++ "/deps")]), - ct:log("~s~n", [os:cmd("ln -s " ++ CommonDir ++ "cowboy " ++ Dir ++ "/deps/cowboy")]), - ct:log("~s~n", [os:cmd("ln -s " ++ CommonDir ++ "cowlib " ++ Dir ++ "/deps/cowlib")]), - ct:log("~s~n", [os:cmd("ln -s " ++ CommonDir ++ "ranch " ++ Dir ++ "/deps/ranch")]), + ct:log("~ts~n", [os:cmd("mkdir " ++ Dir ++ "/deps")]), + ct:log("~ts~n", [os:cmd("ln -s " ++ CommonDir ++ "cowboy " ++ Dir ++ "/deps/cowboy")]), + ct:log("~ts~n", [os:cmd("ln -s " ++ CommonDir ++ "cowlib " ++ Dir ++ "/deps/cowlib")]), + ct:log("~ts~n", [os:cmd("ln -s " ++ CommonDir ++ "ranch " ++ Dir ++ "/deps/ranch")]), %% TERM=dumb disables relx coloring. - ct:log("~s~n", [os:cmd(Make ++ " -C " ++ Dir ++ " TERM=dumb")]), - ct:log("~s~n", [os:cmd(Rel ++ " stop")]), - ct:log("~s~n", [os:cmd(Rel ++ " daemon")]), + ct:log("~ts~n", [os:cmd(Make ++ " -C " ++ Dir ++ " TERM=dumb")]), + ct:log("~ts~n", [os:cmd(Rel ++ " stop")]), + ct:log("~ts~n", [os:cmd(Rel ++ " daemon")]), timer:sleep(2000), ok. do_stop(Example) -> {_, Rel, Log} = do_get_paths(Example), - ct:log("~s~n", [os:cmd(Rel ++ " stop")]), - ct:log("~s~n", [element(2, file:read_file(Log))]), + ct:log("~ts~n", [os:cmd(Rel ++ " stop")]), + ct:log("~ts~n", [element(2, file:read_file(Log))]), ok. %% Fetch a response. diff --git a/test/h2spec_SUITE.erl b/test/h2spec_SUITE.erl index 67ccf03..71a8a41 100644 --- a/test/h2spec_SUITE.erl +++ b/test/h2spec_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]> +%% 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 diff --git a/test/handlers/content_types_provided_h.erl b/test/handlers/content_types_provided_h.erl index 5220c19..397026b 100644 --- a/test/handlers/content_types_provided_h.erl +++ b/test/handlers/content_types_provided_h.erl @@ -11,9 +11,14 @@ init(Req, Opts) -> {cowboy_rest, Req, Opts}. +content_types_provided(Req=#{qs := <<"invalid-type">>}, State) -> + ct_helper:ignore(cowboy_rest, normalize_content_types, 2), + {[{{'*', '*', '*'}, get_text_plain}], Req, State}; content_types_provided(Req=#{qs := <<"wildcard-param">>}, State) -> {[{{<<"text">>, <<"plain">>, '*'}, get_text_plain}], Req, State}. +get_text_plain(Req=#{qs := <<"invalid-type">>}, State) -> + {<<"invalid-type">>, Req, State}; get_text_plain(Req=#{qs := <<"wildcard-param">>}, State) -> {_, _, Param} = maps:get(media_type, Req), Body = if diff --git a/test/handlers/crash_h.erl b/test/handlers/crash_h.erl index b687aba..57d4d85 100644 --- a/test/handlers/crash_h.erl +++ b/test/handlers/crash_h.erl @@ -7,6 +7,9 @@ -export([init/2]). -spec init(_, _) -> no_return(). +init(_, external_exit) -> + ct_helper:ignore(?MODULE, init, 2), + exit(self(), ct_helper_ignore); init(_, no_reply) -> ct_helper:ignore(?MODULE, init, 2), error(crash); diff --git a/test/handlers/read_body_h.erl b/test/handlers/read_body_h.erl new file mode 100644 index 0000000..a0de3b3 --- /dev/null +++ b/test/handlers/read_body_h.erl @@ -0,0 +1,15 @@ +%% This module reads the request body fully and send a 204 response. + +-module(read_body_h). + +-export([init/2]). + +init(Req0, Opts) -> + {ok, Req} = read_body(Req0), + {ok, cowboy_req:reply(200, #{}, Req), Opts}. + +read_body(Req0) -> + case cowboy_req:read_body(Req0) of + {ok, _, Req} -> {ok, Req}; + {more, _, Req} -> read_body(Req) + end. diff --git a/test/handlers/resp_h.erl b/test/handlers/resp_h.erl index 6e9b5f7..d1c46e0 100644 --- a/test/handlers/resp_h.erl +++ b/test/handlers/resp_h.erl @@ -43,12 +43,27 @@ do(<<"set_resp_headers">>, Req0, Opts) -> <<"content-encoding">> => <<"compress">> }, Req0), {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; +do(<<"set_resp_headers_list">>, Req0, Opts) -> + Req = cowboy_req:set_resp_headers([ + {<<"content-type">>, <<"text/plain">>}, + {<<"test-header">>, <<"one">>}, + {<<"content-encoding">>, <<"compress">>}, + {<<"test-header">>, <<"two">>} + ], Req0), + {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; do(<<"set_resp_headers_cookie">>, Req0, Opts) -> ct_helper:ignore(cowboy_req, set_resp_headers, 2), Req = cowboy_req:set_resp_headers(#{ <<"set-cookie">> => <<"name=value">> }, Req0), {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; +do(<<"set_resp_headers_list_cookie">>, Req0, Opts) -> + ct_helper:ignore(cowboy_req, set_resp_headers_list, 3), + Req = cowboy_req:set_resp_headers([ + {<<"set-cookie">>, <<"name=value">>}, + {<<"set-cookie">>, <<"name2=value2">>} + ], Req0), + {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; do(<<"set_resp_headers_http11">>, Req0, Opts) -> Req = cowboy_req:set_resp_headers(#{ <<"connection">> => <<"custom-header, close">>, diff --git a/test/handlers/stream_hello_h.erl b/test/handlers/stream_hello_h.erl new file mode 100644 index 0000000..e67e220 --- /dev/null +++ b/test/handlers/stream_hello_h.erl @@ -0,0 +1,15 @@ +%% This module is the fastest way of producing a Hello world! + +-module(stream_hello_h). + +-export([init/3]). +-export([terminate/3]). + +init(_, _, State) -> + {[ + {response, 200, #{<<"content-length">> => <<"12">>}, <<"Hello world!">>}, + stop + ], State}. + +terminate(_, _, _) -> + ok. diff --git a/test/handlers/ws_ignore.erl b/test/handlers/ws_ignore.erl new file mode 100644 index 0000000..9fe3322 --- /dev/null +++ b/test/handlers/ws_ignore.erl @@ -0,0 +1,20 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(ws_ignore). + +-export([init/2]). +-export([websocket_handle/2]). +-export([websocket_info/2]). + +init(Req, _) -> + {cowboy_websocket, Req, undefined, #{ + compress => true + }}. + +websocket_handle({text, <<"CHECK">>}, State) -> + {[{text, <<"CHECK">>}], State}; +websocket_handle(_Frame, State) -> + {[], State}. + +websocket_info(_Info, State) -> + {[], State}. diff --git a/test/handlers/ws_set_options_commands_h.erl b/test/handlers/ws_set_options_commands_h.erl index 88d4e72..1ab0af4 100644 --- a/test/handlers/ws_set_options_commands_h.erl +++ b/test/handlers/ws_set_options_commands_h.erl @@ -11,10 +11,21 @@ init(Req, RunOrHibernate) -> {cowboy_websocket, Req, RunOrHibernate, #{idle_timeout => infinity}}. -websocket_handle(Frame={text, <<"idle_timeout_short">>}, State=run) -> - {[{set_options, #{idle_timeout => 500}}, Frame], State}; -websocket_handle(Frame={text, <<"idle_timeout_short">>}, State=hibernate) -> - {[{set_options, #{idle_timeout => 500}}, Frame], State, hibernate}. +%% Set the idle_timeout option dynamically. +websocket_handle({text, <<"idle_timeout_short">>}, State=run) -> + {[{set_options, #{idle_timeout => 500}}], State}; +websocket_handle({text, <<"idle_timeout_short">>}, State=hibernate) -> + {[{set_options, #{idle_timeout => 500}}], State, hibernate}; +%% Set the max_frame_size option dynamically. +websocket_handle({text, <<"max_frame_size_small">>}, State=run) -> + {[{set_options, #{max_frame_size => 1000}}], State}; +websocket_handle({text, <<"max_frame_size_small">>}, State=hibernate) -> + {[{set_options, #{max_frame_size => 1000}}], State, hibernate}; +%% We just echo binary frames. +websocket_handle(Frame={binary, _}, State=run) -> + {[Frame], State}; +websocket_handle(Frame={binary, _}, State=hibernate) -> + {[Frame], State, hibernate}. websocket_info(_Info, State) -> {[], State}. diff --git a/test/handlers/wt_echo_h.erl b/test/handlers/wt_echo_h.erl new file mode 100644 index 0000000..5198565 --- /dev/null +++ b/test/handlers/wt_echo_h.erl @@ -0,0 +1,103 @@ +%% This module echoes client events back, +%% including creating new streams. + +-module(wt_echo_h). +-behavior(cowboy_webtransport). + +-export([init/2]). +-export([webtransport_handle/2]). +-export([webtransport_info/2]). +-export([terminate/3]). + +%% -define(DEBUG, 1). +-ifdef(DEBUG). +-define(LOG(Fmt, Args), ct:pal(Fmt, Args)). +-else. +-define(LOG(Fmt, Args), _ = Fmt, _ = Args, ok). +-endif. + +init(Req0, _) -> + ?LOG("WT init ~p~n", [Req0]), + Req = case cowboy_req:parse_header(<<"wt-available-protocols">>, Req0) of + undefined -> + Req0; + [Protocol|_] -> + cowboy_req:set_resp_header(<<"wt-protocol">>, cow_http_hd:wt_protocol(Protocol), Req0) + end, + {cowboy_webtransport, Req, #{}}. + +webtransport_handle(Event = {stream_open, StreamID, bidi}, Streams) -> + ?LOG("WT handle ~p~n", [Event]), + {[], Streams#{StreamID => bidi}}; +webtransport_handle(Event = {stream_open, StreamID, unidi}, Streams) -> + ?LOG("WT handle ~p~n", [Event]), + OpenStreamRef = make_ref(), + {[{open_stream, OpenStreamRef, unidi, <<>>}], Streams#{ + StreamID => {unidi_remote, OpenStreamRef}, + OpenStreamRef => {unidi_local, StreamID}}}; +webtransport_handle(Event = {opened_stream_id, OpenStreamRef, OpenStreamID}, Streams) -> + ?LOG("WT handle ~p~n", [Event]), + case Streams of + #{OpenStreamRef := bidi} -> + {[], maps:remove(OpenStreamRef, Streams#{ + OpenStreamID => bidi + })}; + #{OpenStreamRef := {unidi_local, RemoteStreamID}} -> + #{RemoteStreamID := {unidi_remote, OpenStreamRef}} = Streams, + {[], maps:remove(OpenStreamRef, Streams#{ + RemoteStreamID => {unidi_remote, OpenStreamID}, + OpenStreamID => {unidi_local, RemoteStreamID} + })} + end; +webtransport_handle(Event = {stream_data, StreamID, _IsFin, <<"TEST:", Test/bits>>}, Streams) -> + ?LOG("WT handle ~p~n", [Event]), + case Test of + <<"open_bidi">> -> + OpenStreamRef = make_ref(), + {[{open_stream, OpenStreamRef, bidi, <<>>}], + Streams#{OpenStreamRef => bidi}}; + <<"initiate_close">> -> + {[initiate_close], Streams}; + <<"close">> -> + {[close], Streams}; + <<"close_app_code">> -> + {[{close, 1234567890}], Streams}; + <<"close_app_code_msg">> -> + {[{close, 1234567890, <<"onetwothreefourfivesixseveneightnineten">>}], Streams}; + <<"event_pid:", EventPidBin/bits>> -> + {[{send, StreamID, nofin, <<"event_pid_received">>}], + Streams#{event_pid => binary_to_term(EventPidBin)}} + end; +webtransport_handle(Event = {stream_data, StreamID, IsFin, Data}, Streams) -> + ?LOG("WT handle ~p~n", [Event]), + case Streams of + #{StreamID := bidi} -> + {[{send, StreamID, IsFin, Data}], Streams}; + #{StreamID := {unidi_remote, Ref}} when is_reference(Ref) -> + %% The stream isn't ready. We try again later. + erlang:send_after(100, self(), {try_again, Event}), + {[], Streams}; + #{StreamID := {unidi_remote, LocalStreamID}} -> + {[{send, LocalStreamID, IsFin, Data}], Streams} + end; +webtransport_handle(Event = {datagram, Data}, Streams) -> + ?LOG("WT handle ~p~n", [Event]), + {[{send, datagram, Data}], Streams}; +webtransport_handle(Event = close_initiated, Streams) -> + ?LOG("WT handle ~p~n", [Event]), + {[{send, datagram, <<"TEST:close_initiated">>}], Streams}; +webtransport_handle(Event, Streams) -> + ?LOG("WT handle ignore ~p~n", [Event]), + {[], Streams}. + +webtransport_info({try_again, Event}, Streams) -> + ?LOG("WT try_again ~p", [Event]), + webtransport_handle(Event, Streams). + +terminate(Reason, Req, State=#{event_pid := EventPid}) -> + ?LOG("WT terminate ~0p~n~0p~n~0p", [Reason, Req, State]), + EventPid ! {'$wt_echo_h', terminate, Reason, Req, State}, + ok; +terminate(Reason, Req, State) -> + ?LOG("WT terminate ~0p~n~0p~n~0p", [Reason, Req, State]), + ok. diff --git a/test/http2_SUITE.erl b/test/http2_SUITE.erl index d17508a..6f2d020 100644 --- a/test/http2_SUITE.erl +++ b/test/http2_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]> +%% 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 @@ -51,6 +51,27 @@ do_handshake(Settings, Config) -> {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, Socket}. +hibernate(Config) -> + doc("Ensure that we can enable hibernation for HTTP/1.1 connections."), + {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ + env => #{dispatch => init_dispatch(Config)}, + hibernate => true + }), + Port = ranch:get_port(?FUNCTION_NAME), + try + ConnPid = gun_open([{type, tcp}, {protocol, http2}, {port, Port}|Config]), + {ok, http2} = gun:await_up(ConnPid), + StreamRef1 = gun:get(ConnPid, "/"), + StreamRef2 = gun:get(ConnPid, "/"), + StreamRef3 = gun:get(ConnPid, "/"), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef3), + gun:close(ConnPid) + after + cowboy:stop_listener(?FUNCTION_NAME) + end. + idle_timeout(Config) -> doc("Terminate when the idle timeout is reached."), ProtoOpts = #{ @@ -449,8 +470,8 @@ graceful_shutdown_listener_timeout(Config) -> send_timeout_close(Config) -> doc("Check that connections are closed on send timeout."), TransOpts = #{ - port => 0, socket_opts => [ + {port, 0}, {send_timeout, 100}, {send_timeout_close, true}, {sndbuf, 10} diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl index 0325279..9928136 100644 --- a/test/http_SUITE.erl +++ b/test/http_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2018-2024, Loïc Hoguin <[email protected]> +%% 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 @@ -28,9 +28,16 @@ -import(cowboy_test, [raw_recv/3]). -import(cowboy_test, [raw_expect_recv/2]). -all() -> [{group, clear}]. +all() -> + [{group, clear_no_parallel}, {group, clear}]. -groups() -> [{clear, [parallel], ct_helper:all(?MODULE)}]. +groups() -> + [ + %% cowboy:stop_listener can be slow when called many times + %% in parallel so we must run this test separately from the others. + {clear_no_parallel, [], [graceful_shutdown_listener]}, + {clear, [parallel], ct_helper:all(?MODULE) -- [graceful_shutdown_listener]} + ]. init_per_group(Name, Config) -> cowboy_test:init_http(Name, #{ @@ -43,6 +50,7 @@ end_per_group(Name, _) -> init_dispatch(_) -> cowboy_router:compile([{"localhost", [ {"/", hello_h, []}, + {"/delay_hello", delay_hello_h, #{delay => 1000, notify_received => self()}}, {"/echo/:key", echo_h, []}, {"/resp/:key[/:arg]", resp_h, []}, {"/set_options/:key", set_options_h, []}, @@ -198,6 +206,94 @@ do_chunked_body(ChunkSize0, Data, Acc) -> do_chunked_body(ChunkSize, Rest, [iolist_to_binary(cow_http_te:chunk(Chunk))|Acc]). +disable_http1_tls(Config) -> + doc("Ensure that we can disable HTTP/1.1 over TLS (force HTTP/2)."), + TlsOpts = ct_helper:get_certs_from_ets(), + {ok, _} = cowboy:start_tls(?FUNCTION_NAME, TlsOpts ++ [{port, 0}], #{ + env => #{dispatch => init_dispatch(Config)}, + alpn_default_protocol => http2 + }), + Port = ranch:get_port(?FUNCTION_NAME), + try + {ok, Socket} = ssl:connect("localhost", Port, + [binary, {active, false}|TlsOpts]), + %% ALPN was not negotiated but we're still over HTTP/2. + {error, protocol_not_negotiated} = ssl:negotiated_protocol(Socket), + %% Send a valid preface. + ok = ssl:send(Socket, [ + "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", + cow_http2:settings(#{})]), + %% Receive the server preface. + {ok, << Len:24 >>} = ssl:recv(Socket, 3, 1000), + {ok, << 4:8, 0:40, _:Len/binary >>} = ssl:recv(Socket, 6 + Len, 1000), + ok + after + cowboy:stop_listener(?FUNCTION_NAME) + end. + +disable_http2_prior_knowledge(Config) -> + doc("Ensure that we can disable prior knowledge HTTP/2 upgrade."), + {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ + env => #{dispatch => init_dispatch(Config)}, + protocols => [http] + }), + Port = ranch:get_port(?FUNCTION_NAME), + try + {ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {active, false}]), + %% Send a valid preface. + ok = gen_tcp:send(Socket, [ + "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", + cow_http2:settings(#{})]), + {ok, <<"HTTP/1.1 501">>} = gen_tcp:recv(Socket, 12, 1000), + ok + after + cowboy:stop_listener(?FUNCTION_NAME) + end. + +disable_http2_upgrade(Config) -> + doc("Ensure that we can disable HTTP/1.1 upgrade to HTTP/2."), + {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ + env => #{dispatch => init_dispatch(Config)}, + protocols => [http] + }), + Port = ranch:get_port(?FUNCTION_NAME), + try + {ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {active, false}]), + %% Send a valid preface. + ok = gen_tcp:send(Socket, [ + "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: Upgrade, HTTP2-Settings\r\n" + "Upgrade: h2c\r\n" + "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", + "\r\n"]), + {ok, <<"HTTP/1.1 200">>} = gen_tcp:recv(Socket, 12, 1000), + ok + after + cowboy:stop_listener(?FUNCTION_NAME) + end. + +hibernate(Config) -> + doc("Ensure that we can enable hibernation for HTTP/1.1 connections."), + {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ + env => #{dispatch => init_dispatch(Config)}, + hibernate => true + }), + Port = ranch:get_port(?FUNCTION_NAME), + try + ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]), + {ok, http} = gun:await_up(ConnPid), + StreamRef1 = gun:get(ConnPid, "/"), + StreamRef2 = gun:get(ConnPid, "/"), + StreamRef3 = gun:get(ConnPid, "/"), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef3), + gun:close(ConnPid) + after + cowboy:stop_listener(?FUNCTION_NAME) + end. + http10_keepalive_false(Config) -> doc("Confirm the option http10_keepalive => false disables keep-alive " "completely for HTTP/1.0 connections."), @@ -454,6 +550,26 @@ request_timeout_pipeline(Config) -> cowboy:stop_listener(?FUNCTION_NAME) end. +request_timeout_pipeline_delay(Config) -> + doc("Ensure the request_timeout does not trigger on requests " + "coming in after a large request body."), + {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ + env => #{dispatch => init_dispatch(Config)}, + request_timeout => 500 + }), + Port = ranch:get_port(?FUNCTION_NAME), + try + ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]), + {ok, http} = gun:await_up(ConnPid), + StreamRef1 = gun:post(ConnPid, "/", #{}, <<0:8000000>>), + StreamRef2 = gun:get(ConnPid, "/delay_hello"), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), + {error, {down, {shutdown, closed}}} = gun:await(ConnPid, undefined, 1000) + after + cowboy:stop_listener(?FUNCTION_NAME) + end. + request_timeout_skip_body(Config) -> doc("Ensure the request_timeout drops connections when requests " "fail to come in fast enough after skipping a request body."), @@ -779,8 +895,8 @@ graceful_shutdown_listener(Config) -> send_timeout_close(_Config) -> doc("Check that connections are closed on send timeout."), TransOpts = #{ - port => 0, socket_opts => [ + {port, 0}, {send_timeout, 100}, {send_timeout_close, true}, {sndbuf, 10} diff --git a/test/http_perf_SUITE.erl b/test/http_perf_SUITE.erl new file mode 100644 index 0000000..1484c03 --- /dev/null +++ b/test/http_perf_SUITE.erl @@ -0,0 +1,220 @@ +%% 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(http_perf_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [config/2]). +-import(ct_helper, [doc/1]). +-import(cowboy_test, [gun_open/1]). + +%% ct. + +all() -> + %% @todo Enable HTTP/3 for this test suite. + cowboy_test:common_all() -- [{group, h3}, {group, h3_compress}]. + +groups() -> + cowboy_test:common_groups(ct_helper:all(?MODULE), no_parallel). + +init_per_suite(Config) -> + do_log("", []), + %% Optionally enable `perf` for the current node. +% spawn(fun() -> ct:pal(os:cmd("perf record -g -F 9999 -o /tmp/http_perf.data -p " ++ os:getpid() ++ " -- sleep 60")) end), + Config. + +end_per_suite(_) -> + ok. + +init_per_group(Name, Config) -> + [{group, Name}|cowboy_test:init_common_groups(Name, Config, ?MODULE, #{ + %% HTTP/1.1 + max_keepalive => infinity, + %% HTTP/2 + %% @todo Must configure Gun for performance too. + connection_window_margin_size => 64*1024, + enable_connect_protocol => true, + env => #{dispatch => init_dispatch(Config)}, + max_frame_size_sent => 64*1024, + max_frame_size_received => 16384 * 1024 - 1, + max_received_frame_rate => {10_000_000, 1}, + stream_window_data_threshold => 1024, + stream_window_margin_size => 64*1024 + })]. + +end_per_group(Name, _) -> + do_log("", []), + cowboy_test:stop_group(Name). + +%% Routes. + +init_dispatch(_) -> + cowboy_router:compile([{'_', [ + {"/", hello_h, []}, + {"/read_body", read_body_h, []} + ]}]). + +%% Tests: Hello world. + +plain_h_hello_1(Config) -> + doc("Plain HTTP handler Hello World; 10K requests per 1 client."), + do_bench_get(?FUNCTION_NAME, "/", #{}, 1, 10000, Config). + +plain_h_hello_10(Config) -> + doc("Plain HTTP handler Hello World; 10K requests per 10 clients."), + do_bench_get(?FUNCTION_NAME, "/", #{}, 10, 10000, Config). + +stream_h_hello_1(Config) -> + doc("Stream handler Hello World; 10K requests per 1 client."), + do_stream_h_hello(Config, 1). + +stream_h_hello_10(Config) -> + doc("Stream handler Hello World; 10K requests per 10 clients."), + do_stream_h_hello(Config, 10). + +do_stream_h_hello(Config, NumClients) -> + Ref = config(ref, Config), + ProtoOpts = ranch:get_protocol_options(Ref), + StreamHandlers = case ProtoOpts of + #{stream_handlers := _} -> [cowboy_compress_h, stream_hello_h]; + _ -> [stream_hello_h] + end, + ranch:set_protocol_options(Ref, ProtoOpts#{ + env => #{}, + stream_handlers => StreamHandlers + }), + do_bench_get(?FUNCTION_NAME, "/", #{}, NumClients, 10000, Config), + ranch:set_protocol_options(Ref, ProtoOpts). + +%% Tests: Large body upload. + +plain_h_1M_post_1(Config) -> + doc("Plain HTTP handler body reading; 10K requests per 1 client."), + do_bench_post(?FUNCTION_NAME, "/read_body", #{}, <<0:8_000_000>>, 1, 10000, Config). + +plain_h_1M_post_10(Config) -> + doc("Plain HTTP handler body reading; 10K requests per 10 clients."), + do_bench_post(?FUNCTION_NAME, "/read_body", #{}, <<0:8_000_000>>, 10, 10000, Config). + +plain_h_10G_post(Config) -> + doc("Plain HTTP handler body reading; 1 request with a 10GB body."), + do_bench_post_one_large(?FUNCTION_NAME, "/read_body", #{}, 10_000, <<0:8_000_000>>, Config). + +%% Internal. + +do_bench_get(What, Path, Headers, NumClients, NumRuns, Config) -> + Clients = [spawn_link(?MODULE, do_bench_get_proc, + [self(), What, Path, Headers, NumRuns, Config]) + || _ <- lists:seq(1, NumClients)], + {Time, _} = timer:tc(?MODULE, do_bench_wait, [What, Clients]), + do_log("~32s: ~8bµs ~8.1freqs/s", [ + [atom_to_list(config(group, Config)), $., atom_to_list(What)], + Time, + (NumClients * NumRuns) / Time * 1_000_000]), + ok. + +do_bench_get_proc(Parent, What, Path, Headers0, NumRuns, Config) -> + ConnPid = gun_open(Config), + Headers = Headers0#{<<"accept-encoding">> => <<"gzip">>}, + Parent ! {What, ready}, + receive {What, go} -> ok end, + do_bench_get_run(ConnPid, Path, Headers, NumRuns), + Parent ! {What, done}, + gun:close(ConnPid). + +do_bench_get_run(_, _, _, 0) -> + ok; +do_bench_get_run(ConnPid, Path, Headers, Num) -> + Ref = gun:request(ConnPid, <<"GET">>, Path, Headers, <<>>), + {response, IsFin, 200, _RespHeaders} = gun:await(ConnPid, Ref, infinity), + {ok, _} = case IsFin of + nofin -> gun:await_body(ConnPid, Ref, infinity); + fin -> {ok, <<>>} + end, + do_bench_get_run(ConnPid, Path, Headers, Num - 1). + +do_bench_post(What, Path, Headers, Body, NumClients, NumRuns, Config) -> + Clients = [spawn_link(?MODULE, do_bench_post_proc, + [self(), What, Path, Headers, Body, NumRuns, Config]) + || _ <- lists:seq(1, NumClients)], + {Time, _} = timer:tc(?MODULE, do_bench_wait, [What, Clients]), + do_log("~32s: ~8bµs ~8.1freqs/s", [ + [atom_to_list(config(group, Config)), $., atom_to_list(What)], + Time, + (NumClients * NumRuns) / Time * 1_000_000]), + ok. + +do_bench_post_proc(Parent, What, Path, Headers0, Body, NumRuns, Config) -> + ConnPid = gun_open(Config), + Headers = Headers0#{<<"accept-encoding">> => <<"gzip">>}, + Parent ! {What, ready}, + receive {What, go} -> ok end, + do_bench_post_run(ConnPid, Path, Headers, Body, NumRuns), + Parent ! {What, done}, + gun:close(ConnPid). + +do_bench_post_run(_, _, _, _, 0) -> + ok; +do_bench_post_run(ConnPid, Path, Headers, Body, Num) -> + Ref = gun:request(ConnPid, <<"POST">>, Path, Headers, Body), + {response, IsFin, 200, _RespHeaders} = gun:await(ConnPid, Ref, infinity), + {ok, _} = case IsFin of + nofin -> gun:await_body(ConnPid, Ref, infinity); + fin -> {ok, <<>>} + end, + do_bench_post_run(ConnPid, Path, Headers, Body, Num - 1). + +do_bench_post_one_large(What, Path, Headers, NumChunks, BodyChunk, Config) -> + Client = spawn_link(?MODULE, do_bench_post_one_large_proc, + [self(), What, Path, Headers, NumChunks, BodyChunk, Config]), + {Time, _} = timer:tc(?MODULE, do_bench_wait, [What, [Client]]), + do_log("~32s: ~8bµs ~8.1freqs/s", [ + [atom_to_list(config(group, Config)), $., atom_to_list(What)], + Time, + 1 / Time * 1_000_000]), + ok. + +do_bench_post_one_large_proc(Parent, What, Path, Headers0, NumChunks, BodyChunk, Config) -> + ConnPid = gun_open(Config), + Headers = Headers0#{<<"accept-encoding">> => <<"gzip">>}, + Parent ! {What, ready}, + receive {What, go} -> ok end, + StreamRef = gun:headers(ConnPid, <<"POST">>, Path, Headers#{ + <<"content-length">> => integer_to_binary(NumChunks * byte_size(BodyChunk)) + }), + do_bench_post_one_large_run(ConnPid, StreamRef, NumChunks - 1, BodyChunk), + {response, IsFin, 200, _RespHeaders} = gun:await(ConnPid, StreamRef, infinity), + {ok, _} = case IsFin of + nofin -> gun:await_body(ConnPid, StreamRef, infinity); + fin -> {ok, <<>>} + end, + Parent ! {What, done}, + gun:close(ConnPid). + +do_bench_post_one_large_run(ConnPid, StreamRef, 0, BodyChunk) -> + gun:data(ConnPid, StreamRef, fin, BodyChunk); +do_bench_post_one_large_run(ConnPid, StreamRef, NumChunks, BodyChunk) -> + gun:data(ConnPid, StreamRef, nofin, BodyChunk), + do_bench_post_one_large_run(ConnPid, StreamRef, NumChunks - 1, BodyChunk). + +do_bench_wait(What, Clients) -> + _ = [receive {What, ready} -> ok end || _ <- Clients], + _ = [ClientPid ! {What, go} || ClientPid <- Clients], + _ = [receive {What, done} -> ok end || _ <- Clients], + ok. + +do_log(Str, Args) -> + ct:log(Str, Args), + io:format(ct_default_gl, Str ++ "~n", Args). diff --git a/test/loop_handler_SUITE.erl b/test/loop_handler_SUITE.erl index c5daaf8..71aa801 100644 --- a/test/loop_handler_SUITE.erl +++ b/test/loop_handler_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]> +%% 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 diff --git a/test/metrics_SUITE.erl b/test/metrics_SUITE.erl index 6a272f2..784bec1 100644 --- a/test/metrics_SUITE.erl +++ b/test/metrics_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]> +%% 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 diff --git a/test/misc_SUITE.erl b/test/misc_SUITE.erl index c918321..e834156 100644 --- a/test/misc_SUITE.erl +++ b/test/misc_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]> +%% 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 diff --git a/test/plain_handler_SUITE.erl b/test/plain_handler_SUITE.erl index 756c0a6..7684e6b 100644 --- a/test/plain_handler_SUITE.erl +++ b/test/plain_handler_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2018-2024, Loïc Hoguin <[email protected]> +%% 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 @@ -45,6 +45,7 @@ end_per_group(Name, _) -> init_dispatch(_) -> cowboy_router:compile([{"localhost", [ + {"/crash/external_exit", crash_h, external_exit}, {"/crash/no_reply", crash_h, no_reply}, {"/crash/reply", crash_h, reply} ]}]). @@ -78,3 +79,13 @@ crash_before_reply(Config) -> ]), {response, fin, 500, _} = gun:await(ConnPid, Ref), gun:close(ConnPid). + +external_exit_before_reply(Config) -> + doc("A plain handler exits externally before a response was sent " + "results in a 500 response."), + ConnPid = gun_open(Config), + Ref = gun:get(ConnPid, "/crash/external_exit", [ + {<<"accept-encoding">>, <<"gzip">>} + ]), + {response, fin, 500, _} = gun:await(ConnPid, Ref), + gun:close(ConnPid). diff --git a/test/proxy_header_SUITE.erl b/test/proxy_header_SUITE.erl index cb6ab47..c8f63a3 100644 --- a/test/proxy_header_SUITE.erl +++ b/test/proxy_header_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2018-2024, Loïc Hoguin <[email protected]> +%% 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 diff --git a/test/req_SUITE.erl b/test/req_SUITE.erl index 9036cac..9adc6e4 100644 --- a/test/req_SUITE.erl +++ b/test/req_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2016-2024, Loïc Hoguin <[email protected]> +%% 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 @@ -324,7 +324,7 @@ filter_then_parse_cookies(Config) -> [{<<"cookie">>, "bad name=strawberry"}], Config), <<"[{<<\"cake\">>,<<\"strawberry\">>}]">> = do_get_body("/filter_then_parse_cookies", - [{<<"cookie">>, "bad name=strawberry; cake=strawberry"}], Config), + [{<<"cookie">>, "bad name=strawberry; another bad name=strawberry; cake=strawberry"}], Config), <<"[]">> = do_get_body("/filter_then_parse_cookies", [{<<"cookie">>, "Blocked by http://www.example.com/upgrade-to-remove"}], Config), @@ -858,11 +858,16 @@ set_resp_header(Config) -> set_resp_headers(Config) -> doc("Response using set_resp_headers."), - {200, Headers, <<"OK">>} = do_get("/resp/set_resp_headers", Config), - true = lists:keymember(<<"content-type">>, 1, Headers), - true = lists:keymember(<<"content-encoding">>, 1, Headers), + {200, Headers1, <<"OK">>} = do_get("/resp/set_resp_headers", Config), + true = lists:keymember(<<"content-type">>, 1, Headers1), + true = lists:keymember(<<"content-encoding">>, 1, Headers1), + {200, Headers2, <<"OK">>} = do_get("/resp/set_resp_headers_list", Config), + true = lists:keymember(<<"content-type">>, 1, Headers2), + true = lists:keymember(<<"content-encoding">>, 1, Headers2), + {_, <<"one, two">>} = lists:keyfind(<<"test-header">>, 1, Headers2), %% The set-cookie header is special. set_resp_cookie must be used. {500, _, _} = do_maybe_h3_error3(do_get("/resp/set_resp_headers_cookie", Config)), + {500, _, _} = do_maybe_h3_error3(do_get("/resp/set_resp_headers_list_cookie", Config)), ok. resp_header(Config) -> diff --git a/test/rest_handler_SUITE.erl b/test/rest_handler_SUITE.erl index 6c1f1c1..a3d9533 100644 --- a/test/rest_handler_SUITE.erl +++ b/test/rest_handler_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]> +%% 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 @@ -404,6 +404,18 @@ content_types_accepted_wildcard_param_content_type_with_param(Config) -> {response, fin, 204, _} = gun:await(ConnPid, Ref), ok. +content_types_provided_invalid_type(Config) -> + doc("When an invalid type is returned from the " + "content_types_provided callback, the " + "resource is incorrect and a 500 response is expected."), + ConnPid = gun_open(Config), + Ref = gun:get(ConnPid, "/content_types_provided?invalid-type", [ + {<<"accept">>, <<"*/*">>}, + {<<"accept-encoding">>, <<"gzip">>} + ]), + {response, _, 500, _} = do_maybe_h3_error(gun:await(ConnPid, Ref)), + ok. + content_types_provided_wildcard_param_no_accept_param(Config) -> doc("When a wildcard is returned for parameters from the " "content_types_provided callback, an accept header " @@ -805,6 +817,7 @@ provide_callback(Config) -> ]), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers), + {_, <<"HEAD, GET, OPTIONS">>} = lists:keyfind(<<"allow">>, 1, Headers), {ok, <<"This is REST!">>} = gun:await_body(ConnPid, Ref), ok. diff --git a/test/rfc6585_SUITE.erl b/test/rfc6585_SUITE.erl index 17cbb07..4a627e5 100644 --- a/test/rfc6585_SUITE.erl +++ b/test/rfc6585_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2018-2024, Loïc Hoguin <[email protected]> +%% 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 diff --git a/test/rfc7230_SUITE.erl b/test/rfc7230_SUITE.erl index 17d1905..d0da0df 100644 --- a/test/rfc7230_SUITE.erl +++ b/test/rfc7230_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2015-2024, Loïc Hoguin <[email protected]> +%% 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 diff --git a/test/rfc7231_SUITE.erl b/test/rfc7231_SUITE.erl index 4475899..183fa0f 100644 --- a/test/rfc7231_SUITE.erl +++ b/test/rfc7231_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]> +%% 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 diff --git a/test/rfc7538_SUITE.erl b/test/rfc7538_SUITE.erl index c46d388..ea51209 100644 --- a/test/rfc7538_SUITE.erl +++ b/test/rfc7538_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2018-2024, Loïc Hoguin <[email protected]> +%% 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 diff --git a/test/rfc7540_SUITE.erl b/test/rfc7540_SUITE.erl index f040601..76aa95f 100644 --- a/test/rfc7540_SUITE.erl +++ b/test/rfc7540_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2016-2024, Loïc Hoguin <[email protected]> +%% 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 @@ -35,8 +35,9 @@ all() -> [{group, clear}, {group, tls}]. groups() -> Tests = ct_helper:all(?MODULE), - Clear = [T || T <- Tests, lists:sublist(atom_to_list(T), 4) =/= "alpn"] -- [prior_knowledge_reject_tls], - TLS = [T || T <- Tests, lists:sublist(atom_to_list(T), 4) =:= "alpn"] ++ [prior_knowledge_reject_tls], + RejectTLS = [http_upgrade_reject_tls, prior_knowledge_reject_tls], + Clear = [T || T <- Tests, lists:sublist(atom_to_list(T), 4) =/= "alpn"] -- RejectTLS, + TLS = [T || T <- Tests, lists:sublist(atom_to_list(T), 4) =:= "alpn"] ++ RejectTLS, [{clear, [parallel], Clear}, {tls, [parallel], TLS}]. init_per_group(Name = clear, Config) -> @@ -68,6 +69,24 @@ init_routes(_) -> [ %% Starting HTTP/2 for "http" URIs. +http_upgrade_reject_tls(Config) -> + doc("Implementations that support HTTP/2 over TLS must use ALPN. (RFC7540 3.4)"), + TlsOpts = ct_helper:get_certs_from_ets(), + {ok, Socket} = ssl:connect("localhost", config(port, Config), + [binary, {active, false}|TlsOpts]), + %% Send a valid preface. + ok = ssl:send(Socket, [ + "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: Upgrade, HTTP2-Settings\r\n" + "Upgrade: h2c\r\n" + "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", + "\r\n"]), + %% We expect the server to send an HTTP 400 error + %% when trying to use HTTP/2 without going through ALPN negotiation. + {ok, <<"HTTP/1.1 400">>} = ssl:recv(Socket, 12, 1000), + ok. + http_upgrade_ignore_h2(Config) -> doc("An h2 token in an Upgrade field must be ignored. (RFC7540 3.2)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), diff --git a/test/rfc8297_SUITE.erl b/test/rfc8297_SUITE.erl index c6c1c9d..42ae92e 100644 --- a/test/rfc8297_SUITE.erl +++ b/test/rfc8297_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2018-2024, Loïc Hoguin <[email protected]> +%% 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 diff --git a/test/rfc8441_SUITE.erl b/test/rfc8441_SUITE.erl index 3e71667..b788f9f 100644 --- a/test/rfc8441_SUITE.erl +++ b/test/rfc8441_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2018-2024, Loïc Hoguin <[email protected]> +%% 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 diff --git a/test/rfc9114_SUITE.erl b/test/rfc9114_SUITE.erl index 4a36ee1..a03b493 100644 --- a/test/rfc9114_SUITE.erl +++ b/test/rfc9114_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2023-2024, Loïc Hoguin <[email protected]> +%% 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 diff --git a/test/rfc9204_SUITE.erl b/test/rfc9204_SUITE.erl index e8defd2..942c41b 100644 --- a/test/rfc9204_SUITE.erl +++ b/test/rfc9204_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2024, Loïc Hoguin <[email protected]> +%% 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 diff --git a/test/rfc9220_SUITE.erl b/test/rfc9220_SUITE.erl index 7f447ed..38a59b2 100644 --- a/test/rfc9220_SUITE.erl +++ b/test/rfc9220_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2018, Loïc Hoguin <[email protected]> +%% 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 @@ -426,7 +426,7 @@ reject_upgrade_header(Config) -> % Examples. 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. #{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config), #{enable_connect_protocol := true} = Settings, diff --git a/test/security_SUITE.erl b/test/security_SUITE.erl index 944c491..25d5280 100644 --- a/test/security_SUITE.erl +++ b/test/security_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2018-2024, Loïc Hoguin <[email protected]> +%% 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 diff --git a/test/static_handler_SUITE.erl b/test/static_handler_SUITE.erl index 9620f66..6721b48 100644 --- a/test/static_handler_SUITE.erl +++ b/test/static_handler_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2016-2024, Loïc Hoguin <[email protected]> +%% 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 @@ -230,7 +230,7 @@ execute(Req=#{path := Path}, Env) -> <<"/bad/dir/route">> -> ct_helper:ignore(cowboy_static, escape_reserved, 1); <<"/bad">> -> ct_helper:ignore(cowboy_static, init_opts, 2); <<"/bad/options">> -> ct_helper:ignore(cowboy_static, content_types_provided, 2); - <<"/bad/options/mime">> -> ct_helper:ignore(cowboy_rest, set_content_type, 2); + <<"/bad/options/mime">> -> ct_helper:ignore(cowboy_rest, normalize_content_types, 2); <<"/bad/options/etag">> -> ct_helper:ignore(cowboy_static, generate_etag, 2); <<"/bad/options/charset">> -> ct_helper:ignore(cowboy_static, charsets_provided, 2); _ -> ok diff --git a/test/stream_handler_SUITE.erl b/test/stream_handler_SUITE.erl index f8e2200..90229c0 100644 --- a/test/stream_handler_SUITE.erl +++ b/test/stream_handler_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]> +%% 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 diff --git a/test/sys_SUITE.erl b/test/sys_SUITE.erl index 2feb716..3591490 100644 --- a/test/sys_SUITE.erl +++ b/test/sys_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2018-2024, Loïc Hoguin <[email protected]> +%% 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 diff --git a/test/tracer_SUITE.erl b/test/tracer_SUITE.erl index af1f8f3..4298b44 100644 --- a/test/tracer_SUITE.erl +++ b/test/tracer_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]> +%% 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 diff --git a/test/ws_SUITE.erl b/test/ws_SUITE.erl index 3b74339..6fa4e61 100644 --- a/test/ws_SUITE.erl +++ b/test/ws_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]> +%% 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 @@ -203,6 +203,25 @@ do_ws_version(Socket) -> {error, closed} = gen_tcp:recv(Socket, 0, 6000), ok. +ws_deflate_max_frame_size_close(Config) -> + doc("Server closes connection when decompressed frame size exceeds max_frame_size option"), + %% max_frame_size is set to 8 bytes in ws_max_frame_size. + {ok, Socket, Headers} = do_handshake("/ws_max_frame_size", + "Sec-WebSocket-Extensions: permessage-deflate\r\n", Config), + {_, "permessage-deflate"} = lists:keyfind("sec-websocket-extensions", 1, Headers), + Mask = 16#11223344, + Z = zlib:open(), + zlib:deflateInit(Z, best_compression, deflated, -15, 8, default), + CompressedData0 = iolist_to_binary(zlib:deflate(Z, <<0:800>>, sync)), + CompressedData = binary:part(CompressedData0, 0, byte_size(CompressedData0) - 4), + MaskedData = do_mask(CompressedData, Mask, <<>>), + Len = byte_size(MaskedData), + true = Len < 8, + ok = gen_tcp:send(Socket, << 1:1, 1:1, 0:2, 1:4, 1:1, Len:7, Mask:32, MaskedData/binary >>), + {ok, << 1:1, 0:3, 8:4, 0:1, 2:7, 1009:16 >>} = gen_tcp:recv(Socket, 0, 6000), + {error, closed} = gen_tcp:recv(Socket, 0, 6000), + ok. + ws_deflate_opts_client_context_takeover(Config) -> doc("Handler is configured with client context takeover enabled."), {ok, _, Headers1} = do_handshake("/ws_deflate_opts?client_context_takeover", @@ -248,6 +267,21 @@ ws_deflate_opts_client_max_window_bits_override(Config) -> = lists:keyfind("sec-websocket-extensions", 1, Headers2), ok. +%% @todo This might be better in an rfc7692_SUITE. +%% +%% 7.1.2.2 +%% If a received extension negotiation offer doesn't have the +%% "client_max_window_bits" extension parameter, the corresponding +%% extension negotiation response to the offer MUST NOT include the +%% "client_max_window_bits" extension parameter. +ws_deflate_opts_client_max_window_bits_only_in_server(Config) -> + doc("Handler is configured with non-default client max window bits but " + "client doesn't send the parameter; compression is disabled."), + {ok, _, Headers} = do_handshake("/ws_deflate_opts?client_max_window_bits", + "Sec-WebSocket-Extensions: permessage-deflate\r\n", Config), + false = lists:keyfind("sec-websocket-extensions", 1, Headers), + ok. + ws_deflate_opts_server_context_takeover(Config) -> doc("Handler is configured with server context takeover enabled."), {ok, _, Headers1} = do_handshake("/ws_deflate_opts?server_context_takeover", diff --git a/test/ws_SUITE_data/ws_max_frame_size.erl b/test/ws_SUITE_data/ws_max_frame_size.erl index 3d81497..76df0b0 100644 --- a/test/ws_SUITE_data/ws_max_frame_size.erl +++ b/test/ws_SUITE_data/ws_max_frame_size.erl @@ -5,7 +5,7 @@ -export([websocket_info/2]). init(Req, State) -> - {cowboy_websocket, Req, State, #{max_frame_size => 8}}. + {cowboy_websocket, Req, State, #{max_frame_size => 8, compress => true}}. websocket_handle({text, Data}, State) -> {[{text, Data}], State}; diff --git a/test/ws_autobahn_SUITE.erl b/test/ws_autobahn_SUITE.erl index 0e12300..58d15fa 100644 --- a/test/ws_autobahn_SUITE.erl +++ b/test/ws_autobahn_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]> +%% 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 diff --git a/test/ws_handler_SUITE.erl b/test/ws_handler_SUITE.erl index ab1ffc8..ab9dbe2 100644 --- a/test/ws_handler_SUITE.erl +++ b/test/ws_handler_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2018-2024, Loïc Hoguin <[email protected]> +%% 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 @@ -296,6 +296,41 @@ websocket_set_options_idle_timeout(Config) -> error(timeout) end. +websocket_set_options_max_frame_size(Config) -> + doc("The max_frame_size option can be modified using the " + "command {set_options, Opts} at runtime."), + ConnPid = gun_open(Config), + StreamRef = gun:ws_upgrade(ConnPid, "/set_options"), + receive + {gun_upgrade, ConnPid, StreamRef, [<<"websocket">>], _} -> + ok; + {gun_response, ConnPid, _, _, Status, Headers} -> + exit({ws_upgrade_failed, Status, Headers}); + {gun_error, ConnPid, StreamRef, Reason} -> + exit({ws_upgrade_failed, Reason}) + after 1000 -> + error(timeout) + end, + %% We first send a 1MB frame to confirm that yes, we can + %% send a frame that large. The default max_frame_size is infinity. + gun:ws_send(ConnPid, StreamRef, {binary, <<0:8000000>>}), + {ws, {binary, <<0:8000000>>}} = gun:await(ConnPid, StreamRef), + %% Trigger the change in max_frame_size. From now on we will + %% only allow frames of up to 1000 bytes. + gun:ws_send(ConnPid, StreamRef, {text, <<"max_frame_size_small">>}), + %% Confirm that we can send frames of up to 1000 bytes. + gun:ws_send(ConnPid, StreamRef, {binary, <<0:8000>>}), + {ws, {binary, <<0:8000>>}} = gun:await(ConnPid, StreamRef), + %% Confirm that sending frames larger than 1000 bytes + %% results in the closing of the connection. + gun:ws_send(ConnPid, StreamRef, {binary, <<0:8008>>}), + receive + {gun_down, ConnPid, _, _, _} -> + ok + after 2000 -> + error(timeout) + end. + websocket_shutdown_reason(Config) -> doc("The command {shutdown_reason, any()} can be used to " "change the shutdown reason of a Websocket connection."), diff --git a/test/ws_perf_SUITE.erl b/test/ws_perf_SUITE.erl index 19d1c31..ff88554 100644 --- a/test/ws_perf_SUITE.erl +++ b/test/ws_perf_SUITE.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2024, Loïc Hoguin <[email protected]> +%% 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 @@ -29,7 +29,7 @@ all() -> groups() -> CommonGroups = cowboy_test:common_groups(ct_helper:all(?MODULE), no_parallel), SubGroups = [G || G = {GN, _, _} <- CommonGroups, - GN =:= http orelse GN =:= h2c], + GN =:= http orelse GN =:= h2c orelse GN =:= http_compress orelse GN =:= h2c_compress], [ {binary, [], SubGroups}, {ascii, [], SubGroups}, @@ -45,24 +45,25 @@ init_per_suite(Config) -> end_per_suite(_Config) -> ok. -init_per_group(Name=http, Config) -> - ct:pal("Websocket over cleartext HTTP/1.1 (~s)", - [init_data_info(Config)]), - cowboy_test:init_http(Name, #{ - env => #{dispatch => init_dispatch(Config)} - }, [{flavor, vanilla}|Config]); -init_per_group(Name=h2c, Config) -> - ct:pal("Websocket over cleartext HTTP/2 (~s)", - [init_data_info(Config)]), - Config1 = cowboy_test:init_http(Name, #{ +init_per_group(Name, Config) when Name =:= http; Name =:= http_compress -> + init_info(Name, Config), + cowboy_test:init_common_groups(Name, Config, ?MODULE); +init_per_group(Name, Config) when Name =:= h2c; Name =:= h2c_compress -> + init_info(Name, Config), + {Flavor, Opts} = case Name of + h2c -> {vanilla, #{}}; + h2c_compress -> {compress, #{stream_handlers => [cowboy_compress_h, cowboy_stream_h]}} + end, + Config1 = cowboy_test:init_http(Name, Opts#{ connection_window_margin_size => 64*1024, enable_connect_protocol => true, env => #{dispatch => init_dispatch(Config)}, max_frame_size_sent => 64*1024, max_frame_size_received => 16384 * 1024 - 1, + max_received_frame_rate => {10_000_000, 1}, stream_window_data_threshold => 1024, stream_window_margin_size => 64*1024 - }, [{flavor, vanilla}|Config]), + }, [{flavor, Flavor}|Config]), lists:keyreplace(protocol, 1, Config1, {protocol, http2}); init_per_group(ascii, Config) -> init_text_data("ascii.txt", Config); @@ -73,11 +74,18 @@ init_per_group(japanese, Config) -> init_per_group(binary, Config) -> [{frame_type, binary}|Config]. -init_data_info(Config) -> - case config(frame_type, Config) of +init_info(Name, Config) -> + DataInfo = case config(frame_type, Config) of text -> config(text_data_filename, Config); binary -> binary - end. + end, + ConnInfo = case Name of + http -> "cleartext HTTP/1.1"; + http_compress -> "cleartext HTTP/1.1 with compression"; + h2c -> "cleartext HTTP/2"; + h2c_compress -> "cleartext HTTP/2 with compression" + end, + ct:pal("Websocket over ~s (~s)", [ConnInfo, DataInfo]). init_text_data(Filename, Config) -> {ok, Text} = file:read_file(filename:join(config(data_dir, Config), Filename)), @@ -95,15 +103,15 @@ end_per_group(Name, _Config) -> init_dispatch(_Config) -> cowboy_router:compile([ {"localhost", [ - {"/ws_echo", ws_echo, []} + {"/ws_echo", ws_echo, []}, + {"/ws_ignore", ws_ignore, []} ]} ]). %% Support functions for testing using Gun. -do_gun_open_ws(Config) -> +do_gun_open_ws(Path, Config) -> ConnPid = gun_open(Config, #{ - tcp_opts => [{nodelay, true}], http2_opts => #{ connection_window_margin_size => 64*1024, max_frame_size_sent => 64*1024, @@ -111,7 +119,9 @@ do_gun_open_ws(Config) -> notify_settings_changed => true, stream_window_data_threshold => 1024, stream_window_margin_size => 64*1024 - } + }, + tcp_opts => [{nodelay, true}], + ws_opts => #{compress => config(flavor, Config) =:= compress} }), case config(protocol, Config) of http -> ok; @@ -119,7 +129,7 @@ do_gun_open_ws(Config) -> {notify, settings_changed, #{enable_connect_protocol := true}} = gun:await(ConnPid, undefined) %% @todo Maybe have a gun:await/1? end, - StreamRef = gun:ws_upgrade(ConnPid, "/ws_echo"), + StreamRef = gun:ws_upgrade(ConnPid, Path), receive {gun_upgrade, ConnPid, StreamRef, [<<"websocket">>], _} -> {ok, ConnPid, StreamRef}; @@ -141,72 +151,140 @@ receive_ws(ConnPid, StreamRef) -> %% Tests. -one_00064KiB(Config) -> +echo_1_00064KiB(Config) -> doc("Send and receive a 64KiB frame."), - do_full(Config, one, 1, 64 * 1024). + do_echo(Config, echo_1, 1, 64 * 1024). -one_00256KiB(Config) -> +echo_1_00256KiB(Config) -> doc("Send and receive a 256KiB frame."), - do_full(Config, one, 1, 256 * 1024). + do_echo(Config, echo_1, 1, 256 * 1024). -one_01024KiB(Config) -> +echo_1_01024KiB(Config) -> doc("Send and receive a 1024KiB frame."), - do_full(Config, one, 1, 1024 * 1024). + do_echo(Config, echo_1, 1, 1024 * 1024). -one_04096KiB(Config) -> +echo_1_04096KiB(Config) -> doc("Send and receive a 4096KiB frame."), - do_full(Config, one, 1, 4096 * 1024). + do_echo(Config, echo_1, 1, 4096 * 1024). %% Minus one because frames can only get so big. -one_16384KiB(Config) -> +echo_1_16384KiB(Config) -> doc("Send and receive a 16384KiB - 1 frame."), - do_full(Config, one, 1, 16384 * 1024 - 1). + do_echo(Config, echo_1, 1, 16384 * 1024 - 1). -repeat_00000B(Config) -> +echo_N_00000B(Config) -> doc("Send and receive a 0B frame 1000 times."), - do_full(Config, repeat, 1000, 0). + do_echo(Config, echo_N, 1000, 0). -repeat_00256B(Config) -> +echo_N_00256B(Config) -> doc("Send and receive a 256B frame 1000 times."), - do_full(Config, repeat, 1000, 256). + do_echo(Config, echo_N, 1000, 256). -repeat_01024B(Config) -> +echo_N_01024B(Config) -> doc("Send and receive a 1024B frame 1000 times."), - do_full(Config, repeat, 1000, 1024). + do_echo(Config, echo_N, 1000, 1024). -repeat_04096B(Config) -> +echo_N_04096B(Config) -> doc("Send and receive a 4096B frame 1000 times."), - do_full(Config, repeat, 1000, 4096). + do_echo(Config, echo_N, 1000, 4096). -repeat_16384B(Config) -> +echo_N_16384B(Config) -> doc("Send and receive a 16384B frame 1000 times."), - do_full(Config, repeat, 1000, 16384). + do_echo(Config, echo_N, 1000, 16384). -%repeat_16384B_10K(Config) -> +%echo_N_16384B_10K(Config) -> % doc("Send and receive a 16384B frame 10000 times."), -% do_full(Config, repeat, 10000, 16384). +% do_echo(Config, echo_N, 10000, 16384). -do_full(Config, What, Num, FrameSize) -> - {ok, ConnPid, StreamRef} = do_gun_open_ws(Config), +do_echo(Config, What, Num, FrameSize) -> + {ok, ConnPid, StreamRef} = do_gun_open_ws("/ws_echo", Config), FrameType = config(frame_type, Config), FrameData = case FrameType of text -> do_text_data(Config, FrameSize); binary -> rand:bytes(FrameSize) end, %% Heat up the processes before doing the real run. -% do_full1(ConnPid, StreamRef, Num, FrameType, FrameData), - {Time, _} = timer:tc(?MODULE, do_full1, [ConnPid, StreamRef, Num, FrameType, FrameData]), +% do_echo_loop(ConnPid, StreamRef, Num, FrameType, FrameData), + {Time, _} = timer:tc(?MODULE, do_echo_loop, [ConnPid, StreamRef, Num, FrameType, FrameData]), do_log("~-6s ~-6s ~6s: ~8bµs", [What, FrameType, do_format_size(FrameSize), Time]), gun:ws_send(ConnPid, StreamRef, close), {ok, close} = receive_ws(ConnPid, StreamRef), gun_down(ConnPid). -do_full1(_, _, 0, _, _) -> +do_echo_loop(_, _, 0, _, _) -> ok; -do_full1(ConnPid, StreamRef, Num, FrameType, FrameData) -> +do_echo_loop(ConnPid, StreamRef, Num, FrameType, FrameData) -> gun:ws_send(ConnPid, StreamRef, {FrameType, FrameData}), {ok, {FrameType, FrameData}} = receive_ws(ConnPid, StreamRef), - do_full1(ConnPid, StreamRef, Num - 1, FrameType, FrameData). + do_echo_loop(ConnPid, StreamRef, Num - 1, FrameType, FrameData). + +send_1_00064KiB(Config) -> + doc("Send a 64KiB frame."), + do_send(Config, send_1, 1, 64 * 1024). + +send_1_00256KiB(Config) -> + doc("Send a 256KiB frame."), + do_send(Config, send_1, 1, 256 * 1024). + +send_1_01024KiB(Config) -> + doc("Send a 1024KiB frame."), + do_send(Config, send_1, 1, 1024 * 1024). + +send_1_04096KiB(Config) -> + doc("Send a 4096KiB frame."), + do_send(Config, send_1, 1, 4096 * 1024). + +%% Minus one because frames can only get so big. +send_1_16384KiB(Config) -> + doc("Send a 16384KiB - 1 frame."), + do_send(Config, send_1, 1, 16384 * 1024 - 1). + +send_N_00000B(Config) -> + doc("Send a 0B frame 10000 times."), + do_send(Config, send_N, 10000, 0). + +send_N_00256B(Config) -> + doc("Send a 256B frame 10000 times."), + do_send(Config, send_N, 10000, 256). + +send_N_01024B(Config) -> + doc("Send a 1024B frame 10000 times."), + do_send(Config, send_N, 10000, 1024). + +send_N_04096B(Config) -> + doc("Send a 4096B frame 10000 times."), + do_send(Config, send_N, 10000, 4096). + +send_N_16384B(Config) -> + doc("Send a 16384B frame 10000 times."), + do_send(Config, send_N, 10000, 16384). + +%send_N_16384B_10K(Config) -> +% doc("Send and receive a 16384B frame 10000 times."), +% do_send(Config, send_N, 10000, 16384). + +do_send(Config, What, Num, FrameSize) -> + {ok, ConnPid, StreamRef} = do_gun_open_ws("/ws_ignore", Config), + FrameType = config(frame_type, Config), + FrameData = case FrameType of + text -> do_text_data(Config, FrameSize); + binary -> rand:bytes(FrameSize) + end, + %% Heat up the processes before doing the real run. +% do_send_loop(ConnPid, StreamRef, Num, FrameType, FrameData), + {Time, _} = timer:tc(?MODULE, do_send_loop, [ConnPid, StreamRef, Num, FrameType, FrameData]), + do_log("~-6s ~-6s ~6s: ~8bµs", [What, FrameType, do_format_size(FrameSize), Time]), + gun:ws_send(ConnPid, StreamRef, close), + {ok, close} = receive_ws(ConnPid, StreamRef), + gun_down(ConnPid). + +do_send_loop(ConnPid, StreamRef, 0, _, _) -> + gun:ws_send(ConnPid, StreamRef, {text, <<"CHECK">>}), + {ok, {text, <<"CHECK">>}} = receive_ws(ConnPid, StreamRef), + ok; +do_send_loop(ConnPid, StreamRef, Num, FrameType, FrameData) -> + gun:ws_send(ConnPid, StreamRef, {FrameType, FrameData}), + do_send_loop(ConnPid, StreamRef, Num - 1, FrameType, FrameData). %% Internal. |