%% 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(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.