blob: c4f88d841e9afc91e9413af7eff1cd10553a0a68 [file] [log] [blame]
// Copyright (c) 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "quiche/quic/core/qpack/qpack_encoder.h"
#include <limits>
#include <string>
#include "absl/strings/escaping.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "quiche/quic/core/qpack/qpack_instruction_encoder.h"
#include "quiche/quic/core/qpack/value_splitting_header_list.h"
#include "quiche/quic/platform/api/quic_flags.h"
#include "quiche/quic/platform/api/quic_test.h"
#include "quiche/quic/test_tools/qpack/qpack_encoder_peer.h"
#include "quiche/quic/test_tools/qpack/qpack_test_utils.h"
using ::testing::_;
using ::testing::Eq;
using ::testing::Return;
using ::testing::StrictMock;
namespace quic {
namespace test {
namespace {
// A number larger than kMaxBytesBufferedByStream in
// qpack_encoder_stream_sender.cc. Returning this value from NumBytesBuffered()
// will instruct QpackEncoder not to generate any instructions for the encoder
// stream.
constexpr uint64_t kTooManyBytesBuffered = 1024 * 1024;
std::string PrintToString(const testing::TestParamInfo<HuffmanEncoding>& info) {
switch (info.param) {
case HuffmanEncoding::kEnabled:
return "HuffmanEnabled";
case HuffmanEncoding::kDisabled:
return "HuffmanDisabled";
}
QUICHE_NOTREACHED();
return "InvalidValue";
}
// Mock QpackEncoder::DecoderStreamErrorDelegate implementation.
class MockDecoderStreamErrorDelegate
: public QpackEncoder::DecoderStreamErrorDelegate {
public:
~MockDecoderStreamErrorDelegate() override = default;
MOCK_METHOD(void, OnDecoderStreamError,
(QuicErrorCode error_code, absl::string_view error_message),
(override));
};
class QpackEncoderTest : public QuicTestWithParam<HuffmanEncoding> {
protected:
QpackEncoderTest()
: huffman_encoding_(GetParam()),
encoder_(&decoder_stream_error_delegate_, huffman_encoding_,
CookieCrumbling::kEnabled),
encoder_stream_sent_byte_count_(0) {
encoder_.set_qpack_stream_sender_delegate(&encoder_stream_sender_delegate_);
encoder_.SetMaximumBlockedStreams(1);
}
~QpackEncoderTest() override = default;
bool HuffmanEnabled() const {
return huffman_encoding_ == HuffmanEncoding::kEnabled;
}
std::string Encode(const quiche::HttpHeaderBlock& header_list) {
return encoder_.EncodeHeaderList(/* stream_id = */ 1, header_list,
&encoder_stream_sent_byte_count_);
}
const HuffmanEncoding huffman_encoding_;
StrictMock<MockDecoderStreamErrorDelegate> decoder_stream_error_delegate_;
StrictMock<MockQpackStreamSenderDelegate> encoder_stream_sender_delegate_;
QpackEncoder encoder_;
QuicByteCount encoder_stream_sent_byte_count_;
};
INSTANTIATE_TEST_SUITE_P(HuffmanEncoding, QpackEncoderTest,
::testing::ValuesIn({HuffmanEncoding::kEnabled,
HuffmanEncoding::kDisabled}),
PrintToString);
TEST_P(QpackEncoderTest, Empty) {
EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
.WillRepeatedly(Return(0));
quiche::HttpHeaderBlock header_list;
std::string output = Encode(header_list);
std::string expected_output;
ASSERT_TRUE(absl::HexStringToBytes("0000", &expected_output));
EXPECT_EQ(expected_output, output);
}
TEST_P(QpackEncoderTest, EmptyName) {
EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
.WillRepeatedly(Return(0));
quiche::HttpHeaderBlock header_list;
header_list[""] = "foo";
std::string output = Encode(header_list);
std::string expected_output;
if (HuffmanEnabled()) {
ASSERT_TRUE(absl::HexStringToBytes("0000208294e7", &expected_output));
} else {
ASSERT_TRUE(absl::HexStringToBytes("00002003666f6f", &expected_output));
}
EXPECT_EQ(expected_output, output);
}
TEST_P(QpackEncoderTest, EmptyValue) {
EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
.WillRepeatedly(Return(0));
quiche::HttpHeaderBlock header_list;
header_list["foo"] = "";
std::string output = Encode(header_list);
std::string expected_output;
if (HuffmanEnabled()) {
ASSERT_TRUE(absl::HexStringToBytes("00002a94e700", &expected_output));
} else {
ASSERT_TRUE(absl::HexStringToBytes("000023666f6f00", &expected_output));
}
EXPECT_EQ(expected_output, output);
}
TEST_P(QpackEncoderTest, EmptyNameAndValue) {
EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
.WillRepeatedly(Return(0));
quiche::HttpHeaderBlock header_list;
header_list[""] = "";
std::string output = Encode(header_list);
std::string expected_output;
ASSERT_TRUE(absl::HexStringToBytes("00002000", &expected_output));
EXPECT_EQ(expected_output, output);
}
TEST_P(QpackEncoderTest, Simple) {
EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
.WillRepeatedly(Return(0));
quiche::HttpHeaderBlock header_list;
header_list["foo"] = "bar";
std::string output = Encode(header_list);
std::string expected_output;
if (HuffmanEnabled()) {
ASSERT_TRUE(absl::HexStringToBytes("00002a94e703626172", &expected_output));
} else {
ASSERT_TRUE(
absl::HexStringToBytes("000023666f6f03626172", &expected_output));
}
EXPECT_EQ(expected_output, output);
}
TEST_P(QpackEncoderTest, Multiple) {
EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
.WillRepeatedly(Return(0));
quiche::HttpHeaderBlock header_list;
header_list["foo"] = "bar";
// 'Z' would be Huffman encoded to 8 bits, so no Huffman encoding is used.
header_list["ZZZZZZZ"] = std::string(127, 'Z');
std::string output = Encode(header_list);
std::string expected_output_hex;
if (HuffmanEnabled()) {
expected_output_hex =
"0000" // prefix
"2a94e703626172"; // foo: bar
} else {
expected_output_hex =
"0000" // prefix
"23666f6f03626172"; // foo: bar
}
expected_output_hex +=
"27005a5a5a5a5a5a5a" // 7 octet long header name, the smallest number
// that does not fit on a 3-bit prefix.
"7f005a5a5a5a5a5a5a" // 127 octet long header value, the smallest
"5a5a5a5a5a5a5a5a5a" // number that does not fit on a 7-bit prefix.
"5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a"
"5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a"
"5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a"
"5a5a5a5a5a5a5a5a5a";
std::string expected_output;
ASSERT_TRUE(absl::HexStringToBytes(expected_output_hex, &expected_output));
EXPECT_EQ(expected_output, output);
}
TEST_P(QpackEncoderTest, StaticTable) {
EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
.WillRepeatedly(Return(0));
{
quiche::HttpHeaderBlock header_list;
header_list[":method"] = "GET";
header_list["accept-encoding"] = "gzip, deflate, br";
header_list["location"] = "";
std::string output = Encode(header_list);
std::string expected_output;
ASSERT_TRUE(absl::HexStringToBytes("0000d1dfcc", &expected_output));
EXPECT_EQ(expected_output, output);
}
{
quiche::HttpHeaderBlock header_list;
header_list[":method"] = "POST";
header_list["accept-encoding"] = "compress";
header_list["location"] = "foo";
std::string output = Encode(header_list);
std::string expected_output;
if (HuffmanEnabled()) {
ASSERT_TRUE(absl::HexStringToBytes("0000d45f108621e9aec2a11f5c8294e7",
&expected_output));
} else {
ASSERT_TRUE(absl::HexStringToBytes(
"0000d45f1008636f6d70726573735c03666f6f", &expected_output));
}
EXPECT_EQ(expected_output, output);
}
{
quiche::HttpHeaderBlock header_list;
header_list[":method"] = "TRACE";
header_list["accept-encoding"] = "";
std::string output = Encode(header_list);
std::string expected_output;
ASSERT_TRUE(
absl::HexStringToBytes("00005f000554524143455f1000", &expected_output));
EXPECT_EQ(expected_output, output);
}
}
TEST_P(QpackEncoderTest, DecoderStreamError) {
EXPECT_CALL(decoder_stream_error_delegate_,
OnDecoderStreamError(QUIC_QPACK_DECODER_STREAM_INTEGER_TOO_LARGE,
Eq("Encoded integer too large.")));
QpackEncoder encoder(&decoder_stream_error_delegate_, huffman_encoding_,
CookieCrumbling::kEnabled);
encoder.set_qpack_stream_sender_delegate(&encoder_stream_sender_delegate_);
std::string input;
ASSERT_TRUE(absl::HexStringToBytes("ffffffffffffffffffffff", &input));
encoder.decoder_stream_receiver()->Decode(input);
}
TEST_P(QpackEncoderTest, SplitAlongNullCharacter) {
EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
.WillRepeatedly(Return(0));
quiche::HttpHeaderBlock header_list;
header_list["foo"] = absl::string_view("bar\0bar\0baz", 11);
std::string output = Encode(header_list);
std::string expected_output;
if (HuffmanEnabled()) {
ASSERT_TRUE(
absl::HexStringToBytes("0000" // prefix
"2a94e703626172" // foo: bar
"2a94e703626172" // foo: bar
"2a94e70362617a", // foo: baz
&expected_output));
} else {
ASSERT_TRUE(
absl::HexStringToBytes("0000" // prefix
"23666f6f03626172" // foo: bar
"23666f6f03626172" // foo: bar
"23666f6f0362617a", // foo: bar
&expected_output));
}
EXPECT_EQ(expected_output, output);
}
TEST_P(QpackEncoderTest, ZeroInsertCountIncrement) {
// Encoder receives insert count increment with forbidden value 0.
EXPECT_CALL(
decoder_stream_error_delegate_,
OnDecoderStreamError(QUIC_QPACK_DECODER_STREAM_INVALID_ZERO_INCREMENT,
Eq("Invalid increment value 0.")));
encoder_.OnInsertCountIncrement(0);
}
TEST_P(QpackEncoderTest, TooLargeInsertCountIncrement) {
// Encoder receives insert count increment with value that increases Known
// Received Count to a value (one) which is larger than the number of dynamic
// table insertions sent (zero).
EXPECT_CALL(
decoder_stream_error_delegate_,
OnDecoderStreamError(QUIC_QPACK_DECODER_STREAM_IMPOSSIBLE_INSERT_COUNT,
Eq("Increment value 1 raises known received count "
"to 1 exceeding inserted entry count 0")));
encoder_.OnInsertCountIncrement(1);
}
// Regression test for https://crbug.com/1014372.
TEST_P(QpackEncoderTest, InsertCountIncrementOverflow) {
QpackEncoderHeaderTable* header_table =
QpackEncoderPeer::header_table(&encoder_);
// Set dynamic table capacity large enough to hold one entry.
header_table->SetMaximumDynamicTableCapacity(4096);
header_table->SetDynamicTableCapacity(4096);
// Insert one entry into the header table.
header_table->InsertEntry("foo", "bar");
// Receive Insert Count Increment instruction with increment value 1.
encoder_.OnInsertCountIncrement(1);
// Receive Insert Count Increment instruction that overflows the known
// received count. This must result in an error instead of a crash.
EXPECT_CALL(decoder_stream_error_delegate_,
OnDecoderStreamError(
QUIC_QPACK_DECODER_STREAM_INCREMENT_OVERFLOW,
Eq("Insert Count Increment instruction causes overflow.")));
encoder_.OnInsertCountIncrement(std::numeric_limits<uint64_t>::max());
}
TEST_P(QpackEncoderTest, InvalidHeaderAcknowledgement) {
// Encoder receives header acknowledgement for a stream on which no header
// block with dynamic table entries was ever sent.
EXPECT_CALL(
decoder_stream_error_delegate_,
OnDecoderStreamError(QUIC_QPACK_DECODER_STREAM_INCORRECT_ACKNOWLEDGEMENT,
Eq("Header Acknowledgement received for stream 0 "
"with no outstanding header blocks.")));
encoder_.OnHeaderAcknowledgement(/* stream_id = */ 0);
}
TEST_P(QpackEncoderTest, DynamicTable) {
EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
.WillRepeatedly(Return(0));
encoder_.SetMaximumBlockedStreams(1);
encoder_.SetMaximumDynamicTableCapacity(4096);
encoder_.SetDynamicTableCapacity(4096);
quiche::HttpHeaderBlock header_list;
header_list["foo"] = "bar";
header_list.AppendValueOrAddHeader("foo",
"baz"); // name matches dynamic entry
header_list["cookie"] = "baz"; // name matches static entry
// Set Dynamic Table Capacity instruction.
std::string set_dyanamic_table_capacity;
ASSERT_TRUE(absl::HexStringToBytes("3fe11f", &set_dyanamic_table_capacity));
// Insert three entries into the dynamic table.
std::string insert_entries_hex;
if (HuffmanEnabled()) {
insert_entries_hex =
"62" // insert without name reference
"94e7"; // Huffman-encoded literal name "foo"
} else {
insert_entries_hex =
"43" // insert without name reference
"666f6f"; // literal name "foo"
}
insert_entries_hex +=
"03626172" // value "bar"
"80" // insert with name reference, dynamic index 0
"0362617a" // value "baz"
"c5" // insert with name reference, static index 5
"0362617a"; // value "baz"
std::string insert_entries;
ASSERT_TRUE(absl::HexStringToBytes(insert_entries_hex, &insert_entries));
EXPECT_CALL(encoder_stream_sender_delegate_,
WriteStreamData(Eq(
absl::StrCat(set_dyanamic_table_capacity, insert_entries))));
std::string expected_output;
ASSERT_TRUE(absl::HexStringToBytes(
"0400" // prefix
"828180", // dynamic entries with relative index 0, 1, and 2
&expected_output));
EXPECT_EQ(expected_output, Encode(header_list));
EXPECT_EQ(insert_entries.size(), encoder_stream_sent_byte_count_);
}
// There is no room in the dynamic table after inserting the first entry.
TEST_P(QpackEncoderTest, SmallDynamicTable) {
EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
.WillRepeatedly(Return(0));
encoder_.SetMaximumBlockedStreams(1);
encoder_.SetMaximumDynamicTableCapacity(QpackEntry::Size("foo", "bar"));
encoder_.SetDynamicTableCapacity(QpackEntry::Size("foo", "bar"));
quiche::HttpHeaderBlock header_list;
header_list["foo"] = "bar";
header_list.AppendValueOrAddHeader("foo",
"baz"); // name matches dynamic entry
header_list["cookie"] = "baz"; // name matches static entry
header_list["bar"] = "baz"; // no match
// Set Dynamic Table Capacity instruction.
std::string set_dyanamic_table_capacity;
ASSERT_TRUE(absl::HexStringToBytes("3f07", &set_dyanamic_table_capacity));
// Insert one entry into the dynamic table.
std::string insert_entry;
if (HuffmanEnabled()) {
ASSERT_TRUE(
absl::HexStringToBytes("62" // insert without name reference
"94e7" // Huffman-encoded literal name "foo"
"03626172", // value "bar"
&insert_entry));
} else {
ASSERT_TRUE(
absl::HexStringToBytes("43" // insert without name reference
"666f6f" // literal name "foo"
"03626172", // value "bar"
&insert_entry));
}
EXPECT_CALL(encoder_stream_sender_delegate_,
WriteStreamData(
Eq(absl::StrCat(set_dyanamic_table_capacity, insert_entry))));
std::string expected_output;
ASSERT_TRUE(
absl::HexStringToBytes("0200" // prefix
"80" // dynamic entry 0
"40" // reference to dynamic entry 0 name
"0362617a" // with literal value "baz"
"55" // reference to static entry 5 name
"0362617a" // with literal value "baz"
"23626172" // literal name "bar"
"0362617a", // with literal value "baz"
&expected_output));
EXPECT_EQ(expected_output, Encode(header_list));
EXPECT_EQ(insert_entry.size(), encoder_stream_sent_byte_count_);
}
TEST_P(QpackEncoderTest, BlockedStream) {
EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
.WillRepeatedly(Return(0));
encoder_.SetMaximumBlockedStreams(1);
encoder_.SetMaximumDynamicTableCapacity(4096);
encoder_.SetDynamicTableCapacity(4096);
quiche::HttpHeaderBlock header_list1;
header_list1["foo"] = "bar";
// Set Dynamic Table Capacity instruction.
std::string set_dyanamic_table_capacity;
ASSERT_TRUE(absl::HexStringToBytes("3fe11f", &set_dyanamic_table_capacity));
// Insert one entry into the dynamic table.
std::string insert_entry1;
if (HuffmanEnabled()) {
ASSERT_TRUE(
absl::HexStringToBytes("62" // insert without name reference
"94e7" // Huffman-encoded literal name "foo"
"03626172", // value "bar"
&insert_entry1));
} else {
ASSERT_TRUE(
absl::HexStringToBytes("43" // insert without name reference
"666f6f" // literal name "foo"
"03626172", // value "bar"
&insert_entry1));
}
EXPECT_CALL(encoder_stream_sender_delegate_,
WriteStreamData(Eq(
absl::StrCat(set_dyanamic_table_capacity, insert_entry1))));
std::string expected_output;
ASSERT_TRUE(
absl::HexStringToBytes("0200" // prefix
"80", // dynamic entry 0
&expected_output));
EXPECT_EQ(expected_output,
encoder_.EncodeHeaderList(/* stream_id = */ 1, header_list1,
&encoder_stream_sent_byte_count_));
EXPECT_EQ(insert_entry1.size(), encoder_stream_sent_byte_count_);
// Stream 1 is blocked. Stream 2 is not allowed to block.
quiche::HttpHeaderBlock header_list2;
header_list2["foo"] = "bar"; // name and value match dynamic entry
header_list2.AppendValueOrAddHeader("foo",
"baz"); // name matches dynamic entry
header_list2["cookie"] = "baz"; // name matches static entry
header_list2["bar"] = "baz"; // no match
std::string entries;
if (HuffmanEnabled()) {
ASSERT_TRUE(
absl::HexStringToBytes("0000" // prefix
"2a94e7" // literal name "foo"
"03626172" // with literal value "bar"
"2a94e7" // literal name "foo"
"0362617a" // with literal value "baz"
"55" // name of static entry 5
"0362617a" // with literal value "baz"
"23626172" // literal name "bar"
"0362617a", // with literal value "baz"
&entries));
} else {
ASSERT_TRUE(
absl::HexStringToBytes("0000" // prefix
"23666f6f" // literal name "foo"
"03626172" // with literal value "bar"
"23666f6f" // literal name "foo"
"0362617a" // with literal value "baz"
"55" // name of static entry 5
"0362617a" // with literal value "baz"
"23626172" // literal name "bar"
"0362617a", // with literal value "baz"
&entries));
}
EXPECT_EQ(entries,
encoder_.EncodeHeaderList(/* stream_id = */ 2, header_list2,
&encoder_stream_sent_byte_count_));
EXPECT_EQ(0u, encoder_stream_sent_byte_count_);
// Peer acknowledges receipt of one dynamic table entry.
// Stream 1 is no longer blocked.
encoder_.OnInsertCountIncrement(1);
// Insert three entries into the dynamic table.
std::string insert_entries;
ASSERT_TRUE(absl::HexStringToBytes(
"80" // insert with name reference, dynamic index 0
"0362617a" // value "baz"
"c5" // insert with name reference, static index 5
"0362617a" // value "baz"
"43" // insert without name reference
"626172" // name "bar"
"0362617a", // value "baz"
&insert_entries));
EXPECT_CALL(encoder_stream_sender_delegate_,
WriteStreamData(Eq(insert_entries)));
ASSERT_TRUE(
absl::HexStringToBytes("0500" // prefix
"83828180", // dynamic entries
&expected_output));
EXPECT_EQ(expected_output,
encoder_.EncodeHeaderList(/* stream_id = */ 3, header_list2,
&encoder_stream_sent_byte_count_));
EXPECT_EQ(insert_entries.size(), encoder_stream_sent_byte_count_);
// Stream 3 is blocked. Stream 4 is not allowed to block, but it can
// reference already acknowledged dynamic entry 0.
std::string expected2;
if (HuffmanEnabled()) {
ASSERT_TRUE(
absl::HexStringToBytes("0200" // prefix
"80" // dynamic entry 0
"2a94e7" // literal name "foo"
"0362617a" // with literal value "baz"
"55" // name of static entry 5
"0362617a" // with literal value "baz"
"23626172" // literal name "bar"
"0362617a", // with literal value "baz"
&expected2));
} else {
ASSERT_TRUE(
absl::HexStringToBytes("0200" // prefix
"80" // dynamic entry 0
"23666f6f" // literal name "foo"
"0362617a" // with literal value "baz"
"55" // name of static entry 5
"0362617a" // with literal value "baz"
"23626172" // literal name "bar"
"0362617a", // with literal value "baz"
&expected2));
}
EXPECT_EQ(expected2,
encoder_.EncodeHeaderList(/* stream_id = */ 4, header_list2,
&encoder_stream_sent_byte_count_));
EXPECT_EQ(0u, encoder_stream_sent_byte_count_);
// Peer acknowledges receipt of two more dynamic table entries.
// Stream 3 is still blocked.
encoder_.OnInsertCountIncrement(2);
// Stream 5 is not allowed to block, but it can reference already acknowledged
// dynamic entries 0, 1, and 2.
std::string expected3;
ASSERT_TRUE(
absl::HexStringToBytes("0400" // prefix
"828180" // dynamic entries
"23626172" // literal name "bar"
"0362617a", // with literal value "baz"
&expected3));
EXPECT_EQ(expected3,
encoder_.EncodeHeaderList(/* stream_id = */ 5, header_list2,
&encoder_stream_sent_byte_count_));
EXPECT_EQ(0u, encoder_stream_sent_byte_count_);
// Peer acknowledges decoding header block on stream 3.
// Stream 3 is not blocked any longer.
encoder_.OnHeaderAcknowledgement(3);
std::string expected4;
ASSERT_TRUE(
absl::HexStringToBytes("0500" // prefix
"83828180", // dynamic entries
&expected4));
EXPECT_EQ(expected4,
encoder_.EncodeHeaderList(/* stream_id = */ 6, header_list2,
&encoder_stream_sent_byte_count_));
EXPECT_EQ(0u, encoder_stream_sent_byte_count_);
}
TEST_P(QpackEncoderTest, Draining) {
EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
.WillRepeatedly(Return(0));
quiche::HttpHeaderBlock header_list1;
header_list1["one"] = "foo";
header_list1["two"] = "foo";
header_list1["three"] = "foo";
header_list1["four"] = "foo";
header_list1["five"] = "foo";
header_list1["six"] = "foo";
header_list1["seven"] = "foo";
header_list1["eight"] = "foo";
header_list1["nine"] = "foo";
header_list1["ten"] = "foo";
// Make just enough room in the dynamic table for the header list plus the
// first entry duplicated. This will ensure that the oldest entries are
// draining.
uint64_t maximum_dynamic_table_capacity = 0;
for (const auto& header_field : header_list1) {
maximum_dynamic_table_capacity +=
QpackEntry::Size(header_field.first, header_field.second);
}
maximum_dynamic_table_capacity += QpackEntry::Size("one", "foo");
encoder_.SetMaximumDynamicTableCapacity(maximum_dynamic_table_capacity);
encoder_.SetDynamicTableCapacity(maximum_dynamic_table_capacity);
// Set Dynamic Table Capacity instruction and insert ten entries into the
// dynamic table.
EXPECT_CALL(encoder_stream_sender_delegate_, WriteStreamData(_));
std::string expected_output;
ASSERT_TRUE(
absl::HexStringToBytes("0b00" // prefix
"89888786858483828180", // dynamic entries
&expected_output));
EXPECT_EQ(expected_output, Encode(header_list1));
// Entry is identical to oldest one, which is draining. It will be
// duplicated and referenced.
quiche::HttpHeaderBlock header_list2;
header_list2["one"] = "foo";
// Duplicate oldest entry.
ASSERT_TRUE(absl::HexStringToBytes("09", &expected_output));
EXPECT_CALL(encoder_stream_sender_delegate_,
WriteStreamData(Eq(expected_output)));
ASSERT_TRUE(
absl::HexStringToBytes("0c00" // prefix
"80", // most recent dynamic table entry
&expected_output));
EXPECT_EQ(expected_output, Encode(header_list2));
quiche::HttpHeaderBlock header_list3;
// Entry is identical to second oldest one, which is draining. There is no
// room to duplicate, it will be encoded with string literals.
header_list3.AppendValueOrAddHeader("two", "foo");
// Entry has name identical to second oldest one, which is draining. There is
// no room to insert new entry, it will be encoded with string literals.
header_list3.AppendValueOrAddHeader("two", "bar");
std::string entries =
"0000" // prefix
"2374776f"; // literal name "two"
if (HuffmanEnabled()) {
entries += "8294e7"; // literal value "foo"
} else {
entries += "03666f6f"; // literal name "foo"
}
entries +=
"2374776f" // literal name "two"
"03626172"; // literal value "bar"
ASSERT_TRUE(absl::HexStringToBytes(entries, &expected_output));
EXPECT_EQ(expected_output, Encode(header_list3));
}
TEST_P(QpackEncoderTest, DynamicTableCapacityLessThanMaximum) {
encoder_.SetMaximumDynamicTableCapacity(1024);
encoder_.SetDynamicTableCapacity(30);
QpackEncoderHeaderTable* header_table =
QpackEncoderPeer::header_table(&encoder_);
EXPECT_EQ(1024u, header_table->maximum_dynamic_table_capacity());
EXPECT_EQ(30u, header_table->dynamic_table_capacity());
}
TEST_P(QpackEncoderTest, EncoderStreamWritesDisallowedThenAllowed) {
EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
.WillRepeatedly(Return(kTooManyBytesBuffered));
encoder_.SetMaximumBlockedStreams(1);
encoder_.SetMaximumDynamicTableCapacity(4096);
encoder_.SetDynamicTableCapacity(4096);
quiche::HttpHeaderBlock header_list1;
header_list1["foo"] = "bar";
header_list1.AppendValueOrAddHeader("foo", "baz");
header_list1["cookie"] = "baz"; // name matches static entry
// Encoder is not allowed to write on the encoder stream.
// No Set Dynamic Table Capacity or Insert instructions are sent.
// Headers are encoded as string literals.
std::string entries;
if (HuffmanEnabled()) {
ASSERT_TRUE(
absl::HexStringToBytes("0000" // prefix
"2a94e7" // literal name "foo"
"03626172" // with literal value "bar"
"2a94e7" // literal name "foo"
"0362617a" // with literal value "baz"
"55" // name of static entry 5
"0362617a", // with literal value "baz"
&entries));
} else {
ASSERT_TRUE(
absl::HexStringToBytes("0000" // prefix
"23666f6f" // literal name "foo"
"03626172" // with literal value "bar"
"23666f6f" // literal name "foo"
"0362617a" // with literal value "baz"
"55" // name of static entry 5
"0362617a", // with literal value "baz"
&entries));
}
EXPECT_EQ(entries, Encode(header_list1));
EXPECT_EQ(0u, encoder_stream_sent_byte_count_);
// If number of bytes buffered by encoder stream goes under the threshold,
// then QpackEncoder will resume emitting encoder stream instructions.
::testing::Mock::VerifyAndClearExpectations(&encoder_stream_sender_delegate_);
EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
.WillRepeatedly(Return(0));
quiche::HttpHeaderBlock header_list2;
header_list2["foo"] = "bar";
header_list2.AppendValueOrAddHeader("foo",
"baz"); // name matches dynamic entry
header_list2["cookie"] = "baz"; // name matches static entry
// Set Dynamic Table Capacity instruction.
std::string set_dyanamic_table_capacity;
ASSERT_TRUE(absl::HexStringToBytes("3fe11f", &set_dyanamic_table_capacity));
// Insert three entries into the dynamic table.
std::string insert_entries_hex;
if (HuffmanEnabled()) {
insert_entries_hex =
"62" // insert without name reference
"94e7"; // Huffman-encoded literal name "foo"
} else {
insert_entries_hex =
"43" // insert without name reference
"666f6f"; // literal name "foo"
}
insert_entries_hex +=
"03626172" // value "bar"
"80" // insert with name reference, dynamic index 0
"0362617a" // value "baz"
"c5" // insert with name reference, static index 5
"0362617a"; // value "baz"
std::string insert_entries;
ASSERT_TRUE(absl::HexStringToBytes(insert_entries_hex, &insert_entries));
EXPECT_CALL(encoder_stream_sender_delegate_,
WriteStreamData(Eq(
absl::StrCat(set_dyanamic_table_capacity, insert_entries))));
std::string expected_output;
ASSERT_TRUE(absl::HexStringToBytes(
"0400" // prefix
"828180", // dynamic entries with relative index 0, 1, and 2
&expected_output));
EXPECT_EQ(expected_output, Encode(header_list2));
EXPECT_EQ(insert_entries.size(), encoder_stream_sent_byte_count_);
}
TEST_P(QpackEncoderTest, EncoderStreamWritesAllowedThenDisallowed) {
EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
.WillRepeatedly(Return(0));
encoder_.SetMaximumBlockedStreams(1);
encoder_.SetMaximumDynamicTableCapacity(4096);
encoder_.SetDynamicTableCapacity(4096);
quiche::HttpHeaderBlock header_list1;
header_list1["foo"] = "bar";
header_list1.AppendValueOrAddHeader("foo",
"baz"); // name matches dynamic entry
header_list1["cookie"] = "baz"; // name matches static entry
// Set Dynamic Table Capacity instruction.
std::string set_dyanamic_table_capacity;
ASSERT_TRUE(absl::HexStringToBytes("3fe11f", &set_dyanamic_table_capacity));
// Insert three entries into the dynamic table.
std::string insert_entries_hex;
if (HuffmanEnabled()) {
insert_entries_hex =
"62" // insert without name reference
"94e7"; // Huffman-encoded literal name "foo"
} else {
insert_entries_hex =
"43" // insert without name reference
"666f6f"; // literal name "foo"
}
insert_entries_hex +=
"03626172" // value "bar"
"80" // insert with name reference, dynamic index 0
"0362617a" // value "baz"
"c5" // insert with name reference, static index 5
"0362617a"; // value "baz"
std::string insert_entries;
ASSERT_TRUE(absl::HexStringToBytes(insert_entries_hex, &insert_entries));
EXPECT_CALL(encoder_stream_sender_delegate_,
WriteStreamData(Eq(
absl::StrCat(set_dyanamic_table_capacity, insert_entries))));
std::string expected_output;
ASSERT_TRUE(absl::HexStringToBytes(
"0400" // prefix
"828180", // dynamic entries with relative index 0, 1, and 2
&expected_output));
EXPECT_EQ(expected_output, Encode(header_list1));
EXPECT_EQ(insert_entries.size(), encoder_stream_sent_byte_count_);
// If number of bytes buffered by encoder stream goes over the threshold,
// then QpackEncoder will stop emitting encoder stream instructions.
::testing::Mock::VerifyAndClearExpectations(&encoder_stream_sender_delegate_);
EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
.WillRepeatedly(Return(kTooManyBytesBuffered));
quiche::HttpHeaderBlock header_list2;
header_list2["foo"] = "bar"; // matches previously inserted dynamic entry
header_list2["bar"] = "baz";
header_list2["cookie"] = "baz"; // name matches static entry
// Encoder is not allowed to write on the encoder stream.
// No Set Dynamic Table Capacity or Insert instructions are sent.
// Headers are encoded as string literals.
ASSERT_TRUE(
absl::HexStringToBytes("0400" // prefix
"82" // dynamic entry with relative index 0
"23626172" // literal name "bar"
"0362617a" // with literal value "baz"
"80", // dynamic entry with relative index 2
&expected_output));
EXPECT_EQ(expected_output, Encode(header_list2));
EXPECT_EQ(0u, encoder_stream_sent_byte_count_);
}
// Regression test for https://crbug.com/1441880.
TEST_P(QpackEncoderTest, UnackedEntryCannotBeEvicted) {
EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
.WillRepeatedly(Return(0));
encoder_.SetMaximumBlockedStreams(2);
// With 32 byte overhead per entry, only one entry fits in the dynamic table.
encoder_.SetMaximumDynamicTableCapacity(40);
encoder_.SetDynamicTableCapacity(40);
QpackEncoderHeaderTable* header_table =
QpackEncoderPeer::header_table(&encoder_);
EXPECT_EQ(0u, header_table->inserted_entry_count());
EXPECT_EQ(0u, header_table->dropped_entry_count());
quiche::HttpHeaderBlock header_list1;
header_list1["foo"] = "bar";
// Set Dynamic Table Capacity instruction.
std::string set_dyanamic_table_capacity;
ASSERT_TRUE(absl::HexStringToBytes("3f09", &set_dyanamic_table_capacity));
// Insert one entry into the dynamic table.
std::string insert_entries1;
if (HuffmanEnabled()) {
ASSERT_TRUE(
absl::HexStringToBytes("62" // insert without name reference
"94e7" // Huffman-encoded literal name "foo"
"03626172", // value "bar"
&insert_entries1));
} else {
ASSERT_TRUE(
absl::HexStringToBytes("43" // insert without name reference
"666f6f" // literal name "foo"
"03626172", // value "bar"
&insert_entries1));
}
EXPECT_CALL(encoder_stream_sender_delegate_,
WriteStreamData(Eq(
absl::StrCat(set_dyanamic_table_capacity, insert_entries1))));
std::string expected_output;
ASSERT_TRUE(
absl::HexStringToBytes("0200" // prefix
"80", // dynamic entry with relative index 0
&expected_output));
EXPECT_EQ(expected_output,
encoder_.EncodeHeaderList(/* stream_id = */ 1, header_list1,
&encoder_stream_sent_byte_count_));
EXPECT_EQ(1u, header_table->inserted_entry_count());
EXPECT_EQ(0u, header_table->dropped_entry_count());
encoder_.OnStreamCancellation(/* stream_id = */ 1);
// At this point, entry 0 has no references to it, because stream 1 is
// cancelled. However, this entry is unacknowledged, therefore it must not be
// evicted according to RFC 9204 Section 2.1.1.
quiche::HttpHeaderBlock header_list2;
header_list2["bar"] = "baz";
ASSERT_TRUE(
absl::HexStringToBytes("0000" // prefix
"23626172" // literal name "bar"
"0362617a", // literal value "baz"
&expected_output));
EXPECT_EQ(expected_output,
encoder_.EncodeHeaderList(/* stream_id = */ 2, header_list2,
&encoder_stream_sent_byte_count_));
EXPECT_EQ(1u, header_table->inserted_entry_count());
EXPECT_EQ(0u, header_table->dropped_entry_count());
}
// Header name and value match an entry in the dynamic table, but that entry
// cannot be used. If there is an entry with matching name in the static table,
// use that.
TEST_P(QpackEncoderTest, UseStaticTableNameOnlyMatch) {
EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
.WillRepeatedly(Return(0));
encoder_.SetMaximumBlockedStreams(2);
encoder_.SetMaximumDynamicTableCapacity(4096);
encoder_.SetDynamicTableCapacity(4096);
quiche::HttpHeaderBlock header_list;
header_list[":method"] = "bar";
// Set Dynamic Table Capacity instruction.
std::string set_dyanamic_table_capacity;
ASSERT_TRUE(absl::HexStringToBytes("3fe11f", &set_dyanamic_table_capacity));
// Insert one entry into the dynamic table.
std::string insert_entry1;
ASSERT_TRUE(
absl::HexStringToBytes("cf" // insert with name of static table entry 15
"03626172", // literal value "bar"
&insert_entry1));
EXPECT_CALL(encoder_stream_sender_delegate_,
WriteStreamData(Eq(
absl::StrCat(set_dyanamic_table_capacity, insert_entry1))));
std::string expected_output;
ASSERT_TRUE(
absl::HexStringToBytes("0200" // prefix
"80", // dynamic entry 0
&expected_output));
EXPECT_EQ(expected_output,
encoder_.EncodeHeaderList(/* stream_id = */ 1, header_list,
&encoder_stream_sent_byte_count_));
EXPECT_EQ(insert_entry1.size(), encoder_stream_sent_byte_count_);
// Stream 2 uses the same dynamic entry.
EXPECT_EQ(expected_output,
encoder_.EncodeHeaderList(/* stream_id = */ 2, header_list,
&encoder_stream_sent_byte_count_));
EXPECT_EQ(0u, encoder_stream_sent_byte_count_);
// Streams 1 and 2 are blocked, therefore stream 3 is not allowed to refer to
// the existing dynamic table entry, nor to add a new entry to the dynamic
// table.
ASSERT_TRUE(
absl::HexStringToBytes("0000" // prefix
"5f00" // name reference to static table entry 15
"03626172", // literal value "bar"
&expected_output));
EXPECT_EQ(expected_output,
encoder_.EncodeHeaderList(/* stream_id = */ 3, header_list,
&encoder_stream_sent_byte_count_));
}
// Header name and value match an entry in the dynamic table, but that entry
// cannot be used. If there is an entry with matching name in the dynamic table
// that can be used, do so.
TEST_P(QpackEncoderTest, UseDynamicTableNameOnlyMatch) {
EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
.WillRepeatedly(Return(0));
quiche::HttpHeaderBlock header_list1;
header_list1["one"] = "foo";
header_list1["two"] = "foo";
header_list1["three"] = "foo";
header_list1["four"] = "foo";
header_list1["five"] = "foo";
header_list1["six"] = "foo";
header_list1["seven"] = "foo";
header_list1["eight"] = "foo";
header_list1["nine"] = "foo";
header_list1["ten"] = "foo";
// Make just enough room in the dynamic table for the header list,
// plus another entry using the name of the first one,
// This will ensure that the oldest entries are draining.
uint64_t maximum_dynamic_table_capacity = 0;
for (const auto& header_field : header_list1) {
maximum_dynamic_table_capacity +=
QpackEntry::Size(header_field.first, header_field.second);
}
maximum_dynamic_table_capacity += QpackEntry::Size("one", "bar");
encoder_.SetMaximumDynamicTableCapacity(maximum_dynamic_table_capacity);
encoder_.SetDynamicTableCapacity(maximum_dynamic_table_capacity);
// Set Dynamic Table Capacity instruction and insert ten entries into the
// dynamic table.
EXPECT_CALL(encoder_stream_sender_delegate_, WriteStreamData(_));
std::string expected_output;
ASSERT_TRUE(
absl::HexStringToBytes("0b00" // prefix
"89888786858483828180", // dynamic entries
&expected_output));
EXPECT_EQ(expected_output, Encode(header_list1));
// Entry has the same name as the first one.
quiche::HttpHeaderBlock header_list2;
header_list2["one"] = "bar";
ASSERT_TRUE(absl::HexStringToBytes(
"89" // insert entry with same name as dynamic table entry 9
"03626172", // and literal value "bar"
&expected_output));
EXPECT_CALL(encoder_stream_sender_delegate_,
WriteStreamData(Eq(expected_output)));
ASSERT_TRUE(
absl::HexStringToBytes("0c00" // prefix
"80", // most recent dynamic table entry
&expected_output));
EXPECT_EQ(expected_output, Encode(header_list2));
// Entry is identical to the first one, which is draining, and has the same
// name but different value as the last one, which is not draining.
quiche::HttpHeaderBlock header_list3;
header_list3["one"] = "foo";
// Entry matches name and value of oldest dynamic table entry, which cannot be
// used. Use the name of the most recent dynamic table entry instead, and
// encode value as string literal.
if (HuffmanEnabled()) {
ASSERT_TRUE(
absl::HexStringToBytes("0c00" // prefix
"40" // name as dynamic table entry 0
"8294e7", // Huffman-encoded literal value "foo"
&expected_output));
} else {
ASSERT_TRUE(
absl::HexStringToBytes("0c00" // prefix
"40" // name as dynamic table entry 0
"03666f6f", // literal value "foo"
&expected_output));
}
EXPECT_EQ(expected_output, Encode(header_list3));
}
TEST_P(QpackEncoderTest, CookieCrumblingEnabledNoDynamicTable) {
EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
.WillRepeatedly(Return(0));
quiche::HttpHeaderBlock header_list;
header_list["cookie"] = "foo; bar";
std::string expected_output;
if (HuffmanEnabled()) {
ASSERT_TRUE(
absl::HexStringToBytes("0000" // prefix
"55" // name of static entry 5
"8294e7" // with literal value "bar"
"55" // name of static entry 5
"03626172", // with literal value "bar"
&expected_output));
} else {
ASSERT_TRUE(
absl::HexStringToBytes("0000" // prefix
"55" // name of static entry 5
"03666f6f" // with literal value "foo"
"55" // name of static entry 5
"03626172", // with literal value "bar"
&expected_output));
}
EXPECT_EQ(expected_output, Encode(header_list));
EXPECT_EQ(0u, encoder_stream_sent_byte_count_);
}
TEST_P(QpackEncoderTest, CookieCrumblingEnabledDynamicTable) {
EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
.WillRepeatedly(Return(0));
encoder_.SetMaximumBlockedStreams(1);
encoder_.SetMaximumDynamicTableCapacity(4096);
encoder_.SetDynamicTableCapacity(4096);
quiche::HttpHeaderBlock header_list;
header_list["cookie"] = "foo; bar";
// Set Dynamic Table Capacity instruction.
std::string set_dyanamic_table_capacity;
ASSERT_TRUE(absl::HexStringToBytes("3fe11f", &set_dyanamic_table_capacity));
// Insert entries into the dynamic table.
std::string insert_entries;
if (HuffmanEnabled()) {
ASSERT_TRUE(absl::HexStringToBytes(
"c5" // insert with name reference, static index 5
"8294e7" // with literal value "foo"
"c5" // insert with name reference, static index 5
"03626172", // with literal value "bar"
&insert_entries));
} else {
ASSERT_TRUE(absl::HexStringToBytes(
"c5" // insert with name reference, static index 5
"03666f6f" // with literal value "foo"
"c5" // insert with name reference, static index 5
"03626172", // with literal value "bar"
&insert_entries));
}
EXPECT_CALL(encoder_stream_sender_delegate_,
WriteStreamData(Eq(
absl::StrCat(set_dyanamic_table_capacity, insert_entries))));
std::string expected_output;
ASSERT_TRUE(
absl::HexStringToBytes("0300" // prefix
"81" // dynamic entry with relative index 0
"80", // dynamic entry with relative index 1
&expected_output));
EXPECT_EQ(expected_output, Encode(header_list));
EXPECT_EQ(insert_entries.size(), encoder_stream_sent_byte_count_);
}
TEST_P(QpackEncoderTest, CookieCrumblingDisabledNoDynamicTable) {
QpackEncoder encoder(&decoder_stream_error_delegate_, huffman_encoding_,
CookieCrumbling::kDisabled);
EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
.WillRepeatedly(Return(0));
quiche::HttpHeaderBlock header_list;
header_list["cookie"] = "foo; bar";
std::string expected_output;
if (HuffmanEnabled()) {
ASSERT_TRUE(absl::HexStringToBytes(
"0000" // prefix
"55" // name of static entry 5
"8694e7fb5231d9", // with literal value "foo; bar"
&expected_output));
} else {
ASSERT_TRUE(absl::HexStringToBytes(
"0000" // prefix
"55" // name of static entry 5
"08666f6f3b20626172", // with literal value "foo; bar"
&expected_output));
}
EXPECT_EQ(expected_output,
encoder.EncodeHeaderList(/* stream_id = */ 1, header_list,
&encoder_stream_sent_byte_count_));
EXPECT_EQ(0u, encoder_stream_sent_byte_count_);
}
TEST_P(QpackEncoderTest, CookieCrumblingDisabledDynamicTable) {
QpackEncoder encoder(&decoder_stream_error_delegate_, huffman_encoding_,
CookieCrumbling::kDisabled);
encoder.SetMaximumBlockedStreams(1);
encoder.set_qpack_stream_sender_delegate(&encoder_stream_sender_delegate_);
EXPECT_CALL(encoder_stream_sender_delegate_, NumBytesBuffered())
.WillRepeatedly(Return(0));
encoder.SetMaximumBlockedStreams(1);
encoder.SetMaximumDynamicTableCapacity(4096);
encoder.SetDynamicTableCapacity(4096);
quiche::HttpHeaderBlock header_list;
header_list["cookie"] = "foo; bar";
// Set Dynamic Table Capacity instruction.
std::string set_dyanamic_table_capacity;
ASSERT_TRUE(absl::HexStringToBytes("3fe11f", &set_dyanamic_table_capacity));
// Insert entries into the dynamic table.
std::string insert_entries;
if (HuffmanEnabled()) {
ASSERT_TRUE(absl::HexStringToBytes(
"c5" // insert with name reference, static index 5
"8694e7fb5231d9", // with literal value "foo; bar"
&insert_entries));
} else {
ASSERT_TRUE(absl::HexStringToBytes(
"c5" // insert with name reference, static index 5
"08666f6f3b20626172", // with literal value "foo; bar"
&insert_entries));
}
EXPECT_CALL(encoder_stream_sender_delegate_,
WriteStreamData(Eq(
absl::StrCat(set_dyanamic_table_capacity, insert_entries))));
std::string expected_output;
ASSERT_TRUE(
absl::HexStringToBytes("0200" // prefix
"80", // dynamic entry with relative index 0
&expected_output));
EXPECT_EQ(expected_output,
encoder.EncodeHeaderList(/* stream_id = */ 1, header_list,
&encoder_stream_sent_byte_count_));
EXPECT_EQ(insert_entries.size(), encoder_stream_sent_byte_count_);
}
} // namespace
} // namespace test
} // namespace quic