aboutsummaryrefslogtreecommitdiffstats
path: root/test/rfc9204_SUITE.erl
diff options
context:
space:
mode:
Diffstat (limited to 'test/rfc9204_SUITE.erl')
-rw-r--r--test/rfc9204_SUITE.erl357
1 files changed, 357 insertions, 0 deletions
diff --git a/test/rfc9204_SUITE.erl b/test/rfc9204_SUITE.erl
new file mode 100644
index 0000000..e8defd2
--- /dev/null
+++ b/test/rfc9204_SUITE.erl
@@ -0,0 +1,357 @@
+%% Copyright (c) 2024, 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(rfc9204_SUITE).
+-compile(export_all).
+-compile(nowarn_export_all).
+
+-import(ct_helper, [config/2]).
+-import(ct_helper, [doc/1]).
+
+-ifdef(COWBOY_QUICER).
+
+-include_lib("quicer/include/quicer.hrl").
+
+all() ->
+ [{group, h3}].
+
+groups() ->
+ %% @todo Enable parallel tests but for this issues in the
+ %% QUIC accept loop need to be figured out (can't connect
+ %% concurrently somehow, no backlog?).
+ [{h3, [], ct_helper:all(?MODULE)}].
+
+init_per_group(Name = h3, Config) ->
+ cowboy_test:init_http3(Name, #{
+ env => #{dispatch => cowboy_router:compile(init_routes(Config))}
+ }, Config).
+
+end_per_group(Name, _) ->
+ cowboy_test:stop_group(Name).
+
+init_routes(_) -> [
+ {"localhost", [
+ {"/", hello_h, []}
+ ]}
+].
+
+%% Encoder.
+
+%% 2.1
+%% QPACK preserves the ordering of field lines within
+%% each field section. An encoder MUST emit field
+%% representations in the order they appear in the
+%% input field section.
+
+%% 2.1.1
+%% If the dynamic table does not contain enough room
+%% for a new entry without evicting other entries,
+%% and the entries that would be evicted are not evictable,
+%% the encoder MUST NOT insert that entry into the dynamic
+%% table (including duplicates of existing entries).
+%% In order to avoid this, an encoder that uses the
+%% dynamic table has to keep track of each dynamic
+%% table entry referenced by each field section until
+%% those representations are acknowledged by the decoder;
+%% see Section 4.4.1.
+
+%% 2.1.2
+%% The decoder specifies an upper bound on the number
+%% of streams that can be blocked using the
+%% SETTINGS_QPACK_BLOCKED_STREAMS setting; see Section 5.
+%% An encoder MUST limit the number of streams that could
+%% become blocked to the value of SETTINGS_QPACK_BLOCKED_STREAMS
+%% at all times. If a decoder encounters more blocked streams
+%% than it promised to support, it MUST treat this as a
+%% connection error of type QPACK_DECOMPRESSION_FAILED.
+
+%% 2.1.3
+%% To avoid these deadlocks, an encoder SHOULD NOT
+%% write an instruction unless sufficient stream and
+%% connection flow-control credit is available for
+%% the entire instruction.
+
+%% Decoder.
+
+%% 2.2
+%% The decoder MUST emit field lines in the order their
+%% representations appear in the encoded field section.
+
+%% 2.2.1
+%% While blocked, encoded field section data SHOULD
+%% remain in the blocked stream's flow-control window.
+
+%% If it encounters a Required Insert Count smaller than
+%% expected, it MUST treat this as a connection error of
+%% type QPACK_DECOMPRESSION_FAILED; see Section 2.2.3.
+
+%% If it encounters a Required Insert Count larger than
+%% expected, it MAY treat this as a connection error of
+%% type QPACK_DECOMPRESSION_FAILED.
+
+%% After the decoder finishes decoding a field section
+%% encoded using representations containing dynamic table
+%% references, it MUST emit a Section Acknowledgment
+%% instruction (Section 4.4.1).
+
+%% 2.2.2.2
+%% A decoder with a maximum dynamic table capacity
+%% (Section 3.2.3) equal to zero MAY omit sending Stream
+%% Cancellations, because the encoder cannot have any
+%% dynamic table references.
+
+%% 2.2.3
+%% If the decoder encounters a reference in a field line
+%% representation to a dynamic table entry that has already
+%% been evicted or that has an absolute index greater than
+%% or equal to the declared Required Insert Count (Section 4.5.1),
+%% it MUST treat this as a connection error of type
+%% QPACK_DECOMPRESSION_FAILED.
+
+%% If the decoder encounters a reference in an encoder
+%% instruction to a dynamic table entry that has already
+%% been evicted, it MUST treat this as a connection error
+%% of type QPACK_ENCODER_STREAM_ERROR.
+
+%% Static table.
+
+%% 3.1
+%% When the decoder encounters an invalid static table index
+%% in a field line representation, it MUST treat this as a
+%% connection error of type QPACK_DECOMPRESSION_FAILED.
+%%
+%% If this index is received on the encoder stream, this
+%% MUST be treated as a connection error of type
+%% QPACK_ENCODER_STREAM_ERROR.
+
+%% Dynamic table.
+
+%% 3.2
+%% The dynamic table can contain duplicate entries
+%% (i.e., entries with the same name and same value).
+%% Therefore, duplicate entries MUST NOT be treated
+%% as an error by the decoder.
+
+%% 3.2.2
+%% The encoder MUST NOT cause a dynamic table entry to be
+%% evicted unless that entry is evictable; see Section 2.1.1.
+
+%% It is an error if the encoder attempts to add an entry
+%% that is larger than the dynamic table capacity; the
+%% decoder MUST treat this as a connection error of type
+%% QPACK_ENCODER_STREAM_ERROR.
+
+%% 3.2.3
+%% The encoder MUST NOT set a dynamic table capacity that
+%% exceeds this maximum, but it can choose to use a lower
+%% dynamic table capacity; see Section 4.3.1.
+
+%% When the client's 0-RTT value of the SETTING is zero,
+%% the server MAY set it to a non-zero value in its SETTINGS
+%% frame. If the remembered value is non-zero, the server
+%% MUST send the same non-zero value in its SETTINGS frame.
+%% If it specifies any other value, or omits
+%% SETTINGS_QPACK_MAX_TABLE_CAPACITY from SETTINGS,
+%% the encoder must treat this as a connection error of
+%% type QPACK_DECODER_STREAM_ERROR.
+
+%% When the maximum table capacity is zero, the encoder
+%% MUST NOT insert entries into the dynamic table and
+%% MUST NOT send any encoder instructions on the encoder stream.
+
+%% Wire format.
+
+%% 4.1.1
+%% QPACK implementations MUST be able to decode integers
+%% up to and including 62 bits long.
+
+%% Encoder and decoder streams.
+
+decoder_reject_multiple(Config) ->
+ doc("Endpoints must not create multiple decoder streams. (RFC9204 4.2)"),
+ rfc9114_SUITE:do_critical_reject_multiple(Config, <<3>>).
+
+encoder_reject_multiple(Config) ->
+ doc("Endpoints must not create multiple encoder streams. (RFC9204 4.2)"),
+ rfc9114_SUITE:do_critical_reject_multiple(Config, <<2>>).
+
+%% 4.2
+%% The sender MUST NOT close either of these streams,
+%% and the receiver MUST NOT request that the sender close
+%% either of these streams. Closure of either unidirectional
+%% stream type MUST be treated as a connection error of type
+%% H3_CLOSED_CRITICAL_STREAM.
+
+decoder_local_closed_abort(Config) ->
+ doc("Endpoints must not close the decoder stream. (RFC9204 4.2)"),
+ rfc9114_SUITE:do_critical_local_closed_abort(Config, <<3>>).
+
+decoder_local_closed_graceful(Config) ->
+ doc("Endpoints must not close the decoder stream. (RFC9204 4.2)"),
+ rfc9114_SUITE:do_critical_local_closed_graceful(Config, <<3>>).
+
+decoder_remote_closed_abort(Config) ->
+ doc("Endpoints must not close the decoder stream. (RFC9204 4.2)"),
+ #{conn := Conn} = rfc9114_SUITE:do_connect(Config, #{peer_unidi_stream_count => 3}),
+ {ok, #{decoder := StreamRef}} = do_wait_unidi_streams(Conn, #{}),
+ %% Close the control stream.
+ quicer:async_shutdown_stream(StreamRef, ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT, 0),
+ %% The connection should have been closed.
+ #{reason := h3_closed_critical_stream} = rfc9114_SUITE:do_wait_connection_closed(Conn),
+ ok.
+
+encoder_local_closed_abort(Config) ->
+ doc("Endpoints must not close the encoder stream. (RFC9204 4.2)"),
+ rfc9114_SUITE:do_critical_local_closed_abort(Config, <<2>>).
+
+encoder_local_closed_graceful(Config) ->
+ doc("Endpoints must not close the encoder stream. (RFC9204 4.2)"),
+ rfc9114_SUITE:do_critical_local_closed_graceful(Config, <<2>>).
+
+encoder_remote_closed_abort(Config) ->
+ doc("Endpoints must not close the encoder stream. (RFC9204 4.2)"),
+ #{conn := Conn} = rfc9114_SUITE:do_connect(Config, #{peer_unidi_stream_count => 3}),
+ {ok, #{encoder := StreamRef}} = do_wait_unidi_streams(Conn, #{}),
+ %% Close the control stream.
+ quicer:async_shutdown_stream(StreamRef, ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT, 0),
+ %% The connection should have been closed.
+ #{reason := h3_closed_critical_stream} = rfc9114_SUITE:do_wait_connection_closed(Conn),
+ ok.
+
+do_wait_unidi_streams(_, Acc=#{decoder := _, encoder := _}) ->
+ {ok, Acc};
+do_wait_unidi_streams(Conn, Acc) ->
+ receive
+ {quic, new_stream, StreamRef, #{flags := Flags}} ->
+ ok = quicer:setopt(StreamRef, active, true),
+ true = quicer:is_unidirectional(Flags),
+ receive {quic, <<TypeValue>>, StreamRef, _} ->
+ Type = case TypeValue of
+ 2 -> encoder;
+ 3 -> decoder
+ end,
+ do_wait_unidi_streams(Conn, Acc#{Type => StreamRef})
+ after 5000 ->
+ {error, timeout}
+ end
+ after 5000 ->
+ {error, timeout}
+ end.
+
+%% An endpoint MAY avoid creating an encoder stream if it will
+%% not be used (for example, if its encoder does not wish to
+%% use the dynamic table or if the maximum size of the dynamic
+%% table permitted by the peer is zero).
+
+%% An endpoint MAY avoid creating a decoder stream if its
+%% decoder sets the maximum capacity of the dynamic table to zero.
+
+%% An endpoint MUST allow its peer to create an encoder stream
+%% and a decoder stream even if the connection's settings
+%% prevent their use.
+
+%% Encoder instructions.
+
+%% 4.3.1
+%% The new capacity MUST be lower than or equal to the limit
+%% described in Section 3.2.3. In HTTP/3, this limit is the
+%% value of the SETTINGS_QPACK_MAX_TABLE_CAPACITY parameter
+%% (Section 5) received from the decoder. The decoder MUST
+%% treat a new dynamic table capacity value that exceeds this
+%% limit as a connection error of type QPACK_ENCODER_STREAM_ERROR.
+
+%% Reducing the dynamic table capacity can cause entries to be
+%% evicted; see Section 3.2.2. This MUST NOT cause the eviction
+%% of entries that are not evictable; see Section 2.1.1.
+
+%% Decoder instructions.
+
+%% 4.4.1
+%% If an encoder receives a Section Acknowledgment instruction
+%% referring to a stream on which every encoded field section
+%% with a non-zero Required Insert Count has already been
+%% acknowledged, this MUST be treated as a connection error
+%% of type QPACK_DECODER_STREAM_ERROR.
+
+%% 4.4.3
+%% An encoder that receives an Increment field equal to zero,
+%% or one that increases the Known Received Count beyond what
+%% the encoder has sent, MUST treat this as a connection error
+%% of type QPACK_DECODER_STREAM_ERROR.
+
+%% Field line representation.
+
+%% 4.5.1.1
+%% If the decoder encounters a value of EncodedInsertCount that
+%% could not have been produced by a conformant encoder, it MUST
+%% treat this as a connection error of type QPACK_DECOMPRESSION_FAILED.
+
+%% 4.5.1.2
+%% The value of Base MUST NOT be negative. Though the protocol
+%% might operate correctly with a negative Base using post-Base
+%% indexing, it is unnecessary and inefficient. An endpoint MUST
+%% treat a field block with a Sign bit of 1 as invalid if the
+%% value of Required Insert Count is less than or equal to the
+%% value of Delta Base.
+
+%% 4.5.4
+%% When the 'N' bit is set, the encoded field line MUST always
+%% be encoded with a literal representation. In particular,
+%% when a peer sends a field line that it received represented
+%% as a literal field line with the 'N' bit set, it MUST use a
+%% literal representation to forward this field line. This bit
+%% is intended for protecting field values that are not to be
+%% put at risk by compressing them; see Section 7.1 for more details.
+
+%% Configuration.
+
+%% 5
+%% SETTINGS_QPACK_MAX_TABLE_CAPACITY
+%% SETTINGS_QPACK_BLOCKED_STREAMS
+
+%% Security considerations.
+
+%% 7.1.2
+%% (security if used as a proxy merging many connections into one)
+%% An ideal solution segregates access to the dynamic table
+%% based on the entity that is constructing the message.
+%% Field values that are added to the table are attributed
+%% to an entity, and only the entity that created a particular
+%% value can extract that value.
+
+%% 7.1.3
+%% An intermediary MUST NOT re-encode a value that uses a
+%% literal representation with the 'N' bit set with another
+%% representation that would index it. If QPACK is used for
+%% re-encoding, a literal representation with the 'N' bit set
+%% MUST be used. If HPACK is used for re-encoding, the
+%% never-indexed literal representation (see Section 6.2.3
+%% of [RFC7541]) MUST be used.
+
+%% 7.4
+%% An implementation has to set a limit for the values it
+%% accepts for integers, as well as for the encoded length;
+%% see Section 4.1.1. In the same way, it has to set a limit
+%% to the length it accepts for string literals; see Section 4.1.2.
+%% These limits SHOULD be large enough to process the largest
+%% individual field the HTTP implementation can be configured
+%% to accept.
+
+%% If an implementation encounters a value larger than it is
+%% able to decode, this MUST be treated as a stream error of
+%% type QPACK_DECOMPRESSION_FAILED if on a request stream or
+%% a connection error of the appropriate type if on the
+%% encoder or decoder stream.
+
+-endif.