Project import generated by Copybara. PiperOrigin-RevId: 224614037 Change-Id: I14e53449d4aeccb328f86828c76b5f09dea0d4b8
diff --git a/http2/hpack/decoder/hpack_block_collector.cc b/http2/hpack/decoder/hpack_block_collector.cc new file mode 100644 index 0000000..5b2c6b7 --- /dev/null +++ b/http2/hpack/decoder/hpack_block_collector.cc
@@ -0,0 +1,151 @@ +// Copyright 2016 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_block_collector.h" + +#include <algorithm> +#include <memory> + +#include "base/logging.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_test_helpers.h" + +using ::testing::AssertionResult; +using ::testing::AssertionSuccess; + +namespace http2 { +namespace test { + +HpackBlockCollector::HpackBlockCollector() = default; +HpackBlockCollector::HpackBlockCollector(const HpackBlockCollector& other) + : pending_entry_(other.pending_entry_), entries_(other.entries_) {} +HpackBlockCollector::~HpackBlockCollector() = default; + +void HpackBlockCollector::OnIndexedHeader(size_t index) { + pending_entry_.OnIndexedHeader(index); + PushPendingEntry(); +} +void HpackBlockCollector::OnDynamicTableSizeUpdate(size_t size) { + pending_entry_.OnDynamicTableSizeUpdate(size); + PushPendingEntry(); +} +void HpackBlockCollector::OnStartLiteralHeader(HpackEntryType header_type, + size_t maybe_name_index) { + pending_entry_.OnStartLiteralHeader(header_type, maybe_name_index); +} +void HpackBlockCollector::OnNameStart(bool huffman_encoded, size_t len) { + pending_entry_.OnNameStart(huffman_encoded, len); +} +void HpackBlockCollector::OnNameData(const char* data, size_t len) { + pending_entry_.OnNameData(data, len); +} +void HpackBlockCollector::OnNameEnd() { + pending_entry_.OnNameEnd(); +} +void HpackBlockCollector::OnValueStart(bool huffman_encoded, size_t len) { + pending_entry_.OnValueStart(huffman_encoded, len); +} +void HpackBlockCollector::OnValueData(const char* data, size_t len) { + pending_entry_.OnValueData(data, len); +} +void HpackBlockCollector::OnValueEnd() { + pending_entry_.OnValueEnd(); + PushPendingEntry(); +} + +void HpackBlockCollector::PushPendingEntry() { + EXPECT_TRUE(pending_entry_.IsComplete()); + DVLOG(2) << "PushPendingEntry: " << pending_entry_; + entries_.push_back(pending_entry_); + EXPECT_TRUE(entries_.back().IsComplete()); + pending_entry_.Clear(); +} +void HpackBlockCollector::Clear() { + pending_entry_.Clear(); + entries_.clear(); +} + +void HpackBlockCollector::ExpectIndexedHeader(size_t index) { + entries_.push_back( + HpackEntryCollector(HpackEntryType::kIndexedHeader, index)); +} +void HpackBlockCollector::ExpectDynamicTableSizeUpdate(size_t size) { + entries_.push_back( + HpackEntryCollector(HpackEntryType::kDynamicTableSizeUpdate, size)); +} +void HpackBlockCollector::ExpectNameIndexAndLiteralValue( + HpackEntryType type, + size_t index, + bool value_huffman, + const Http2String& value) { + entries_.push_back(HpackEntryCollector(type, index, value_huffman, value)); +} +void HpackBlockCollector::ExpectLiteralNameAndValue(HpackEntryType type, + bool name_huffman, + const Http2String& name, + bool value_huffman, + const Http2String& value) { + entries_.push_back( + HpackEntryCollector(type, name_huffman, name, value_huffman, value)); +} + +void HpackBlockCollector::ShuffleEntries(Http2Random* rng) { + std::shuffle(entries_.begin(), entries_.end(), *rng); +} + +void HpackBlockCollector::AppendToHpackBlockBuilder( + HpackBlockBuilder* hbb) const { + CHECK(IsNotPending()); + for (const auto& entry : entries_) { + entry.AppendToHpackBlockBuilder(hbb); + } +} + +AssertionResult HpackBlockCollector::ValidateSoleIndexedHeader( + size_t ndx) const { + VERIFY_TRUE(pending_entry_.IsClear()); + VERIFY_EQ(1u, entries_.size()); + VERIFY_TRUE(entries_.front().ValidateIndexedHeader(ndx)); + return AssertionSuccess(); +} +AssertionResult HpackBlockCollector::ValidateSoleLiteralValueHeader( + HpackEntryType expected_type, + size_t expected_index, + bool expected_value_huffman, + Http2StringPiece expected_value) const { + VERIFY_TRUE(pending_entry_.IsClear()); + VERIFY_EQ(1u, entries_.size()); + VERIFY_TRUE(entries_.front().ValidateLiteralValueHeader( + expected_type, expected_index, expected_value_huffman, expected_value)); + return AssertionSuccess(); +} +AssertionResult HpackBlockCollector::ValidateSoleLiteralNameValueHeader( + HpackEntryType expected_type, + bool expected_name_huffman, + Http2StringPiece expected_name, + bool expected_value_huffman, + Http2StringPiece expected_value) const { + VERIFY_TRUE(pending_entry_.IsClear()); + VERIFY_EQ(1u, entries_.size()); + VERIFY_TRUE(entries_.front().ValidateLiteralNameValueHeader( + expected_type, expected_name_huffman, expected_name, + expected_value_huffman, expected_value)); + return AssertionSuccess(); +} +AssertionResult HpackBlockCollector::ValidateSoleDynamicTableSizeUpdate( + size_t size) const { + VERIFY_TRUE(pending_entry_.IsClear()); + VERIFY_EQ(1u, entries_.size()); + VERIFY_TRUE(entries_.front().ValidateDynamicTableSizeUpdate(size)); + return AssertionSuccess(); +} + +AssertionResult HpackBlockCollector::VerifyEq( + const HpackBlockCollector& that) const { + VERIFY_EQ(pending_entry_, that.pending_entry_); + VERIFY_EQ(entries_, that.entries_); + return AssertionSuccess(); +} + +} // namespace test +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_block_collector.h b/http2/hpack/decoder/hpack_block_collector.h new file mode 100644 index 0000000..6ad1405 --- /dev/null +++ b/http2/hpack/decoder/hpack_block_collector.h
@@ -0,0 +1,129 @@ +// Copyright 2016 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. + +#ifndef QUICHE_HTTP2_HPACK_DECODER_HPACK_BLOCK_COLLECTOR_H_ +#define QUICHE_HTTP2_HPACK_DECODER_HPACK_BLOCK_COLLECTOR_H_ + +// HpackBlockCollector implements HpackEntryDecoderListener in order to record +// the calls using HpackEntryCollector instances (one per HPACK entry). This +// supports testing of HpackBlockDecoder, which decodes entire HPACK blocks. +// +// In addition to implementing the callback methods, HpackBlockCollector also +// supports comparing two HpackBlockCollector instances (i.e. an expected and +// an actual), or a sole HPACK entry against an expected value. + +#include <stddef.h> + +#include <vector> + +#include "testing/gtest/include/gtest/gtest.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_entry_collector.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_entry_decoder_listener.h" +#include "net/third_party/quiche/src/http2/hpack/http2_hpack_constants.h" +#include "net/third_party/quiche/src/http2/hpack/tools/hpack_block_builder.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string_piece.h" +#include "net/third_party/quiche/src/http2/test_tools/http2_random.h" + +namespace http2 { +namespace test { + +class HpackBlockCollector : public HpackEntryDecoderListener { + public: + HpackBlockCollector(); + HpackBlockCollector(const HpackBlockCollector& other); + ~HpackBlockCollector() override; + + // Implementations of HpackEntryDecoderListener, forwarding to pending_entry_, + // an HpackEntryCollector for the "in-progress" HPACK entry. OnIndexedHeader + // and OnDynamicTableSizeUpdate are pending only for that one call, while + // OnStartLiteralHeader is followed by many calls, ending with OnValueEnd. + // Once all the calls for one HPACK entry have been received, PushPendingEntry + // is used to append the pending_entry_ entry to the collected entries_. + void OnIndexedHeader(size_t index) override; + void OnDynamicTableSizeUpdate(size_t size) override; + void OnStartLiteralHeader(HpackEntryType header_type, + size_t maybe_name_index) override; + void OnNameStart(bool huffman_encoded, size_t len) override; + void OnNameData(const char* data, size_t len) override; + void OnNameEnd() override; + void OnValueStart(bool huffman_encoded, size_t len) override; + void OnValueData(const char* data, size_t len) override; + void OnValueEnd() override; + + // Methods for creating a set of expectations (i.e. HPACK entries to compare + // against those collected by another instance of HpackBlockCollector). + + // Add an HPACK entry for an indexed header. + void ExpectIndexedHeader(size_t index); + + // Add an HPACK entry for a dynamic table size update. + void ExpectDynamicTableSizeUpdate(size_t size); + + // Add an HPACK entry for a header entry with an index for the name, and a + // literal value. + void ExpectNameIndexAndLiteralValue(HpackEntryType type, + size_t index, + bool value_huffman, + const Http2String& value); + + // Add an HPACK entry for a header entry with a literal name and value. + void ExpectLiteralNameAndValue(HpackEntryType type, + bool name_huffman, + const Http2String& name, + bool value_huffman, + const Http2String& value); + + // Shuffle the entries, in support of generating an HPACK block of entries + // in some random order. + void ShuffleEntries(Http2Random* rng); + + // Serialize entries_ to the HpackBlockBuilder. + void AppendToHpackBlockBuilder(HpackBlockBuilder* hbb) const; + + // Return AssertionSuccess if there is just one entry, and it is an + // Indexed Header with the specified index. + ::testing::AssertionResult ValidateSoleIndexedHeader(size_t ndx) const; + + // Return AssertionSuccess if there is just one entry, and it is a + // Dynamic Table Size Update with the specified size. + ::testing::AssertionResult ValidateSoleDynamicTableSizeUpdate( + size_t size) const; + + // Return AssertionSuccess if there is just one entry, and it is a Header + // entry with an index for the name and a literal value. + ::testing::AssertionResult ValidateSoleLiteralValueHeader( + HpackEntryType expected_type, + size_t expected_index, + bool expected_value_huffman, + Http2StringPiece expected_value) const; + + // Return AssertionSuccess if there is just one entry, and it is a Header + // with a literal name and literal value. + ::testing::AssertionResult ValidateSoleLiteralNameValueHeader( + HpackEntryType expected_type, + bool expected_name_huffman, + Http2StringPiece expected_name, + bool expected_value_huffman, + Http2StringPiece expected_value) const; + + bool IsNotPending() const { return pending_entry_.IsClear(); } + bool IsClear() const { return IsNotPending() && entries_.empty(); } + void Clear(); + + ::testing::AssertionResult VerifyEq(const HpackBlockCollector& that) const; + + private: + // Push the value of pending_entry_ onto entries_, and clear pending_entry_. + // The pending_entry_ must be complete. + void PushPendingEntry(); + + HpackEntryCollector pending_entry_; + std::vector<HpackEntryCollector> entries_; +}; + +} // namespace test +} // namespace http2 + +#endif // QUICHE_HTTP2_HPACK_DECODER_HPACK_BLOCK_COLLECTOR_H_
diff --git a/http2/hpack/decoder/hpack_block_decoder.cc b/http2/hpack/decoder/hpack_block_decoder.cc new file mode 100644 index 0000000..c6882d2 --- /dev/null +++ b/http2/hpack/decoder/hpack_block_decoder.cc
@@ -0,0 +1,62 @@ +// Copyright 2016 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_block_decoder.h" + +#include <cstdint> + +#include "base/logging.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string_utils.h" + +namespace http2 { + +DecodeStatus HpackBlockDecoder::Decode(DecodeBuffer* db) { + if (!before_entry_) { + DVLOG(2) << "HpackBlockDecoder::Decode resume entry, db->Remaining=" + << db->Remaining(); + DecodeStatus status = entry_decoder_.Resume(db, listener_); + switch (status) { + case DecodeStatus::kDecodeDone: + before_entry_ = true; + break; + case DecodeStatus::kDecodeInProgress: + DCHECK_EQ(0u, db->Remaining()); + return DecodeStatus::kDecodeInProgress; + case DecodeStatus::kDecodeError: + return DecodeStatus::kDecodeError; + } + } + DCHECK(before_entry_); + while (db->HasData()) { + DVLOG(2) << "HpackBlockDecoder::Decode start entry, db->Remaining=" + << db->Remaining(); + DecodeStatus status = entry_decoder_.Start(db, listener_); + switch (status) { + case DecodeStatus::kDecodeDone: + continue; + case DecodeStatus::kDecodeInProgress: + DCHECK_EQ(0u, db->Remaining()); + before_entry_ = false; + return DecodeStatus::kDecodeInProgress; + case DecodeStatus::kDecodeError: + return DecodeStatus::kDecodeError; + } + DCHECK(false); + } + DCHECK(before_entry_); + return DecodeStatus::kDecodeDone; +} + +Http2String HpackBlockDecoder::DebugString() const { + return Http2StrCat("HpackBlockDecoder(", entry_decoder_.DebugString(), + ", listener@", + Http2Hex(reinterpret_cast<intptr_t>(listener_)), + (before_entry_ ? ", between entries)" : ", in an entry)")); +} + +std::ostream& operator<<(std::ostream& out, const HpackBlockDecoder& v) { + return out << v.DebugString(); +} + +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_block_decoder.h b/http2/hpack/decoder/hpack_block_decoder.h new file mode 100644 index 0000000..e8b23e1 --- /dev/null +++ b/http2/hpack/decoder/hpack_block_decoder.h
@@ -0,0 +1,64 @@ +// Copyright 2016 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. + +#ifndef QUICHE_HTTP2_HPACK_DECODER_HPACK_BLOCK_DECODER_H_ +#define QUICHE_HTTP2_HPACK_DECODER_HPACK_BLOCK_DECODER_H_ + +// HpackBlockDecoder decodes an entire HPACK block (or the available portion +// thereof in the DecodeBuffer) into entries, but doesn't include HPACK static +// or dynamic table support, so table indices remain indices at this level. +// Reports the entries to an HpackEntryDecoderListener. + +#include "base/logging.h" +#include "net/third_party/quiche/src/http2/decoder/decode_buffer.h" +#include "net/third_party/quiche/src/http2/decoder/decode_status.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_entry_decoder.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_entry_decoder_listener.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_export.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string.h" + +namespace http2 { + +class HTTP2_EXPORT_PRIVATE HpackBlockDecoder { + public: + explicit HpackBlockDecoder(HpackEntryDecoderListener* listener) + : listener_(listener) { + DCHECK_NE(listener_, nullptr); + } + ~HpackBlockDecoder() {} + + HpackBlockDecoder(const HpackBlockDecoder&) = delete; + HpackBlockDecoder& operator=(const HpackBlockDecoder&) = delete; + + // Prepares the decoder to start decoding a new HPACK block. Expected + // to be called from an implementation of Http2FrameDecoderListener's + // OnHeadersStart or OnPushPromiseStart methods. + void Reset() { + DVLOG(2) << "HpackBlockDecoder::Reset"; + before_entry_ = true; + } + + // Decode the fragment of the HPACK block contained in the decode buffer. + // Expected to be called from an implementation of Http2FrameDecoderListener's + // OnHpackFragment method. + DecodeStatus Decode(DecodeBuffer* db); + + // Is the decoding process between entries (i.e. would the next byte be the + // first byte of a new HPACK entry)? + bool before_entry() const { return before_entry_; } + + Http2String DebugString() const; + + private: + HpackEntryDecoder entry_decoder_; + HpackEntryDecoderListener* const listener_; + bool before_entry_ = true; +}; + +HTTP2_EXPORT_PRIVATE std::ostream& operator<<(std::ostream& out, + const HpackBlockDecoder& v); + +} // namespace http2 + +#endif // QUICHE_HTTP2_HPACK_DECODER_HPACK_BLOCK_DECODER_H_
diff --git a/http2/hpack/decoder/hpack_block_decoder_test.cc b/http2/hpack/decoder/hpack_block_decoder_test.cc new file mode 100644 index 0000000..fce45b2 --- /dev/null +++ b/http2/hpack/decoder/hpack_block_decoder_test.cc
@@ -0,0 +1,294 @@ +// Copyright 2016 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_block_decoder.h" + +// Tests of HpackBlockDecoder. + +#include <cstdint> + +#include "testing/gtest/include/gtest/gtest.h" +#include "net/third_party/quiche/src/http2/decoder/decode_buffer.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_block_collector.h" +#include "net/third_party/quiche/src/http2/hpack/http2_hpack_constants.h" +#include "net/third_party/quiche/src/http2/hpack/tools/hpack_block_builder.h" +#include "net/third_party/quiche/src/http2/hpack/tools/hpack_example.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string_piece.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_test_helpers.h" +#include "net/third_party/quiche/src/http2/test_tools/http2_random.h" +#include "net/third_party/quiche/src/http2/tools/random_decoder_test.h" + +using ::testing::AssertionSuccess; + +namespace http2 { +namespace test { +namespace { + +class HpackBlockDecoderTest : public RandomDecoderTest { + protected: + HpackBlockDecoderTest() : listener_(&collector_), decoder_(&listener_) { + stop_decode_on_done_ = false; + decoder_.Reset(); + // Make sure logging doesn't crash. Not examining the result. + std::ostringstream strm; + strm << decoder_; + } + + DecodeStatus StartDecoding(DecodeBuffer* db) override { + collector_.Clear(); + decoder_.Reset(); + return ResumeDecoding(db); + } + + DecodeStatus ResumeDecoding(DecodeBuffer* db) override { + DecodeStatus status = decoder_.Decode(db); + + // Make sure logging doesn't crash. Not examining the result. + std::ostringstream strm; + strm << decoder_; + + return status; + } + + AssertionResult DecodeAndValidateSeveralWays(DecodeBuffer* db, + const Validator& validator) { + bool return_non_zero_on_first = false; + return RandomDecoderTest::DecodeAndValidateSeveralWays( + db, return_non_zero_on_first, validator); + } + + AssertionResult DecodeAndValidateSeveralWays(const HpackBlockBuilder& hbb, + const Validator& validator) { + DecodeBuffer db(hbb.buffer()); + return DecodeAndValidateSeveralWays(&db, validator); + } + + AssertionResult DecodeHpackExampleAndValidateSeveralWays( + Http2StringPiece hpack_example, + Validator validator) { + Http2String input = HpackExampleToStringOrDie(hpack_example); + DecodeBuffer db(input); + return DecodeAndValidateSeveralWays(&db, validator); + } + + uint8_t Rand8() { return Random().Rand8(); } + + Http2String Rand8String() { return Random().RandString(Rand8()); } + + HpackBlockCollector collector_; + HpackEntryDecoderVLoggingListener listener_; + HpackBlockDecoder decoder_; +}; + +// http://httpwg.org/specs/rfc7541.html#rfc.section.C.2.1 +TEST_F(HpackBlockDecoderTest, SpecExample_C_2_1) { + auto do_check = [this]() { + VERIFY_AND_RETURN_SUCCESS(collector_.ValidateSoleLiteralNameValueHeader( + HpackEntryType::kIndexedLiteralHeader, false, "custom-key", false, + "custom-header")); + }; + const char hpack_example[] = R"( + 40 | == Literal indexed == + 0a | Literal name (len = 10) + 6375 7374 6f6d 2d6b 6579 | custom-key + 0d | Literal value (len = 13) + 6375 7374 6f6d 2d68 6561 6465 72 | custom-header + | -> custom-key: + | custom-header + )"; + EXPECT_TRUE(DecodeHpackExampleAndValidateSeveralWays( + hpack_example, ValidateDoneAndEmpty(do_check))); + EXPECT_TRUE(do_check()); +} + +// http://httpwg.org/specs/rfc7541.html#rfc.section.C.2.2 +TEST_F(HpackBlockDecoderTest, SpecExample_C_2_2) { + auto do_check = [this]() { + VERIFY_AND_RETURN_SUCCESS(collector_.ValidateSoleLiteralValueHeader( + HpackEntryType::kUnindexedLiteralHeader, 4, false, "/sample/path")); + }; + const char hpack_example[] = R"( + 04 | == Literal not indexed == + | Indexed name (idx = 4) + | :path + 0c | Literal value (len = 12) + 2f73 616d 706c 652f 7061 7468 | /sample/path + | -> :path: /sample/path + )"; + EXPECT_TRUE(DecodeHpackExampleAndValidateSeveralWays( + hpack_example, ValidateDoneAndEmpty(do_check))); + EXPECT_TRUE(do_check()); +} + +// http://httpwg.org/specs/rfc7541.html#rfc.section.C.2.3 +TEST_F(HpackBlockDecoderTest, SpecExample_C_2_3) { + auto do_check = [this]() { + VERIFY_AND_RETURN_SUCCESS(collector_.ValidateSoleLiteralNameValueHeader( + HpackEntryType::kNeverIndexedLiteralHeader, false, "password", false, + "secret")); + }; + const char hpack_example[] = R"( + 10 | == Literal never indexed == + 08 | Literal name (len = 8) + 7061 7373 776f 7264 | password + 06 | Literal value (len = 6) + 7365 6372 6574 | secret + | -> password: secret + )"; + EXPECT_TRUE(DecodeHpackExampleAndValidateSeveralWays( + hpack_example, ValidateDoneAndEmpty(do_check))); + EXPECT_TRUE(do_check()); +} + +// http://httpwg.org/specs/rfc7541.html#rfc.section.C.2.4 +TEST_F(HpackBlockDecoderTest, SpecExample_C_2_4) { + auto do_check = [this]() { + VERIFY_AND_RETURN_SUCCESS(collector_.ValidateSoleIndexedHeader(2)); + }; + const char hpack_example[] = R"( + 82 | == Indexed - Add == + | idx = 2 + | -> :method: GET + )"; + EXPECT_TRUE(DecodeHpackExampleAndValidateSeveralWays( + hpack_example, ValidateDoneAndEmpty(do_check))); + EXPECT_TRUE(do_check()); +} +// http://httpwg.org/specs/rfc7541.html#rfc.section.C.3.1 +TEST_F(HpackBlockDecoderTest, SpecExample_C_3_1) { + Http2String example = R"( + 82 | == Indexed - Add == + | idx = 2 + | -> :method: GET + 86 | == Indexed - Add == + | idx = 6 + | -> :scheme: http + 84 | == Indexed - Add == + | idx = 4 + | -> :path: / + 41 | == Literal indexed == + | Indexed name (idx = 1) + | :authority + 0f | Literal value (len = 15) + 7777 772e 6578 616d 706c 652e 636f 6d | www.example.com + | -> :authority: + | www.example.com + )"; + HpackBlockCollector expected; + expected.ExpectIndexedHeader(2); + expected.ExpectIndexedHeader(6); + expected.ExpectIndexedHeader(4); + expected.ExpectNameIndexAndLiteralValue(HpackEntryType::kIndexedLiteralHeader, + 1, false, "www.example.com"); + NoArgValidator do_check = [expected, this]() { + VERIFY_AND_RETURN_SUCCESS(collector_.VerifyEq(expected)); + }; + EXPECT_TRUE(DecodeHpackExampleAndValidateSeveralWays( + example, ValidateDoneAndEmpty(do_check))); + EXPECT_TRUE(do_check()); +} + +// http://httpwg.org/specs/rfc7541.html#rfc.section.C.5.1 +TEST_F(HpackBlockDecoderTest, SpecExample_C_5_1) { + Http2String example = R"( + 48 | == Literal indexed == + | Indexed name (idx = 8) + | :status + 03 | Literal value (len = 3) + 3330 32 | 302 + | -> :status: 302 + 58 | == Literal indexed == + | Indexed name (idx = 24) + | cache-control + 07 | Literal value (len = 7) + 7072 6976 6174 65 | private + | -> cache-control: private + 61 | == Literal indexed == + | Indexed name (idx = 33) + | date + 1d | Literal value (len = 29) + 4d6f 6e2c 2032 3120 4f63 7420 3230 3133 | Mon, 21 Oct 2013 + 2032 303a 3133 3a32 3120 474d 54 | 20:13:21 GMT + | -> date: Mon, 21 Oct 2013 + | 20:13:21 GMT + 6e | == Literal indexed == + | Indexed name (idx = 46) + | location + 17 | Literal value (len = 23) + 6874 7470 733a 2f2f 7777 772e 6578 616d | https://www.exam + 706c 652e 636f 6d | ple.com + | -> location: + | https://www.example.com + )"; + HpackBlockCollector expected; + expected.ExpectNameIndexAndLiteralValue(HpackEntryType::kIndexedLiteralHeader, + 8, false, "302"); + expected.ExpectNameIndexAndLiteralValue(HpackEntryType::kIndexedLiteralHeader, + 24, false, "private"); + expected.ExpectNameIndexAndLiteralValue(HpackEntryType::kIndexedLiteralHeader, + 33, false, + "Mon, 21 Oct 2013 20:13:21 GMT"); + expected.ExpectNameIndexAndLiteralValue(HpackEntryType::kIndexedLiteralHeader, + 46, false, "https://www.example.com"); + NoArgValidator do_check = [expected, this]() { + VERIFY_AND_RETURN_SUCCESS(collector_.VerifyEq(expected)); + }; + EXPECT_TRUE(DecodeHpackExampleAndValidateSeveralWays( + example, ValidateDoneAndEmpty(do_check))); + EXPECT_TRUE(do_check()); +} + +// Generate a bunch of HPACK block entries to expect, use those expectations +// to generate an HPACK block, then decode it and confirm it matches those +// expectations. Some of these are invalid (such as Indexed, with index=0), +// but well-formed, and the decoder doesn't check for validity, just +// well-formedness. That includes the validity of the strings not being checked, +// such as lower-case ascii for the names, and valid Huffman encodings. +TEST_F(HpackBlockDecoderTest, Computed) { + HpackBlockCollector expected; + expected.ExpectIndexedHeader(0); + expected.ExpectIndexedHeader(1); + expected.ExpectIndexedHeader(126); + expected.ExpectIndexedHeader(127); + expected.ExpectIndexedHeader(128); + expected.ExpectDynamicTableSizeUpdate(0); + expected.ExpectDynamicTableSizeUpdate(1); + expected.ExpectDynamicTableSizeUpdate(14); + expected.ExpectDynamicTableSizeUpdate(15); + expected.ExpectDynamicTableSizeUpdate(30); + expected.ExpectDynamicTableSizeUpdate(31); + expected.ExpectDynamicTableSizeUpdate(4095); + expected.ExpectDynamicTableSizeUpdate(4096); + expected.ExpectDynamicTableSizeUpdate(8192); + for (auto type : {HpackEntryType::kIndexedLiteralHeader, + HpackEntryType::kUnindexedLiteralHeader, + HpackEntryType::kNeverIndexedLiteralHeader}) { + for (bool value_huffman : {false, true}) { + // An entry with an index for the name. Ensure the name index + // is not zero by adding one to the Rand8() result. + expected.ExpectNameIndexAndLiteralValue(type, Rand8() + 1, value_huffman, + Rand8String()); + // And two entries with literal names, one plain, one huffman encoded. + expected.ExpectLiteralNameAndValue(type, false, Rand8String(), + value_huffman, Rand8String()); + expected.ExpectLiteralNameAndValue(type, true, Rand8String(), + value_huffman, Rand8String()); + } + } + // Shuffle the entries and serialize them to produce an HPACK block. + expected.ShuffleEntries(RandomPtr()); + HpackBlockBuilder hbb; + expected.AppendToHpackBlockBuilder(&hbb); + + NoArgValidator do_check = [expected, this]() { + VERIFY_AND_RETURN_SUCCESS(collector_.VerifyEq(expected)); + }; + EXPECT_TRUE( + DecodeAndValidateSeveralWays(hbb, ValidateDoneAndEmpty(do_check))); + EXPECT_TRUE(do_check()); +} + +} // namespace +} // namespace test +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_decoder.cc b/http2/hpack/decoder/hpack_decoder.cc new file mode 100644 index 0000000..d6897cb --- /dev/null +++ b/http2/hpack/decoder/hpack_decoder.cc
@@ -0,0 +1,122 @@ +// Copyright 2017 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_decoder.h" + +#include "base/logging.h" +#include "net/third_party/quiche/src/http2/decoder/decode_status.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_estimate_memory_usage.h" + +namespace http2 { + +HpackDecoder::HpackDecoder(HpackDecoderListener* listener, + size_t max_string_size) + : decoder_state_(listener), + entry_buffer_(&decoder_state_, max_string_size), + block_decoder_(&entry_buffer_), + error_detected_(false) {} + +HpackDecoder::~HpackDecoder() = default; + +void HpackDecoder::set_tables_debug_listener( + HpackDecoderTablesDebugListener* debug_listener) { + decoder_state_.set_tables_debug_listener(debug_listener); +} + +void HpackDecoder::set_max_string_size_bytes(size_t max_string_size_bytes) { + entry_buffer_.set_max_string_size_bytes(max_string_size_bytes); +} + +void HpackDecoder::ApplyHeaderTableSizeSetting(uint32_t max_header_table_size) { + decoder_state_.ApplyHeaderTableSizeSetting(max_header_table_size); +} + +bool HpackDecoder::StartDecodingBlock() { + DVLOG(3) << "HpackDecoder::StartDecodingBlock, error_detected=" + << (error_detected() ? "true" : "false"); + if (error_detected()) { + return false; + } + // TODO(jamessynge): Eliminate Reset(), which shouldn't be necessary + // if there are no errors, and shouldn't be necessary with errors if + // we never resume decoding after an error has been detected. + block_decoder_.Reset(); + decoder_state_.OnHeaderBlockStart(); + return true; +} + +bool HpackDecoder::DecodeFragment(DecodeBuffer* db) { + DVLOG(3) << "HpackDecoder::DecodeFragment, error_detected=" + << (error_detected() ? "true" : "false") + << ", size=" << db->Remaining(); + if (error_detected()) { + return false; + } + // Decode contents of db as an HPACK block fragment, forwards the decoded + // entries to entry_buffer_, which in turn forwards them to decode_state_, + // which finally forwards them to the HpackDecoderListener. + DecodeStatus status = block_decoder_.Decode(db); + if (status == DecodeStatus::kDecodeError) { + // This has probably already been reported, but just in case... + ReportError("HPACK block malformed."); + return false; + } else if (error_detected()) { + return false; + } + // Should be positioned between entries iff decoding is complete. + DCHECK_EQ(block_decoder_.before_entry(), status == DecodeStatus::kDecodeDone) + << status; + if (!block_decoder_.before_entry()) { + entry_buffer_.BufferStringsIfUnbuffered(); + } + return true; +} + +bool HpackDecoder::EndDecodingBlock() { + DVLOG(3) << "HpackDecoder::EndDecodingBlock, error_detected=" + << (error_detected() ? "true" : "false"); + if (error_detected()) { + return false; + } + if (!block_decoder_.before_entry()) { + // The HPACK block ended in the middle of an entry. + ReportError("HPACK block truncated."); + return false; + } + decoder_state_.OnHeaderBlockEnd(); + if (error_detected()) { + // HpackDecoderState will have reported the error. + return false; + } + return true; +} + +bool HpackDecoder::error_detected() { + if (!error_detected_) { + if (entry_buffer_.error_detected()) { + DVLOG(2) << "HpackDecoder::error_detected in entry_buffer_"; + error_detected_ = true; + } else if (decoder_state_.error_detected()) { + DVLOG(2) << "HpackDecoder::error_detected in decoder_state_"; + error_detected_ = true; + } + } + return error_detected_; +} + +size_t HpackDecoder::EstimateMemoryUsage() const { + return Http2EstimateMemoryUsage(entry_buffer_); +} + +void HpackDecoder::ReportError(Http2StringPiece error_message) { + DVLOG(3) << "HpackDecoder::ReportError is new=" + << (!error_detected_ ? "true" : "false") + << ", error_message: " << error_message; + if (!error_detected_) { + error_detected_ = true; + decoder_state_.listener()->OnHeaderErrorDetected(error_message); + } +} + +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_decoder.h b/http2/hpack/decoder/hpack_decoder.h new file mode 100644 index 0000000..e173bc6 --- /dev/null +++ b/http2/hpack/decoder/hpack_decoder.h
@@ -0,0 +1,123 @@ +// Copyright 2017 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. + +#ifndef QUICHE_HTTP2_HPACK_DECODER_HPACK_DECODER_H_ +#define QUICHE_HTTP2_HPACK_DECODER_HPACK_DECODER_H_ + +// Decodes HPACK blocks, calls an HpackDecoderListener with the decoded header +// entries. Also notifies the listener of errors and of the boundaries of the +// HPACK blocks. + +// TODO(jamessynge): Add feature allowing an HpackEntryDecoderListener +// sub-class (and possibly others) to be passed in for counting events, +// so that deciding whether to count is not done by having lots of if +// statements, but instead by inserting an indirection only when needed. + +// TODO(jamessynge): Consider whether to return false from methods below +// when an error has been previously detected. It protects calling code +// from its failure to pay attention to previous errors, but should we +// spend time to do that? + +#include <stddef.h> + +#include <cstdint> + +#include "net/third_party/quiche/src/http2/decoder/decode_buffer.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_block_decoder.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_decoder_listener.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_decoder_state.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_decoder_tables.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_whole_entry_buffer.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_export.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string_piece.h" + +namespace http2 { +namespace test { +class HpackDecoderPeer; +} // namespace test + +class HTTP2_EXPORT_PRIVATE HpackDecoder { + public: + HpackDecoder(HpackDecoderListener* listener, size_t max_string_size); + virtual ~HpackDecoder(); + + HpackDecoder(const HpackDecoder&) = delete; + HpackDecoder& operator=(const HpackDecoder&) = delete; + + // Set listener to be notified of insertions into the HPACK dynamic table, + // and uses of those entries. + void set_tables_debug_listener( + HpackDecoderTablesDebugListener* debug_listener); + + // max_string_size specifies the maximum size of an on-the-wire string (name + // or value, plain or Huffman encoded) that will be accepted. See sections + // 5.1 and 5.2 of RFC 7541. This is a defense against OOM attacks; HTTP/2 + // allows a decoder to enforce any limit of the size of the header lists + // that it is willing to decode, including less than the MAX_HEADER_LIST_SIZE + // setting, a setting that is initially unlimited. For example, we might + // choose to send a MAX_HEADER_LIST_SIZE of 64KB, and to use that same value + // as the upper bound for individual strings. + void set_max_string_size_bytes(size_t max_string_size_bytes); + + // ApplyHeaderTableSizeSetting notifies this object that this endpoint has + // received a SETTINGS ACK frame acknowledging an earlier SETTINGS frame from + // this endpoint specifying a new value for SETTINGS_HEADER_TABLE_SIZE (the + // maximum size of the dynamic table that this endpoint will use to decode + // HPACK blocks). + // Because a SETTINGS frame can contain SETTINGS_HEADER_TABLE_SIZE values, + // the caller must keep track of those multiple changes, and make + // corresponding calls to this method. In particular, a call must be made + // with the lowest value acknowledged by the peer, and a call must be made + // with the final value acknowledged, in that order; additional calls may + // be made if additional values were sent. These calls must be made between + // decoding the SETTINGS ACK, and before the next HPACK block is decoded. + void ApplyHeaderTableSizeSetting(uint32_t max_header_table_size); + + // Prepares the decoder for decoding a new HPACK block, and announces this to + // its listener. Returns true if OK to continue with decoding, false if an + // error has been detected, which for StartDecodingBlock means the error was + // detected while decoding a previous HPACK block. + bool StartDecodingBlock(); + + // Decodes a fragment (some or all of the remainder) of an HPACK block, + // reporting header entries (name & value pairs) that it completely decodes + // in the process to the listener. Returns true successfully decoded, false if + // an error has been detected, either during decoding of the fragment, or + // prior to this call. + bool DecodeFragment(DecodeBuffer* db); + + // Completes the process of decoding an HPACK block: if the HPACK block was + // properly terminated, announces the end of the header list to the listener + // and returns true; else returns false. + bool EndDecodingBlock(); + + // Was an error detected? + bool error_detected(); + + // Returns the estimate of dynamically allocated memory in bytes. + size_t EstimateMemoryUsage() const; + + private: + friend class test::HpackDecoderPeer; + + // Reports an error to the listener IF this is the first error detected. + void ReportError(Http2StringPiece error_message); + + // The decompressor state, as defined by HPACK (i.e. the static and dynamic + // tables). + HpackDecoderState decoder_state_; + + // Assembles the various parts of a header entry into whole entries. + HpackWholeEntryBuffer entry_buffer_; + + // The decoder of HPACK blocks into entry parts, passed to entry_buffer_. + HpackBlockDecoder block_decoder_; + + // Has an error been detected? + bool error_detected_; +}; + +} // namespace http2 + +#endif // QUICHE_HTTP2_HPACK_DECODER_HPACK_DECODER_H_
diff --git a/http2/hpack/decoder/hpack_decoder_listener.cc b/http2/hpack/decoder/hpack_decoder_listener.cc new file mode 100644 index 0000000..8afa8aa --- /dev/null +++ b/http2/hpack/decoder/hpack_decoder_listener.cc
@@ -0,0 +1,30 @@ +// Copyright 2016 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_decoder_listener.h" + +namespace http2 { + +HpackDecoderListener::HpackDecoderListener() = default; +HpackDecoderListener::~HpackDecoderListener() = default; + +HpackDecoderNoOpListener::HpackDecoderNoOpListener() = default; +HpackDecoderNoOpListener::~HpackDecoderNoOpListener() = default; + +void HpackDecoderNoOpListener::OnHeaderListStart() {} +void HpackDecoderNoOpListener::OnHeader(HpackEntryType entry_type, + const HpackString& name, + const HpackString& value) {} +void HpackDecoderNoOpListener::OnHeaderListEnd() {} +void HpackDecoderNoOpListener::OnHeaderErrorDetected( + Http2StringPiece error_message) {} + +// static +HpackDecoderNoOpListener* HpackDecoderNoOpListener::NoOpListener() { + static HpackDecoderNoOpListener* static_instance = + new HpackDecoderNoOpListener(); + return static_instance; +} + +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_decoder_listener.h b/http2/hpack/decoder/hpack_decoder_listener.h new file mode 100644 index 0000000..fa68591 --- /dev/null +++ b/http2/hpack/decoder/hpack_decoder_listener.h
@@ -0,0 +1,66 @@ +// Copyright 2016 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. + +// Defines HpackDecoderListener, the base class of listeners for HTTP header +// lists decoded from an HPACK block. + +#ifndef QUICHE_HTTP2_HPACK_DECODER_HPACK_DECODER_LISTENER_H_ +#define QUICHE_HTTP2_HPACK_DECODER_HPACK_DECODER_LISTENER_H_ + +#include "net/third_party/quiche/src/http2/hpack/hpack_string.h" +#include "net/third_party/quiche/src/http2/hpack/http2_hpack_constants.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_export.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string_piece.h" + +namespace http2 { + +class HTTP2_EXPORT_PRIVATE HpackDecoderListener { + public: + HpackDecoderListener(); + virtual ~HpackDecoderListener(); + + // OnHeaderListStart is called at the start of decoding an HPACK block into + // an HTTP/2 header list. Will only be called once per block, even if it + // extends into CONTINUATION frames. + virtual void OnHeaderListStart() = 0; + + // Called for each header name-value pair that is decoded, in the order they + // appear in the HPACK block. Multiple values for a given key will be emitted + // as multiple calls to OnHeader. + virtual void OnHeader(HpackEntryType entry_type, + const HpackString& name, + const HpackString& value) = 0; + + // OnHeaderListEnd is called after successfully decoding an HPACK block into + // an HTTP/2 header list. Will only be called once per block, even if it + // extends into CONTINUATION frames. + virtual void OnHeaderListEnd() = 0; + + // OnHeaderErrorDetected is called if an error is detected while decoding. + // error_message may be used in a GOAWAY frame as the Opaque Data. + virtual void OnHeaderErrorDetected(Http2StringPiece error_message) = 0; +}; + +// A no-op implementation of HpackDecoderListener, useful for ignoring +// callbacks once an error is detected. +class HTTP2_EXPORT_PRIVATE HpackDecoderNoOpListener + : public HpackDecoderListener { + public: + HpackDecoderNoOpListener(); + ~HpackDecoderNoOpListener() override; + + void OnHeaderListStart() override; + void OnHeader(HpackEntryType entry_type, + const HpackString& name, + const HpackString& value) override; + void OnHeaderListEnd() override; + void OnHeaderErrorDetected(Http2StringPiece error_message) override; + + // Returns a listener that ignores all the calls. + static HpackDecoderNoOpListener* NoOpListener(); +}; + +} // namespace http2 + +#endif // QUICHE_HTTP2_HPACK_DECODER_HPACK_DECODER_LISTENER_H_
diff --git a/http2/hpack/decoder/hpack_decoder_state.cc b/http2/hpack/decoder/hpack_decoder_state.cc new file mode 100644 index 0000000..2ddf421 --- /dev/null +++ b/http2/hpack/decoder/hpack_decoder_state.cc
@@ -0,0 +1,218 @@ +// Copyright 2016 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_decoder_state.h" + +#include "base/logging.h" +#include "net/third_party/quiche/src/http2/hpack/hpack_string.h" +#include "net/third_party/quiche/src/http2/http2_constants.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_macros.h" + +namespace http2 { +namespace { + +HpackString ExtractHpackString(HpackDecoderStringBuffer* string_buffer) { + if (string_buffer->IsBuffered()) { + return HpackString(string_buffer->ReleaseString()); + } else { + auto result = HpackString(string_buffer->str()); + string_buffer->Reset(); + return result; + } +} + +} // namespace + +HpackDecoderState::HpackDecoderState(HpackDecoderListener* listener) + : listener_(HTTP2_DIE_IF_NULL(listener)), + final_header_table_size_(Http2SettingsInfo::DefaultHeaderTableSize()), + lowest_header_table_size_(final_header_table_size_), + require_dynamic_table_size_update_(false), + allow_dynamic_table_size_update_(true), + saw_dynamic_table_size_update_(false), + error_detected_(false) {} +HpackDecoderState::~HpackDecoderState() = default; + +void HpackDecoderState::set_tables_debug_listener( + HpackDecoderTablesDebugListener* debug_listener) { + decoder_tables_.set_debug_listener(debug_listener); +} + +void HpackDecoderState::ApplyHeaderTableSizeSetting( + uint32_t header_table_size) { + DVLOG(2) << "HpackDecoderState::ApplyHeaderTableSizeSetting(" + << header_table_size << ")"; + DCHECK_LE(lowest_header_table_size_, final_header_table_size_); + if (header_table_size < lowest_header_table_size_) { + lowest_header_table_size_ = header_table_size; + } + final_header_table_size_ = header_table_size; + DVLOG(2) << "low water mark: " << lowest_header_table_size_; + DVLOG(2) << "final limit: " << final_header_table_size_; +} + +// Called to notify this object that we're starting to decode an HPACK block +// (e.g. a HEADERS or PUSH_PROMISE frame's header has been decoded). +void HpackDecoderState::OnHeaderBlockStart() { + DVLOG(2) << "HpackDecoderState::OnHeaderBlockStart"; + // This instance can't be reused after an error has been detected, as we must + // assume that the encoder and decoder compression states are no longer + // synchronized. + DCHECK(!error_detected_); + DCHECK_LE(lowest_header_table_size_, final_header_table_size_); + allow_dynamic_table_size_update_ = true; + saw_dynamic_table_size_update_ = false; + // If the peer has acknowledged a HEADER_TABLE_SIZE smaller than that which + // its HPACK encoder has been using, then the next HPACK block it sends MUST + // start with a Dynamic Table Size Update entry that is at least as low as + // lowest_header_table_size_. That may be followed by another as great as + // final_header_table_size_, if those are different. + require_dynamic_table_size_update_ = + (lowest_header_table_size_ < + decoder_tables_.current_header_table_size() || + final_header_table_size_ < decoder_tables_.header_table_size_limit()); + DVLOG(2) << "HpackDecoderState::OnHeaderListStart " + << "require_dynamic_table_size_update_=" + << require_dynamic_table_size_update_; + listener_->OnHeaderListStart(); +} + +void HpackDecoderState::OnIndexedHeader(size_t index) { + DVLOG(2) << "HpackDecoderState::OnIndexedHeader: " << index; + if (error_detected_) { + return; + } + if (require_dynamic_table_size_update_) { + ReportError("Missing dynamic table size update."); + return; + } + allow_dynamic_table_size_update_ = false; + const HpackStringPair* entry = decoder_tables_.Lookup(index); + if (entry != nullptr) { + listener_->OnHeader(HpackEntryType::kIndexedHeader, entry->name, + entry->value); + } else { + ReportError("Invalid index."); + } +} + +void HpackDecoderState::OnNameIndexAndLiteralValue( + HpackEntryType entry_type, + size_t name_index, + HpackDecoderStringBuffer* value_buffer) { + DVLOG(2) << "HpackDecoderState::OnNameIndexAndLiteralValue " << entry_type + << ", " << name_index << ", " << value_buffer->str(); + if (error_detected_) { + return; + } + if (require_dynamic_table_size_update_) { + ReportError("Missing dynamic table size update."); + return; + } + allow_dynamic_table_size_update_ = false; + const HpackStringPair* entry = decoder_tables_.Lookup(name_index); + if (entry != nullptr) { + HpackString value(ExtractHpackString(value_buffer)); + listener_->OnHeader(entry_type, entry->name, value); + if (entry_type == HpackEntryType::kIndexedLiteralHeader) { + decoder_tables_.Insert(entry->name, value); + } + } else { + ReportError("Invalid name index."); + } +} + +void HpackDecoderState::OnLiteralNameAndValue( + HpackEntryType entry_type, + HpackDecoderStringBuffer* name_buffer, + HpackDecoderStringBuffer* value_buffer) { + DVLOG(2) << "HpackDecoderState::OnLiteralNameAndValue " << entry_type << ", " + << name_buffer->str() << ", " << value_buffer->str(); + if (error_detected_) { + return; + } + if (require_dynamic_table_size_update_) { + ReportError("Missing dynamic table size update."); + return; + } + allow_dynamic_table_size_update_ = false; + HpackString name(ExtractHpackString(name_buffer)); + HpackString value(ExtractHpackString(value_buffer)); + listener_->OnHeader(entry_type, name, value); + if (entry_type == HpackEntryType::kIndexedLiteralHeader) { + decoder_tables_.Insert(name, value); + } +} + +void HpackDecoderState::OnDynamicTableSizeUpdate(size_t size_limit) { + DVLOG(2) << "HpackDecoderState::OnDynamicTableSizeUpdate " << size_limit + << ", required=" + << (require_dynamic_table_size_update_ ? "true" : "false") + << ", allowed=" + << (allow_dynamic_table_size_update_ ? "true" : "false"); + if (error_detected_) { + return; + } + DCHECK_LE(lowest_header_table_size_, final_header_table_size_); + if (!allow_dynamic_table_size_update_) { + // At most two dynamic table size updates allowed at the start, and not + // after a header. + ReportError("Dynamic table size update not allowed."); + return; + } + if (require_dynamic_table_size_update_) { + // The new size must not be greater than the low water mark. + if (size_limit > lowest_header_table_size_) { + ReportError("Initial dynamic table size update is above low water mark."); + return; + } + require_dynamic_table_size_update_ = false; + } else if (size_limit > final_header_table_size_) { + // The new size must not be greater than the final max header table size + // that the peer acknowledged. + ReportError("Dynamic table size update is above acknowledged setting."); + return; + } + decoder_tables_.DynamicTableSizeUpdate(size_limit); + if (saw_dynamic_table_size_update_) { + allow_dynamic_table_size_update_ = false; + } else { + saw_dynamic_table_size_update_ = true; + } + // We no longer need to keep an eye out for a lower header table size. + lowest_header_table_size_ = final_header_table_size_; +} + +void HpackDecoderState::OnHpackDecodeError(Http2StringPiece error_message) { + DVLOG(2) << "HpackDecoderState::OnHpackDecodeError " << error_message; + if (!error_detected_) { + ReportError(error_message); + } +} + +void HpackDecoderState::OnHeaderBlockEnd() { + DVLOG(2) << "HpackDecoderState::OnHeaderBlockEnd"; + if (error_detected_) { + return; + } + if (require_dynamic_table_size_update_) { + // Apparently the HPACK block was empty, but we needed it to contain at + // least 1 dynamic table size update. + ReportError("Missing dynamic table size update."); + } else { + listener_->OnHeaderListEnd(); + } +} + +void HpackDecoderState::ReportError(Http2StringPiece error_message) { + DVLOG(2) << "HpackDecoderState::ReportError is new=" + << (!error_detected_ ? "true" : "false") + << ", error_message: " << error_message; + if (!error_detected_) { + listener_->OnHeaderErrorDetected(error_message); + error_detected_ = true; + } +} + +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_decoder_state.h b/http2/hpack/decoder/hpack_decoder_state.h new file mode 100644 index 0000000..fd34179 --- /dev/null +++ b/http2/hpack/decoder/hpack_decoder_state.h
@@ -0,0 +1,129 @@ +// Copyright 2016 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. + +// HpackDecoderState maintains the HPACK decompressor state; i.e. updates the +// HPACK dynamic table according to RFC 7541 as the entries in an HPACK block +// are decoded, and reads from the static and dynamic tables in order to build +// complete header entries. Calls an HpackDecoderListener with the completely +// decoded headers (i.e. after resolving table indices into names or values), +// thus translating the decoded HPACK entries into HTTP/2 headers. + +#ifndef QUICHE_HTTP2_HPACK_DECODER_HPACK_DECODER_STATE_H_ +#define QUICHE_HTTP2_HPACK_DECODER_HPACK_DECODER_STATE_H_ + +#include <stddef.h> + +#include <cstdint> + +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_decoder_listener.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_decoder_string_buffer.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_decoder_tables.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_whole_entry_listener.h" +#include "net/third_party/quiche/src/http2/hpack/http2_hpack_constants.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_export.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string_piece.h" + +namespace http2 { +namespace test { +class HpackDecoderStatePeer; +} // namespace test + +class HTTP2_EXPORT_PRIVATE HpackDecoderState : public HpackWholeEntryListener { + public: + explicit HpackDecoderState(HpackDecoderListener* listener); + ~HpackDecoderState() override; + + HpackDecoderState(const HpackDecoderState&) = delete; + HpackDecoderState& operator=(const HpackDecoderState&) = delete; + + // Set the listener to be notified when a whole entry has been decoded, + // including resolving name or name and value references. + // The listener may be changed at any time. + HpackDecoderListener* listener() const { return listener_; } + + // Set listener to be notified of insertions into the HPACK dynamic table, + // and uses of those entries. + void set_tables_debug_listener( + HpackDecoderTablesDebugListener* debug_listener); + + // ApplyHeaderTableSizeSetting notifies this object that this endpoint has + // received a SETTINGS ACK frame acknowledging an earlier SETTINGS frame from + // this endpoint specifying a new value for SETTINGS_HEADER_TABLE_SIZE (the + // maximum size of the dynamic table that this endpoint will use to decode + // HPACK blocks). + // Because a SETTINGS frame can contain SETTINGS_HEADER_TABLE_SIZE values, + // the caller must keep track of those multiple changes, and make + // corresponding calls to this method. In particular, a call must be made + // with the lowest value acknowledged by the peer, and a call must be made + // with the final value acknowledged, in that order; additional calls may + // be made if additional values were sent. These calls must be made between + // decoding the SETTINGS ACK, and before the next HPACK block is decoded. + void ApplyHeaderTableSizeSetting(uint32_t max_header_table_size); + + // OnHeaderBlockStart notifies this object that we're starting to decode the + // HPACK payload of a HEADERS or PUSH_PROMISE frame. + void OnHeaderBlockStart(); + + // Implement the HpackWholeEntryListener methods, each of which notifies this + // object when an entire entry has been decoded. + void OnIndexedHeader(size_t index) override; + void OnNameIndexAndLiteralValue( + HpackEntryType entry_type, + size_t name_index, + HpackDecoderStringBuffer* value_buffer) override; + void OnLiteralNameAndValue(HpackEntryType entry_type, + HpackDecoderStringBuffer* name_buffer, + HpackDecoderStringBuffer* value_buffer) override; + void OnDynamicTableSizeUpdate(size_t size) override; + void OnHpackDecodeError(Http2StringPiece error_message) override; + + // OnHeaderBlockEnd notifies this object that an entire HPACK block has been + // decoded, which might have extended into CONTINUATION blocks. + void OnHeaderBlockEnd(); + + // Was an error detected? After an error has been detected and reported, + // no further callbacks will be made to the listener. + bool error_detected() const { return error_detected_; } + + const HpackDecoderTables& decoder_tables_for_test() const { + return decoder_tables_; + } + + private: + friend class test::HpackDecoderStatePeer; + + // Reports an error to the listener IF this is the first error detected. + void ReportError(Http2StringPiece error_message); + + // The static and dynamic HPACK tables. + HpackDecoderTables decoder_tables_; + + // The listener to be notified of headers, the start and end of header + // lists, and of errors. + HpackDecoderListener* listener_; + + // The most recent HEADER_TABLE_SIZE setting acknowledged by the peer. + uint32_t final_header_table_size_; + + // The lowest HEADER_TABLE_SIZE setting acknowledged by the peer; valid until + // the next HPACK block is decoded. + // TODO(jamessynge): Test raising the HEADER_TABLE_SIZE. + uint32_t lowest_header_table_size_; + + // Must the next (first) HPACK entry be a dynamic table size update? + bool require_dynamic_table_size_update_; + + // May the next (first or second) HPACK entry be a dynamic table size update? + bool allow_dynamic_table_size_update_; + + // Have we already seen a dynamic table size update in this HPACK block? + bool saw_dynamic_table_size_update_; + + // Has an error already been detected and reported to the listener? + bool error_detected_; +}; + +} // namespace http2 + +#endif // QUICHE_HTTP2_HPACK_DECODER_HPACK_DECODER_STATE_H_
diff --git a/http2/hpack/decoder/hpack_decoder_state_test.cc b/http2/hpack/decoder/hpack_decoder_state_test.cc new file mode 100644 index 0000000..115827e --- /dev/null +++ b/http2/hpack/decoder/hpack_decoder_state_test.cc
@@ -0,0 +1,539 @@ +// Copyright 2016 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_decoder_state.h" + +// Tests of HpackDecoderState. + +#include <utility> +#include <vector> + +#include "base/logging.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "net/third_party/quiche/src/http2/hpack/hpack_string.h" +#include "net/third_party/quiche/src/http2/hpack/http2_hpack_constants.h" +#include "net/third_party/quiche/src/http2/http2_constants.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string_piece.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_test_helpers.h" + +using ::testing::AssertionResult; +using ::testing::AssertionSuccess; +using ::testing::Eq; +using ::testing::HasSubstr; +using ::testing::Mock; +using ::testing::StrictMock; + +namespace http2 { +namespace test { +class HpackDecoderStatePeer { + public: + static HpackDecoderTables* GetDecoderTables(HpackDecoderState* state) { + return &state->decoder_tables_; + } +}; + +namespace { + +class MockHpackDecoderListener : public HpackDecoderListener { + public: + MOCK_METHOD0(OnHeaderListStart, void()); + MOCK_METHOD3(OnHeader, + void(HpackEntryType entry_type, + const HpackString& name, + const HpackString& value)); + MOCK_METHOD0(OnHeaderListEnd, void()); + MOCK_METHOD1(OnHeaderErrorDetected, void(Http2StringPiece error_message)); +}; + +enum StringBacking { STATIC, UNBUFFERED, BUFFERED }; + +class HpackDecoderStateTest : public ::testing::Test { + protected: + HpackDecoderStateTest() : decoder_state_(&listener_) {} + + HpackDecoderTables* GetDecoderTables() { + return HpackDecoderStatePeer::GetDecoderTables(&decoder_state_); + } + + const HpackStringPair* Lookup(size_t index) { + return GetDecoderTables()->Lookup(index); + } + + size_t current_header_table_size() { + return GetDecoderTables()->current_header_table_size(); + } + + size_t header_table_size_limit() { + return GetDecoderTables()->header_table_size_limit(); + } + + void set_header_table_size_limit(size_t size) { + GetDecoderTables()->DynamicTableSizeUpdate(size); + } + + void SetStringBuffer(const char* s, + StringBacking backing, + HpackDecoderStringBuffer* string_buffer) { + switch (backing) { + case STATIC: + string_buffer->Set(s, true); + break; + case UNBUFFERED: + string_buffer->Set(s, false); + break; + case BUFFERED: + string_buffer->Set(s, false); + string_buffer->BufferStringIfUnbuffered(); + break; + } + } + + void SetName(const char* s, StringBacking backing) { + SetStringBuffer(s, backing, &name_buffer_); + } + + void SetValue(const char* s, StringBacking backing) { + SetStringBuffer(s, backing, &value_buffer_); + } + + void SendStartAndVerifyCallback() { + EXPECT_CALL(listener_, OnHeaderListStart()); + decoder_state_.OnHeaderBlockStart(); + Mock::VerifyAndClearExpectations(&listener_); + } + + void SendSizeUpdate(size_t size) { + decoder_state_.OnDynamicTableSizeUpdate(size); + Mock::VerifyAndClearExpectations(&listener_); + } + + void SendIndexAndVerifyCallback(size_t index, + HpackEntryType expected_type, + const char* expected_name, + const char* expected_value) { + EXPECT_CALL(listener_, + OnHeader(expected_type, Eq(expected_name), Eq(expected_value))); + decoder_state_.OnIndexedHeader(index); + Mock::VerifyAndClearExpectations(&listener_); + } + + void SendValueAndVerifyCallback(size_t name_index, + HpackEntryType entry_type, + const char* name, + const char* value, + StringBacking value_backing) { + SetValue(value, value_backing); + EXPECT_CALL(listener_, OnHeader(entry_type, Eq(name), Eq(value))); + decoder_state_.OnNameIndexAndLiteralValue(entry_type, name_index, + &value_buffer_); + Mock::VerifyAndClearExpectations(&listener_); + } + + void SendNameAndValueAndVerifyCallback(HpackEntryType entry_type, + const char* name, + StringBacking name_backing, + const char* value, + StringBacking value_backing) { + SetName(name, name_backing); + SetValue(value, value_backing); + EXPECT_CALL(listener_, OnHeader(entry_type, Eq(name), Eq(value))); + decoder_state_.OnLiteralNameAndValue(entry_type, &name_buffer_, + &value_buffer_); + Mock::VerifyAndClearExpectations(&listener_); + } + + void SendEndAndVerifyCallback() { + EXPECT_CALL(listener_, OnHeaderListEnd()); + decoder_state_.OnHeaderBlockEnd(); + Mock::VerifyAndClearExpectations(&listener_); + } + + // dynamic_index is one-based, because that is the way RFC 7541 shows it. + AssertionResult VerifyEntry(size_t dynamic_index, + const char* name, + const char* value) { + const HpackStringPair* entry = + Lookup(dynamic_index + kFirstDynamicTableIndex - 1); + VERIFY_NE(entry, nullptr); + VERIFY_EQ(entry->name.ToStringPiece(), name); + VERIFY_EQ(entry->value.ToStringPiece(), value); + return AssertionSuccess(); + } + AssertionResult VerifyNoEntry(size_t dynamic_index) { + const HpackStringPair* entry = + Lookup(dynamic_index + kFirstDynamicTableIndex - 1); + VERIFY_EQ(entry, nullptr); + return AssertionSuccess(); + } + AssertionResult VerifyDynamicTableContents( + const std::vector<std::pair<const char*, const char*>>& entries) { + size_t index = 1; + for (const auto& entry : entries) { + VERIFY_SUCCESS(VerifyEntry(index, entry.first, entry.second)); + ++index; + } + VERIFY_SUCCESS(VerifyNoEntry(index)); + return AssertionSuccess(); + } + + StrictMock<MockHpackDecoderListener> listener_; + HpackDecoderState decoder_state_; + HpackDecoderStringBuffer name_buffer_, value_buffer_; +}; + +// Test based on RFC 7541, section C.3: Request Examples without Huffman Coding. +// This section shows several consecutive header lists, corresponding to HTTP +// requests, on the same connection. +TEST_F(HpackDecoderStateTest, C3_RequestExamples) { + // C.3.1 First Request + // + // Header list to encode: + // + // :method: GET + // :scheme: http + // :path: / + // :authority: www.example.com + + SendStartAndVerifyCallback(); + SendIndexAndVerifyCallback(2, HpackEntryType::kIndexedHeader, ":method", + "GET"); + SendIndexAndVerifyCallback(6, HpackEntryType::kIndexedHeader, ":scheme", + "http"); + SendIndexAndVerifyCallback(4, HpackEntryType::kIndexedHeader, ":path", "/"); + SendValueAndVerifyCallback(1, HpackEntryType::kIndexedLiteralHeader, + ":authority", "www.example.com", UNBUFFERED); + SendEndAndVerifyCallback(); + + // Dynamic Table (after decoding): + // + // [ 1] (s = 57) :authority: www.example.com + // Table size: 57 + + ASSERT_TRUE(VerifyDynamicTableContents({{":authority", "www.example.com"}})); + ASSERT_EQ(57u, current_header_table_size()); + + // C.3.2 Second Request + // + // Header list to encode: + // + // :method: GET + // :scheme: http + // :path: / + // :authority: www.example.com + // cache-control: no-cache + + SendStartAndVerifyCallback(); + SendIndexAndVerifyCallback(2, HpackEntryType::kIndexedHeader, ":method", + "GET"); + SendIndexAndVerifyCallback(6, HpackEntryType::kIndexedHeader, ":scheme", + "http"); + SendIndexAndVerifyCallback(4, HpackEntryType::kIndexedHeader, ":path", "/"); + SendIndexAndVerifyCallback(62, HpackEntryType::kIndexedHeader, ":authority", + "www.example.com"); + SendValueAndVerifyCallback(24, HpackEntryType::kIndexedLiteralHeader, + "cache-control", "no-cache", UNBUFFERED); + SendEndAndVerifyCallback(); + + // Dynamic Table (after decoding): + // + // [ 1] (s = 53) cache-control: no-cache + // [ 2] (s = 57) :authority: www.example.com + // Table size: 110 + + ASSERT_TRUE(VerifyDynamicTableContents( + {{"cache-control", "no-cache"}, {":authority", "www.example.com"}})); + ASSERT_EQ(110u, current_header_table_size()); + + // C.3.3 Third Request + // + // Header list to encode: + // + // :method: GET + // :scheme: https + // :path: /index.html + // :authority: www.example.com + // custom-key: custom-value + + SendStartAndVerifyCallback(); + SendIndexAndVerifyCallback(2, HpackEntryType::kIndexedHeader, ":method", + "GET"); + SendIndexAndVerifyCallback(7, HpackEntryType::kIndexedHeader, ":scheme", + "https"); + SendIndexAndVerifyCallback(5, HpackEntryType::kIndexedHeader, ":path", + "/index.html"); + SendIndexAndVerifyCallback(63, HpackEntryType::kIndexedHeader, ":authority", + "www.example.com"); + SendNameAndValueAndVerifyCallback(HpackEntryType::kIndexedLiteralHeader, + "custom-key", UNBUFFERED, "custom-value", + UNBUFFERED); + SendEndAndVerifyCallback(); + + // Dynamic Table (after decoding): + // + // [ 1] (s = 54) custom-key: custom-value + // [ 2] (s = 53) cache-control: no-cache + // [ 3] (s = 57) :authority: www.example.com + // Table size: 164 + + ASSERT_TRUE(VerifyDynamicTableContents({{"custom-key", "custom-value"}, + {"cache-control", "no-cache"}, + {":authority", "www.example.com"}})); + ASSERT_EQ(164u, current_header_table_size()); +} + +// Test based on RFC 7541, section C.5: Response Examples without Huffman +// Coding. This section shows several consecutive header lists, corresponding +// to HTTP responses, on the same connection. The HTTP/2 setting parameter +// SETTINGS_HEADER_TABLE_SIZE is set to the value of 256 octets, causing +// some evictions to occur. +TEST_F(HpackDecoderStateTest, C5_ResponseExamples) { + set_header_table_size_limit(256); + + // C.5.1 First Response + // + // Header list to encode: + // + // :status: 302 + // cache-control: private + // date: Mon, 21 Oct 2013 20:13:21 GMT + // location: https://www.example.com + + SendStartAndVerifyCallback(); + SendValueAndVerifyCallback(8, HpackEntryType::kIndexedLiteralHeader, + ":status", "302", BUFFERED); + SendValueAndVerifyCallback(24, HpackEntryType::kIndexedLiteralHeader, + "cache-control", "private", UNBUFFERED); + SendValueAndVerifyCallback(33, HpackEntryType::kIndexedLiteralHeader, "date", + "Mon, 21 Oct 2013 20:13:21 GMT", UNBUFFERED); + SendValueAndVerifyCallback(46, HpackEntryType::kIndexedLiteralHeader, + "location", "https://www.example.com", UNBUFFERED); + SendEndAndVerifyCallback(); + + // Dynamic Table (after decoding): + // + // [ 1] (s = 63) location: https://www.example.com + // [ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT + // [ 3] (s = 52) cache-control: private + // [ 4] (s = 42) :status: 302 + // Table size: 222 + + ASSERT_TRUE( + VerifyDynamicTableContents({{"location", "https://www.example.com"}, + {"date", "Mon, 21 Oct 2013 20:13:21 GMT"}, + {"cache-control", "private"}, + {":status", "302"}})); + ASSERT_EQ(222u, current_header_table_size()); + + // C.5.2 Second Response + // + // The (":status", "302") header field is evicted from the dynamic table to + // free space to allow adding the (":status", "307") header field. + // + // Header list to encode: + // + // :status: 307 + // cache-control: private + // date: Mon, 21 Oct 2013 20:13:21 GMT + // location: https://www.example.com + + SendStartAndVerifyCallback(); + SendValueAndVerifyCallback(8, HpackEntryType::kIndexedLiteralHeader, + ":status", "307", BUFFERED); + SendIndexAndVerifyCallback(65, HpackEntryType::kIndexedHeader, + "cache-control", "private"); + SendIndexAndVerifyCallback(64, HpackEntryType::kIndexedHeader, "date", + "Mon, 21 Oct 2013 20:13:21 GMT"); + SendIndexAndVerifyCallback(63, HpackEntryType::kIndexedHeader, "location", + "https://www.example.com"); + SendEndAndVerifyCallback(); + + // Dynamic Table (after decoding): + // + // [ 1] (s = 42) :status: 307 + // [ 2] (s = 63) location: https://www.example.com + // [ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT + // [ 4] (s = 52) cache-control: private + // Table size: 222 + + ASSERT_TRUE( + VerifyDynamicTableContents({{":status", "307"}, + {"location", "https://www.example.com"}, + {"date", "Mon, 21 Oct 2013 20:13:21 GMT"}, + {"cache-control", "private"}})); + ASSERT_EQ(222u, current_header_table_size()); + + // C.5.3 Third Response + // + // Several header fields are evicted from the dynamic table during the + // processing of this header list. + // + // Header list to encode: + // + // :status: 200 + // cache-control: private + // date: Mon, 21 Oct 2013 20:13:22 GMT + // location: https://www.example.com + // content-encoding: gzip + // set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1 + + SendStartAndVerifyCallback(); + SendIndexAndVerifyCallback(8, HpackEntryType::kIndexedHeader, ":status", + "200"); + SendIndexAndVerifyCallback(65, HpackEntryType::kIndexedHeader, + "cache-control", "private"); + SendValueAndVerifyCallback(33, HpackEntryType::kIndexedLiteralHeader, "date", + "Mon, 21 Oct 2013 20:13:22 GMT", BUFFERED); + SendIndexAndVerifyCallback(64, HpackEntryType::kIndexedHeader, "location", + "https://www.example.com"); + SendValueAndVerifyCallback(26, HpackEntryType::kIndexedLiteralHeader, + "content-encoding", "gzip", UNBUFFERED); + SendValueAndVerifyCallback( + 55, HpackEntryType::kIndexedLiteralHeader, "set-cookie", + "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1", BUFFERED); + SendEndAndVerifyCallback(); + + // Dynamic Table (after decoding): + // + // [ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; + // max-age=3600; version=1 + // [ 2] (s = 52) content-encoding: gzip + // [ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT + // Table size: 215 + + ASSERT_TRUE(VerifyDynamicTableContents( + {{"set-cookie", + "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"}, + {"content-encoding", "gzip"}, + {"date", "Mon, 21 Oct 2013 20:13:22 GMT"}})); + ASSERT_EQ(215u, current_header_table_size()); +} + +// Confirm that the table size can be changed, but at most twice. +TEST_F(HpackDecoderStateTest, OptionalTableSizeChanges) { + SendStartAndVerifyCallback(); + EXPECT_EQ(Http2SettingsInfo::DefaultHeaderTableSize(), + header_table_size_limit()); + SendSizeUpdate(1024); + EXPECT_EQ(1024u, header_table_size_limit()); + SendSizeUpdate(0); + EXPECT_EQ(0u, header_table_size_limit()); + + // Three updates aren't allowed. + EXPECT_CALL(listener_, + OnHeaderErrorDetected(HasSubstr("size update not allowed"))); + SendSizeUpdate(0); +} + +// Confirm that required size updates are indeed required before headers. +TEST_F(HpackDecoderStateTest, RequiredTableSizeChangeBeforeHeader) { + decoder_state_.ApplyHeaderTableSizeSetting(1024); + decoder_state_.ApplyHeaderTableSizeSetting(2048); + + // First provide the required update, and an allowed second update. + SendStartAndVerifyCallback(); + EXPECT_EQ(Http2SettingsInfo::DefaultHeaderTableSize(), + header_table_size_limit()); + SendSizeUpdate(1024); + EXPECT_EQ(1024u, header_table_size_limit()); + SendSizeUpdate(1500); + EXPECT_EQ(1500u, header_table_size_limit()); + SendEndAndVerifyCallback(); + + // Another HPACK block, but this time missing the required size update. + decoder_state_.ApplyHeaderTableSizeSetting(1024); + SendStartAndVerifyCallback(); + EXPECT_CALL(listener_, OnHeaderErrorDetected( + HasSubstr("Missing dynamic table size update"))); + decoder_state_.OnIndexedHeader(1); + + // Further decoded entries are ignored. + decoder_state_.OnIndexedHeader(1); + decoder_state_.OnDynamicTableSizeUpdate(1); + SetValue("value", UNBUFFERED); + decoder_state_.OnNameIndexAndLiteralValue( + HpackEntryType::kIndexedLiteralHeader, 4, &value_buffer_); + SetName("name", UNBUFFERED); + decoder_state_.OnLiteralNameAndValue(HpackEntryType::kIndexedLiteralHeader, + &name_buffer_, &value_buffer_); + decoder_state_.OnHeaderBlockEnd(); + decoder_state_.OnHpackDecodeError("NOT FORWARDED"); +} + +// Confirm that required size updates are validated. +TEST_F(HpackDecoderStateTest, InvalidRequiredSizeUpdate) { + // Require a size update, but provide one that isn't small enough. + decoder_state_.ApplyHeaderTableSizeSetting(1024); + SendStartAndVerifyCallback(); + EXPECT_EQ(Http2SettingsInfo::DefaultHeaderTableSize(), + header_table_size_limit()); + EXPECT_CALL(listener_, + OnHeaderErrorDetected(HasSubstr("above low water mark"))); + SendSizeUpdate(2048); +} + +// Confirm that required size updates are indeed required before the end. +TEST_F(HpackDecoderStateTest, RequiredTableSizeChangeBeforeEnd) { + decoder_state_.ApplyHeaderTableSizeSetting(1024); + SendStartAndVerifyCallback(); + EXPECT_CALL(listener_, OnHeaderErrorDetected( + HasSubstr("Missing dynamic table size update"))); + decoder_state_.OnHeaderBlockEnd(); +} + +// Confirm that optional size updates are validated. +TEST_F(HpackDecoderStateTest, InvalidOptionalSizeUpdate) { + // Require a size update, but provide one that isn't small enough. + SendStartAndVerifyCallback(); + EXPECT_EQ(Http2SettingsInfo::DefaultHeaderTableSize(), + header_table_size_limit()); + EXPECT_CALL(listener_, + OnHeaderErrorDetected(HasSubstr("size update is above"))); + SendSizeUpdate(Http2SettingsInfo::DefaultHeaderTableSize() + 1); +} + +TEST_F(HpackDecoderStateTest, InvalidStaticIndex) { + SendStartAndVerifyCallback(); + EXPECT_CALL(listener_, OnHeaderErrorDetected(HasSubstr("Invalid index"))); + decoder_state_.OnIndexedHeader(0); +} + +TEST_F(HpackDecoderStateTest, InvalidDynamicIndex) { + SendStartAndVerifyCallback(); + EXPECT_CALL(listener_, OnHeaderErrorDetected(HasSubstr("Invalid index"))); + decoder_state_.OnIndexedHeader(kFirstDynamicTableIndex); +} + +TEST_F(HpackDecoderStateTest, InvalidNameIndex) { + SendStartAndVerifyCallback(); + EXPECT_CALL(listener_, + OnHeaderErrorDetected(HasSubstr("Invalid name index"))); + SetValue("value", UNBUFFERED); + decoder_state_.OnNameIndexAndLiteralValue( + HpackEntryType::kIndexedLiteralHeader, kFirstDynamicTableIndex, + &value_buffer_); +} + +TEST_F(HpackDecoderStateTest, ErrorsSuppressCallbacks) { + SendStartAndVerifyCallback(); + EXPECT_CALL(listener_, + OnHeaderErrorDetected(Http2StringPiece("Huffman decode error."))); + decoder_state_.OnHpackDecodeError("Huffman decode error."); + + // Further decoded entries are ignored. + decoder_state_.OnIndexedHeader(1); + decoder_state_.OnDynamicTableSizeUpdate(1); + SetValue("value", UNBUFFERED); + decoder_state_.OnNameIndexAndLiteralValue( + HpackEntryType::kIndexedLiteralHeader, 4, &value_buffer_); + SetName("name", UNBUFFERED); + decoder_state_.OnLiteralNameAndValue(HpackEntryType::kIndexedLiteralHeader, + &name_buffer_, &value_buffer_); + decoder_state_.OnHeaderBlockEnd(); + decoder_state_.OnHpackDecodeError("NOT FORWARDED"); +} + +} // namespace +} // namespace test +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_decoder_string_buffer.cc b/http2/hpack/decoder/hpack_decoder_string_buffer.cc new file mode 100644 index 0000000..b20c37a --- /dev/null +++ b/http2/hpack/decoder/hpack_decoder_string_buffer.cc
@@ -0,0 +1,235 @@ +// Copyright 2016 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_decoder_string_buffer.h" + +#include <utility> + +#include "base/logging.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_bug_tracker.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_estimate_memory_usage.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string.h" + +namespace http2 { + +std::ostream& operator<<(std::ostream& out, + const HpackDecoderStringBuffer::State v) { + switch (v) { + case HpackDecoderStringBuffer::State::RESET: + return out << "RESET"; + case HpackDecoderStringBuffer::State::COLLECTING: + return out << "COLLECTING"; + case HpackDecoderStringBuffer::State::COMPLETE: + return out << "COMPLETE"; + } + // Since the value doesn't come over the wire, only a programming bug should + // result in reaching this point. + int unknown = static_cast<int>(v); + HTTP2_BUG << "Invalid HpackDecoderStringBuffer::State: " << unknown; + return out << "HpackDecoderStringBuffer::State(" << unknown << ")"; +} + +std::ostream& operator<<(std::ostream& out, + const HpackDecoderStringBuffer::Backing v) { + switch (v) { + case HpackDecoderStringBuffer::Backing::RESET: + return out << "RESET"; + case HpackDecoderStringBuffer::Backing::UNBUFFERED: + return out << "UNBUFFERED"; + case HpackDecoderStringBuffer::Backing::BUFFERED: + return out << "BUFFERED"; + case HpackDecoderStringBuffer::Backing::STATIC: + return out << "STATIC"; + } + // Since the value doesn't come over the wire, only a programming bug should + // result in reaching this point. + auto v2 = static_cast<int>(v); + HTTP2_BUG << "Invalid HpackDecoderStringBuffer::Backing: " << v2; + return out << "HpackDecoderStringBuffer::Backing(" << v2 << ")"; +} + +HpackDecoderStringBuffer::HpackDecoderStringBuffer() + : remaining_len_(0), + is_huffman_encoded_(false), + state_(State::RESET), + backing_(Backing::RESET) {} +HpackDecoderStringBuffer::~HpackDecoderStringBuffer() = default; + +void HpackDecoderStringBuffer::Reset() { + DVLOG(3) << "HpackDecoderStringBuffer::Reset"; + state_ = State::RESET; +} + +void HpackDecoderStringBuffer::Set(Http2StringPiece value, bool is_static) { + DVLOG(2) << "HpackDecoderStringBuffer::Set"; + DCHECK_EQ(state_, State::RESET); + value_ = value; + state_ = State::COMPLETE; + backing_ = is_static ? Backing::STATIC : Backing::UNBUFFERED; + // TODO(jamessynge): Determine which of these two fields must be set. + remaining_len_ = 0; + is_huffman_encoded_ = false; +} + +void HpackDecoderStringBuffer::OnStart(bool huffman_encoded, size_t len) { + DVLOG(2) << "HpackDecoderStringBuffer::OnStart"; + DCHECK_EQ(state_, State::RESET); + + remaining_len_ = len; + is_huffman_encoded_ = huffman_encoded; + state_ = State::COLLECTING; + + if (huffman_encoded) { + // We don't set, clear or use value_ for buffered strings until OnEnd. + decoder_.Reset(); + buffer_.clear(); + backing_ = Backing::BUFFERED; + + // Reserve space in buffer_ for the uncompressed string, assuming the + // maximum expansion. The shortest Huffman codes in the RFC are 5 bits long, + // which then expand to 8 bits during decoding (i.e. each code is for one + // plain text octet, aka byte), so the maximum size is 60% longer than the + // encoded size. + len = len * 8 / 5; + if (buffer_.capacity() < len) { + buffer_.reserve(len); + } + } else { + // Assume for now that we won't need to use buffer_, so don't reserve space + // in it. + backing_ = Backing::RESET; + // OnData is not called for empty (zero length) strings, so make sure that + // value_ is cleared. + value_ = Http2StringPiece(); + } +} + +bool HpackDecoderStringBuffer::OnData(const char* data, size_t len) { + DVLOG(2) << "HpackDecoderStringBuffer::OnData state=" << state_ + << ", backing=" << backing_; + DCHECK_EQ(state_, State::COLLECTING); + DCHECK_LE(len, remaining_len_); + remaining_len_ -= len; + + if (is_huffman_encoded_) { + DCHECK_EQ(backing_, Backing::BUFFERED); + return decoder_.Decode(Http2StringPiece(data, len), &buffer_); + } + + if (backing_ == Backing::RESET) { + // This is the first call to OnData. If data contains the entire string, + // don't copy the string. If we later find that the HPACK entry is split + // across input buffers, then we'll copy the string into buffer_. + if (remaining_len_ == 0) { + value_ = Http2StringPiece(data, len); + backing_ = Backing::UNBUFFERED; + return true; + } + + // We need to buffer the string because it is split across input buffers. + // Reserve space in buffer_ for the entire string. + backing_ = Backing::BUFFERED; + buffer_.reserve(remaining_len_ + len); + buffer_.assign(data, len); + return true; + } + + // This is not the first call to OnData for this string, so it should be + // buffered. + DCHECK_EQ(backing_, Backing::BUFFERED); + + // Append to the current contents of the buffer. + buffer_.append(data, len); + return true; +} + +bool HpackDecoderStringBuffer::OnEnd() { + DVLOG(2) << "HpackDecoderStringBuffer::OnEnd"; + DCHECK_EQ(state_, State::COLLECTING); + DCHECK_EQ(0u, remaining_len_); + + if (is_huffman_encoded_) { + DCHECK_EQ(backing_, Backing::BUFFERED); + // Did the Huffman encoding of the string end properly? + if (!decoder_.InputProperlyTerminated()) { + return false; // No, it didn't. + } + value_ = buffer_; + } else if (backing_ == Backing::BUFFERED) { + value_ = buffer_; + } + state_ = State::COMPLETE; + return true; +} + +void HpackDecoderStringBuffer::BufferStringIfUnbuffered() { + DVLOG(3) << "HpackDecoderStringBuffer::BufferStringIfUnbuffered state=" + << state_ << ", backing=" << backing_; + if (state_ != State::RESET && backing_ == Backing::UNBUFFERED) { + DVLOG(2) << "HpackDecoderStringBuffer buffering Http2String of length " + << value_.size(); + buffer_.assign(value_.data(), value_.size()); + if (state_ == State::COMPLETE) { + value_ = buffer_; + } + backing_ = Backing::BUFFERED; + } +} + +bool HpackDecoderStringBuffer::IsBuffered() const { + DVLOG(3) << "HpackDecoderStringBuffer::IsBuffered"; + return state_ != State::RESET && backing_ == Backing::BUFFERED; +} + +size_t HpackDecoderStringBuffer::BufferedLength() const { + DVLOG(3) << "HpackDecoderStringBuffer::BufferedLength"; + return IsBuffered() ? buffer_.size() : 0; +} + +Http2StringPiece HpackDecoderStringBuffer::str() const { + DVLOG(3) << "HpackDecoderStringBuffer::str"; + DCHECK_EQ(state_, State::COMPLETE); + return value_; +} + +Http2String HpackDecoderStringBuffer::ReleaseString() { + DVLOG(3) << "HpackDecoderStringBuffer::ReleaseString"; + DCHECK_EQ(state_, State::COMPLETE); + DCHECK_EQ(backing_, Backing::BUFFERED); + if (state_ == State::COMPLETE) { + state_ = State::RESET; + if (backing_ == Backing::BUFFERED) { + return std::move(buffer_); + } else { + return Http2String(value_); + } + } + return ""; +} + +void HpackDecoderStringBuffer::OutputDebugStringTo(std::ostream& out) const { + out << "{state=" << state_; + if (state_ != State::RESET) { + out << ", backing=" << backing_; + out << ", remaining_len=" << remaining_len_; + out << ", is_huffman_encoded=" << is_huffman_encoded_; + if (backing_ == Backing::BUFFERED) { + out << ", buffer: " << buffer_; + } else { + out << ", value: " << value_; + } + } + out << "}"; +} + +size_t HpackDecoderStringBuffer::EstimateMemoryUsage() const { + return Http2EstimateMemoryUsage(buffer_); +} + +std::ostream& operator<<(std::ostream& out, const HpackDecoderStringBuffer& v) { + v.OutputDebugStringTo(out); + return out; +} + +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_decoder_string_buffer.h b/http2/hpack/decoder/hpack_decoder_string_buffer.h new file mode 100644 index 0000000..8a810b2 --- /dev/null +++ b/http2/hpack/decoder/hpack_decoder_string_buffer.h
@@ -0,0 +1,102 @@ +// Copyright 2016 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. + +#ifndef QUICHE_HTTP2_HPACK_DECODER_HPACK_DECODER_STRING_BUFFER_H_ +#define QUICHE_HTTP2_HPACK_DECODER_HPACK_DECODER_STRING_BUFFER_H_ + +// HpackDecoderStringBuffer helps an HPACK decoder to avoid copies of a string +// literal (name or value) except when necessary (e.g. when split across two +// or more HPACK block fragments). + +#include <stddef.h> + +#include <ostream> + +#include "net/third_party/quiche/src/http2/hpack/huffman/hpack_huffman_decoder.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_export.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string_piece.h" + +namespace http2 { + +class HTTP2_EXPORT_PRIVATE HpackDecoderStringBuffer { + public: + enum class State : uint8_t { RESET, COLLECTING, COMPLETE }; + enum class Backing : uint8_t { RESET, UNBUFFERED, BUFFERED, STATIC }; + + HpackDecoderStringBuffer(); + ~HpackDecoderStringBuffer(); + + HpackDecoderStringBuffer(const HpackDecoderStringBuffer&) = delete; + HpackDecoderStringBuffer& operator=(const HpackDecoderStringBuffer&) = delete; + + void Reset(); + void Set(Http2StringPiece value, bool is_static); + + // Note that for Huffman encoded strings the length of the string after + // decoding may be larger (expected), the same or even smaller; the latter + // are unlikely, but possible if the encoder makes odd choices. + void OnStart(bool huffman_encoded, size_t len); + bool OnData(const char* data, size_t len); + bool OnEnd(); + void BufferStringIfUnbuffered(); + bool IsBuffered() const; + size_t BufferedLength() const; + + // Accessors for the completely collected string (i.e. Set or OnEnd has just + // been called, and no reset of the state has occurred). + + // Returns a Http2StringPiece pointing to the backing store for the string, + // either the internal buffer or the original transport buffer (e.g. for a + // literal value that wasn't Huffman encoded, and that wasn't split across + // transport buffers). + Http2StringPiece str() const; + + // Returns the completely collected string by value, using std::move in an + // effort to avoid unnecessary copies. ReleaseString() must not be called + // unless the string has been buffered (to avoid forcing a potentially + // unnecessary copy). ReleaseString() also resets the instance so that it can + // be used to collect another string. + Http2String ReleaseString(); + + State state_for_testing() const { return state_; } + Backing backing_for_testing() const { return backing_; } + void OutputDebugStringTo(std::ostream& out) const; + + // Returns the estimate of dynamically allocated memory in bytes. + size_t EstimateMemoryUsage() const; + + private: + // Storage for the string being buffered, if buffering is necessary + // (e.g. if Huffman encoded, buffer_ is storage for the decoded string). + Http2String buffer_; + + // The Http2StringPiece to be returned by HpackDecoderStringBuffer::str(). If + // a string has been collected, but not buffered, value_ points to that + // string. + Http2StringPiece value_; + + // The decoder to use if the string is Huffman encoded. + HpackHuffmanDecoder decoder_; + + // Count of bytes not yet passed to OnData. + size_t remaining_len_; + + // Is the HPACK string Huffman encoded? + bool is_huffman_encoded_; + + // State of the string decoding process. + State state_; + + // Where is the string stored? + Backing backing_; +}; + +HTTP2_EXPORT_PRIVATE std::ostream& operator<<( + std::ostream& out, + const HpackDecoderStringBuffer& v); + +} // namespace http2 + +#endif // QUICHE_HTTP2_HPACK_DECODER_HPACK_DECODER_STRING_BUFFER_H_
diff --git a/http2/hpack/decoder/hpack_decoder_string_buffer_test.cc b/http2/hpack/decoder/hpack_decoder_string_buffer_test.cc new file mode 100644 index 0000000..009b894 --- /dev/null +++ b/http2/hpack/decoder/hpack_decoder_string_buffer_test.cc
@@ -0,0 +1,251 @@ +// Copyright 2016 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_decoder_string_buffer.h" + +// Tests of HpackDecoderStringBuffer. + +#include <initializer_list> + +#include "base/logging.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string_utils.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_test_helpers.h" + +using ::testing::AssertionResult; +using ::testing::AssertionSuccess; +using ::testing::HasSubstr; + +namespace http2 { +namespace test { +namespace { + +class HpackDecoderStringBufferTest : public ::testing::Test { + protected: + typedef HpackDecoderStringBuffer::State State; + typedef HpackDecoderStringBuffer::Backing Backing; + + State state() const { return buf_.state_for_testing(); } + Backing backing() const { return buf_.backing_for_testing(); } + + // We want to know that LOG(x) << buf_ will work in production should that + // be needed, so we test that it outputs the expected values. + AssertionResult VerifyLogHasSubstrs(std::initializer_list<Http2String> strs) { + VLOG(1) << buf_; + std::ostringstream ss; + buf_.OutputDebugStringTo(ss); + Http2String dbg_str(ss.str()); + for (const auto& expected : strs) { + VERIFY_THAT(dbg_str, HasSubstr(expected)); + } + return AssertionSuccess(); + } + + HpackDecoderStringBuffer buf_; +}; + +TEST_F(HpackDecoderStringBufferTest, SetStatic) { + Http2StringPiece data("static string"); + + EXPECT_EQ(state(), State::RESET); + EXPECT_TRUE(VerifyLogHasSubstrs({"state=RESET"})); + + buf_.Set(data, /*is_static*/ true); + LOG(INFO) << buf_; + EXPECT_EQ(state(), State::COMPLETE); + EXPECT_EQ(backing(), Backing::STATIC); + EXPECT_EQ(data, buf_.str()); + EXPECT_EQ(data.data(), buf_.str().data()); + EXPECT_TRUE(VerifyLogHasSubstrs( + {"state=COMPLETE", "backing=STATIC", "value: static string"})); + + // The string is static, so BufferStringIfUnbuffered won't change anything. + buf_.BufferStringIfUnbuffered(); + EXPECT_EQ(state(), State::COMPLETE); + EXPECT_EQ(backing(), Backing::STATIC); + EXPECT_EQ(data, buf_.str()); + EXPECT_EQ(data.data(), buf_.str().data()); + EXPECT_TRUE(VerifyLogHasSubstrs( + {"state=COMPLETE", "backing=STATIC", "value: static string"})); +} + +TEST_F(HpackDecoderStringBufferTest, PlainWhole) { + Http2StringPiece data("some text."); + + LOG(INFO) << buf_; + EXPECT_EQ(state(), State::RESET); + + buf_.OnStart(/*huffman_encoded*/ false, data.size()); + EXPECT_EQ(state(), State::COLLECTING); + EXPECT_EQ(backing(), Backing::RESET); + LOG(INFO) << buf_; + + EXPECT_TRUE(buf_.OnData(data.data(), data.size())); + EXPECT_EQ(state(), State::COLLECTING); + EXPECT_EQ(backing(), Backing::UNBUFFERED); + + EXPECT_TRUE(buf_.OnEnd()); + EXPECT_EQ(state(), State::COMPLETE); + EXPECT_EQ(backing(), Backing::UNBUFFERED); + EXPECT_EQ(0u, buf_.BufferedLength()); + EXPECT_TRUE(VerifyLogHasSubstrs( + {"state=COMPLETE", "backing=UNBUFFERED", "value: some text."})); + + // We expect that the string buffer points to the passed in Http2StringPiece's + // backing store. + EXPECT_EQ(data.data(), buf_.str().data()); + + // Now force it to buffer the string, after which it will still have the same + // string value, but the backing store will be different. + buf_.BufferStringIfUnbuffered(); + LOG(INFO) << buf_; + EXPECT_EQ(backing(), Backing::BUFFERED); + EXPECT_EQ(buf_.BufferedLength(), data.size()); + EXPECT_EQ(data, buf_.str()); + EXPECT_NE(data.data(), buf_.str().data()); + EXPECT_TRUE(VerifyLogHasSubstrs( + {"state=COMPLETE", "backing=BUFFERED", "buffer: some text."})); +} + +TEST_F(HpackDecoderStringBufferTest, PlainSplit) { + Http2StringPiece data("some text."); + Http2StringPiece part1 = data.substr(0, 1); + Http2StringPiece part2 = data.substr(1); + + EXPECT_EQ(state(), State::RESET); + buf_.OnStart(/*huffman_encoded*/ false, data.size()); + EXPECT_EQ(state(), State::COLLECTING); + EXPECT_EQ(backing(), Backing::RESET); + + // OnData with only a part of the data, not the whole, so buf_ will buffer + // the data. + EXPECT_TRUE(buf_.OnData(part1.data(), part1.size())); + EXPECT_EQ(state(), State::COLLECTING); + EXPECT_EQ(backing(), Backing::BUFFERED); + EXPECT_EQ(buf_.BufferedLength(), part1.size()); + LOG(INFO) << buf_; + + EXPECT_TRUE(buf_.OnData(part2.data(), part2.size())); + EXPECT_EQ(state(), State::COLLECTING); + EXPECT_EQ(backing(), Backing::BUFFERED); + EXPECT_EQ(buf_.BufferedLength(), data.size()); + + EXPECT_TRUE(buf_.OnEnd()); + EXPECT_EQ(state(), State::COMPLETE); + EXPECT_EQ(backing(), Backing::BUFFERED); + EXPECT_EQ(buf_.BufferedLength(), data.size()); + LOG(INFO) << buf_; + + Http2StringPiece buffered = buf_.str(); + EXPECT_EQ(data, buffered); + EXPECT_NE(data.data(), buffered.data()); + + // The string is already buffered, so BufferStringIfUnbuffered should not make + // any change. + buf_.BufferStringIfUnbuffered(); + EXPECT_EQ(backing(), Backing::BUFFERED); + EXPECT_EQ(buf_.BufferedLength(), data.size()); + EXPECT_EQ(buffered, buf_.str()); + EXPECT_EQ(buffered.data(), buf_.str().data()); +} + +TEST_F(HpackDecoderStringBufferTest, HuffmanWhole) { + Http2String encoded = Http2HexDecode("f1e3c2e5f23a6ba0ab90f4ff"); + Http2StringPiece decoded("www.example.com"); + + EXPECT_EQ(state(), State::RESET); + buf_.OnStart(/*huffman_encoded*/ true, encoded.size()); + EXPECT_EQ(state(), State::COLLECTING); + + EXPECT_TRUE(buf_.OnData(encoded.data(), encoded.size())); + EXPECT_EQ(state(), State::COLLECTING); + EXPECT_EQ(backing(), Backing::BUFFERED); + + EXPECT_TRUE(buf_.OnEnd()); + EXPECT_EQ(state(), State::COMPLETE); + EXPECT_EQ(backing(), Backing::BUFFERED); + EXPECT_EQ(buf_.BufferedLength(), decoded.size()); + EXPECT_EQ(decoded, buf_.str()); + EXPECT_TRUE(VerifyLogHasSubstrs( + {"{state=COMPLETE", "backing=BUFFERED", "buffer: www.example.com}"})); + + Http2String s = buf_.ReleaseString(); + EXPECT_EQ(s, decoded); + EXPECT_EQ(state(), State::RESET); +} + +TEST_F(HpackDecoderStringBufferTest, HuffmanSplit) { + Http2String encoded = Http2HexDecode("f1e3c2e5f23a6ba0ab90f4ff"); + Http2String part1 = encoded.substr(0, 5); + Http2String part2 = encoded.substr(5); + Http2StringPiece decoded("www.example.com"); + + EXPECT_EQ(state(), State::RESET); + buf_.OnStart(/*huffman_encoded*/ true, encoded.size()); + EXPECT_EQ(state(), State::COLLECTING); + EXPECT_EQ(backing(), Backing::BUFFERED); + EXPECT_EQ(0u, buf_.BufferedLength()); + LOG(INFO) << buf_; + + EXPECT_TRUE(buf_.OnData(part1.data(), part1.size())); + EXPECT_EQ(state(), State::COLLECTING); + EXPECT_EQ(backing(), Backing::BUFFERED); + EXPECT_GT(buf_.BufferedLength(), 0u); + EXPECT_LT(buf_.BufferedLength(), decoded.size()); + LOG(INFO) << buf_; + + EXPECT_TRUE(buf_.OnData(part2.data(), part2.size())); + EXPECT_EQ(state(), State::COLLECTING); + EXPECT_EQ(backing(), Backing::BUFFERED); + EXPECT_EQ(buf_.BufferedLength(), decoded.size()); + LOG(INFO) << buf_; + + EXPECT_TRUE(buf_.OnEnd()); + EXPECT_EQ(state(), State::COMPLETE); + EXPECT_EQ(backing(), Backing::BUFFERED); + EXPECT_EQ(buf_.BufferedLength(), decoded.size()); + EXPECT_EQ(decoded, buf_.str()); + LOG(INFO) << buf_; + + buf_.Reset(); + EXPECT_EQ(state(), State::RESET); + LOG(INFO) << buf_; +} + +TEST_F(HpackDecoderStringBufferTest, InvalidHuffmanOnData) { + // Explicitly encode the End-of-String symbol, a no-no. + Http2String encoded = Http2HexDecode("ffffffff"); + + buf_.OnStart(/*huffman_encoded*/ true, encoded.size()); + EXPECT_EQ(state(), State::COLLECTING); + + EXPECT_FALSE(buf_.OnData(encoded.data(), encoded.size())); + EXPECT_EQ(state(), State::COLLECTING); + EXPECT_EQ(backing(), Backing::BUFFERED); + + LOG(INFO) << buf_; +} + +TEST_F(HpackDecoderStringBufferTest, InvalidHuffmanOnEnd) { + // Last byte of string doesn't end with prefix of End-of-String symbol. + Http2String encoded = Http2HexDecode("00"); + + buf_.OnStart(/*huffman_encoded*/ true, encoded.size()); + EXPECT_EQ(state(), State::COLLECTING); + + EXPECT_TRUE(buf_.OnData(encoded.data(), encoded.size())); + EXPECT_EQ(state(), State::COLLECTING); + EXPECT_EQ(backing(), Backing::BUFFERED); + + EXPECT_FALSE(buf_.OnEnd()); + LOG(INFO) << buf_; +} + +// TODO(jamessynge): Add tests for ReleaseString(). + +} // namespace +} // namespace test +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_decoder_tables.cc b/http2/hpack/decoder/hpack_decoder_tables.cc new file mode 100644 index 0000000..5002ab1 --- /dev/null +++ b/http2/hpack/decoder/hpack_decoder_tables.cc
@@ -0,0 +1,153 @@ +// Copyright 2016 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_decoder_tables.h" + +#include "base/logging.h" +#include "net/third_party/quiche/src/http2/hpack/http2_hpack_constants.h" + +namespace http2 { +namespace { + +std::vector<HpackStringPair>* MakeStaticTable() { + auto* ptr = new std::vector<HpackStringPair>(); + ptr->reserve(kFirstDynamicTableIndex); + ptr->emplace_back("", ""); + +#define STATIC_TABLE_ENTRY(name, value, index) \ + DCHECK_EQ(ptr->size(), index); \ + ptr->emplace_back(name, value) + +#include "net/third_party/quiche/src/http2/hpack/hpack_static_table_entries.inc" + +#undef STATIC_TABLE_ENTRY + + return ptr; +} + +const std::vector<HpackStringPair>* GetStaticTable() { + static const std::vector<HpackStringPair>* const g_static_table = + MakeStaticTable(); + return g_static_table; +} + +} // namespace + +HpackDecoderTablesDebugListener::HpackDecoderTablesDebugListener() = default; +HpackDecoderTablesDebugListener::~HpackDecoderTablesDebugListener() = default; + +HpackDecoderStaticTable::HpackDecoderStaticTable( + const std::vector<HpackStringPair>* table) + : table_(table) {} + +HpackDecoderStaticTable::HpackDecoderStaticTable() : table_(GetStaticTable()) {} + +const HpackStringPair* HpackDecoderStaticTable::Lookup(size_t index) const { + if (0 < index && index < kFirstDynamicTableIndex) { + return &((*table_)[index]); + } + return nullptr; +} + +HpackDecoderDynamicTable::HpackDecoderTableEntry::HpackDecoderTableEntry( + const HpackString& name, + const HpackString& value) + : HpackStringPair(name, value) {} + +HpackDecoderDynamicTable::HpackDecoderDynamicTable() + : insert_count_(kFirstDynamicTableIndex - 1), debug_listener_(nullptr) {} +HpackDecoderDynamicTable::~HpackDecoderDynamicTable() = default; + +void HpackDecoderDynamicTable::DynamicTableSizeUpdate(size_t size_limit) { + DVLOG(3) << "HpackDecoderDynamicTable::DynamicTableSizeUpdate " << size_limit; + EnsureSizeNoMoreThan(size_limit); + DCHECK_LE(current_size_, size_limit); + size_limit_ = size_limit; +} + +// TODO(jamessynge): Check somewhere before here that names received from the +// peer are valid (e.g. are lower-case, no whitespace, etc.). +bool HpackDecoderDynamicTable::Insert(const HpackString& name, + const HpackString& value) { + HpackDecoderTableEntry entry(name, value); + size_t entry_size = entry.size(); + DVLOG(2) << "InsertEntry of size=" << entry_size << "\n name: " << name + << "\n value: " << value; + if (entry_size > size_limit_) { + DVLOG(2) << "InsertEntry: entry larger than table, removing " + << table_.size() << " entries, of total size " << current_size_ + << " bytes."; + table_.clear(); + current_size_ = 0; + return false; // Not inserted because too large. + } + ++insert_count_; + if (debug_listener_ != nullptr) { + entry.time_added = debug_listener_->OnEntryInserted(entry, insert_count_); + DVLOG(2) << "OnEntryInserted returned time_added=" << entry.time_added + << " for insert_count_=" << insert_count_; + } + size_t insert_limit = size_limit_ - entry_size; + EnsureSizeNoMoreThan(insert_limit); + table_.push_front(entry); + current_size_ += entry_size; + DVLOG(2) << "InsertEntry: current_size_=" << current_size_; + DCHECK_GE(current_size_, entry_size); + DCHECK_LE(current_size_, size_limit_); + return true; +} + +const HpackStringPair* HpackDecoderDynamicTable::Lookup(size_t index) const { + if (index < table_.size()) { + const HpackDecoderTableEntry& entry = table_[index]; + if (debug_listener_ != nullptr) { + size_t insert_count_of_index = insert_count_ + table_.size() - index; + debug_listener_->OnUseEntry(entry, insert_count_of_index, + entry.time_added); + } + return &entry; + } + return nullptr; +} + +void HpackDecoderDynamicTable::EnsureSizeNoMoreThan(size_t limit) { + DVLOG(2) << "EnsureSizeNoMoreThan limit=" << limit + << ", current_size_=" << current_size_; + // Not the most efficient choice, but any easy way to start. + while (current_size_ > limit) { + RemoveLastEntry(); + } + DCHECK_LE(current_size_, limit); +} + +void HpackDecoderDynamicTable::RemoveLastEntry() { + DCHECK(!table_.empty()); + if (!table_.empty()) { + DVLOG(2) << "RemoveLastEntry current_size_=" << current_size_ + << ", last entry size=" << table_.back().size(); + DCHECK_GE(current_size_, table_.back().size()); + current_size_ -= table_.back().size(); + table_.pop_back(); + // Empty IFF current_size_ == 0. + DCHECK_EQ(table_.empty(), current_size_ == 0); + } +} + +HpackDecoderTables::HpackDecoderTables() = default; +HpackDecoderTables::~HpackDecoderTables() = default; + +void HpackDecoderTables::set_debug_listener( + HpackDecoderTablesDebugListener* debug_listener) { + dynamic_table_.set_debug_listener(debug_listener); +} + +const HpackStringPair* HpackDecoderTables::Lookup(size_t index) const { + if (index < kFirstDynamicTableIndex) { + return static_table_.Lookup(index); + } else { + return dynamic_table_.Lookup(index - kFirstDynamicTableIndex); + } +} + +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_decoder_tables.h b/http2/hpack/decoder/hpack_decoder_tables.h new file mode 100644 index 0000000..6df145c --- /dev/null +++ b/http2/hpack/decoder/hpack_decoder_tables.h
@@ -0,0 +1,197 @@ +// Copyright 2016 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. + +#ifndef QUICHE_HTTP2_HPACK_DECODER_HPACK_DECODER_TABLES_H_ +#define QUICHE_HTTP2_HPACK_DECODER_HPACK_DECODER_TABLES_H_ + +// Static and dynamic tables for the HPACK decoder. See: +// http://httpwg.org/specs/rfc7541.html#indexing.tables + +// Note that the Lookup methods return nullptr if the requested index was not +// found. This should be treated as a COMPRESSION error according to the HTTP/2 +// spec, which is a connection level protocol error (i.e. the connection must +// be terminated). See these sections in the two RFCs: +// http://httpwg.org/specs/rfc7541.html#indexed.header.representation +// http://httpwg.org/specs/rfc7541.html#index.address.space +// http://httpwg.org/specs/rfc7540.html#HeaderBlock + +#include <stddef.h> + +#include <cstdint> +#include <vector> + +#include "net/third_party/quiche/src/http2/hpack/hpack_string.h" +#include "net/third_party/quiche/src/http2/http2_constants.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_containers.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_export.h" + +namespace http2 { +namespace test { +class HpackDecoderTablesPeer; +} // namespace test + +// HpackDecoderTablesDebugListener supports a QUIC experiment, enabling +// the gathering of information about the time-line of use of HPACK +// dynamic table entries. +class HTTP2_EXPORT_PRIVATE HpackDecoderTablesDebugListener { + public: + HpackDecoderTablesDebugListener(); + virtual ~HpackDecoderTablesDebugListener(); + + HpackDecoderTablesDebugListener(const HpackDecoderTablesDebugListener&) = + delete; + HpackDecoderTablesDebugListener& operator=( + const HpackDecoderTablesDebugListener&) = delete; + + // The entry has been inserted into the dynamic table. insert_count starts at + // 62 because 61 is the last index in the static table; insert_count increases + // by 1 with each insert into the dynamic table; it is not incremented when + // when a entry is too large to fit into the dynamic table at all (which has + // the effect of emptying the dynamic table). + // Returns a value that can be used as time_added in OnUseEntry. + virtual int64_t OnEntryInserted(const HpackStringPair& entry, + size_t insert_count) = 0; + + // The entry has been used, either for the name or for the name and value. + // insert_count is the same as passed to OnEntryInserted when entry was + // inserted to the dynamic table, and time_added is the value that was + // returned by OnEntryInserted. + virtual void OnUseEntry(const HpackStringPair& entry, + size_t insert_count, + int64_t time_added) = 0; +}; + +// See http://httpwg.org/specs/rfc7541.html#static.table.definition for the +// contents, and http://httpwg.org/specs/rfc7541.html#index.address.space for +// info about accessing the static table. +class HTTP2_EXPORT_PRIVATE HpackDecoderStaticTable { + public: + explicit HpackDecoderStaticTable(const std::vector<HpackStringPair>* table); + // Uses a global table shared by all threads. + HpackDecoderStaticTable(); + + // If index is valid, returns a pointer to the entry, otherwise returns + // nullptr. + const HpackStringPair* Lookup(size_t index) const; + + private: + friend class test::HpackDecoderTablesPeer; + const std::vector<HpackStringPair>* const table_; +}; + +// HpackDecoderDynamicTable implements HPACK compression feature "indexed +// headers"; previously sent headers may be referenced later by their index +// in the dynamic table. See these sections of the RFC: +// http://httpwg.org/specs/rfc7541.html#dynamic.table +// http://httpwg.org/specs/rfc7541.html#dynamic.table.management +class HTTP2_EXPORT_PRIVATE HpackDecoderDynamicTable { + public: + HpackDecoderDynamicTable(); + ~HpackDecoderDynamicTable(); + + HpackDecoderDynamicTable(const HpackDecoderDynamicTable&) = delete; + HpackDecoderDynamicTable& operator=(const HpackDecoderDynamicTable&) = delete; + + // Set the listener to be notified of insertions into this table, and later + // uses of those entries. Added for evaluation of changes to QUIC's use + // of HPACK. + void set_debug_listener(HpackDecoderTablesDebugListener* debug_listener) { + debug_listener_ = debug_listener; + } + + // Sets a new size limit, received from the peer; performs evictions if + // necessary to ensure that the current size does not exceed the new limit. + // The caller needs to have validated that size_limit does not + // exceed the acknowledged value of SETTINGS_HEADER_TABLE_SIZE. + void DynamicTableSizeUpdate(size_t size_limit); + + // Returns true if inserted, false if too large (at which point the + // dynamic table will be empty.) + bool Insert(const HpackString& name, const HpackString& value); + + // If index is valid, returns a pointer to the entry, otherwise returns + // nullptr. + const HpackStringPair* Lookup(size_t index) const; + + size_t size_limit() const { return size_limit_; } + size_t current_size() const { return current_size_; } + + private: + friend class test::HpackDecoderTablesPeer; + struct HpackDecoderTableEntry : public HpackStringPair { + HpackDecoderTableEntry(const HpackString& name, const HpackString& value); + int64_t time_added; + }; + + // Drop older entries to ensure the size is not greater than limit. + void EnsureSizeNoMoreThan(size_t limit); + + // Removes the oldest dynamic table entry. + void RemoveLastEntry(); + + Http2Deque<HpackDecoderTableEntry> table_; + + // The last received DynamicTableSizeUpdate value, initialized to + // SETTINGS_HEADER_TABLE_SIZE. + size_t size_limit_ = Http2SettingsInfo::DefaultHeaderTableSize(); + + size_t current_size_ = 0; + + // insert_count_ and debug_listener_ are used by a QUIC experiment; remove + // when the experiment is done. + size_t insert_count_; + HpackDecoderTablesDebugListener* debug_listener_; +}; + +class HTTP2_EXPORT_PRIVATE HpackDecoderTables { + public: + HpackDecoderTables(); + ~HpackDecoderTables(); + + HpackDecoderTables(const HpackDecoderTables&) = delete; + HpackDecoderTables& operator=(const HpackDecoderTables&) = delete; + + // Set the listener to be notified of insertions into the dynamic table, and + // later uses of those entries. Added for evaluation of changes to QUIC's use + // of HPACK. + void set_debug_listener(HpackDecoderTablesDebugListener* debug_listener); + + // Sets a new size limit, received from the peer; performs evictions if + // necessary to ensure that the current size does not exceed the new limit. + // The caller needs to have validated that size_limit does not + // exceed the acknowledged value of SETTINGS_HEADER_TABLE_SIZE. + void DynamicTableSizeUpdate(size_t size_limit) { + dynamic_table_.DynamicTableSizeUpdate(size_limit); + } + + // Returns true if inserted, false if too large (at which point the + // dynamic table will be empty.) + // TODO(jamessynge): Add methods for moving the string(s) into the table, + // or for otherwise avoiding unnecessary copies. + bool Insert(const HpackString& name, const HpackString& value) { + return dynamic_table_.Insert(name, value); + } + + // If index is valid, returns a pointer to the entry, otherwise returns + // nullptr. + const HpackStringPair* Lookup(size_t index) const; + + // The size limit that the peer (the HPACK encoder) has told the decoder it is + // currently operating with. Defaults to SETTINGS_HEADER_TABLE_SIZE, 4096. + size_t header_table_size_limit() const { return dynamic_table_.size_limit(); } + + // Sum of the sizes of the dynamic table entries. + size_t current_header_table_size() const { + return dynamic_table_.current_size(); + } + + private: + friend class test::HpackDecoderTablesPeer; + HpackDecoderStaticTable static_table_; + HpackDecoderDynamicTable dynamic_table_; +}; + +} // namespace http2 + +#endif // QUICHE_HTTP2_HPACK_DECODER_HPACK_DECODER_TABLES_H_
diff --git a/http2/hpack/decoder/hpack_decoder_tables_test.cc b/http2/hpack/decoder/hpack_decoder_tables_test.cc new file mode 100644 index 0000000..370c1a5 --- /dev/null +++ b/http2/hpack/decoder/hpack_decoder_tables_test.cc
@@ -0,0 +1,266 @@ +// Copyright 2016 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_decoder_tables.h" + +#include <algorithm> +#include <tuple> +#include <vector> + +#include "base/logging.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "net/third_party/quiche/src/http2/hpack/http2_hpack_constants.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_test_helpers.h" +#include "net/third_party/quiche/src/http2/test_tools/http2_random.h" +#include "net/third_party/quiche/src/http2/tools/random_util.h" + +using ::testing::AssertionResult; +using ::testing::AssertionSuccess; + +namespace http2 { +namespace test { +class HpackDecoderTablesPeer { + public: + static size_t num_dynamic_entries(const HpackDecoderTables& tables) { + return tables.dynamic_table_.table_.size(); + } +}; + +namespace { +struct StaticEntry { + const char* name; + const char* value; + size_t index; +}; + +std::vector<StaticEntry> MakeSpecStaticEntries() { + std::vector<StaticEntry> static_entries; + +#define STATIC_TABLE_ENTRY(name, value, index) \ + DCHECK_EQ(static_entries.size() + 1, index); \ + static_entries.push_back({name, value, index}); + +#include "net/third_party/quiche/src/http2/hpack/hpack_static_table_entries.inc" + +#undef STATIC_TABLE_ENTRY + + return static_entries; +} + +template <class C> +void ShuffleCollection(C* collection, Http2Random* r) { + std::shuffle(collection->begin(), collection->end(), *r); +} + +class HpackDecoderStaticTableTest : public ::testing::Test { + protected: + HpackDecoderStaticTableTest() = default; + + std::vector<StaticEntry> shuffled_static_entries() { + std::vector<StaticEntry> entries = MakeSpecStaticEntries(); + ShuffleCollection(&entries, &random_); + return entries; + } + + // This test is in a function so that it can be applied to both the static + // table and the combined static+dynamic tables. + AssertionResult VerifyStaticTableContents() { + for (const auto& expected : shuffled_static_entries()) { + const HpackStringPair* found = Lookup(expected.index); + VERIFY_NE(found, nullptr); + VERIFY_EQ(expected.name, found->name) << expected.index; + VERIFY_EQ(expected.value, found->value) << expected.index; + } + + // There should be no entry with index 0. + VERIFY_EQ(nullptr, Lookup(0)); + return AssertionSuccess(); + } + + virtual const HpackStringPair* Lookup(size_t index) { + return static_table_.Lookup(index); + } + + Http2Random* RandomPtr() { return &random_; } + + Http2Random random_; + + private: + HpackDecoderStaticTable static_table_; +}; + +TEST_F(HpackDecoderStaticTableTest, StaticTableContents) { + EXPECT_TRUE(VerifyStaticTableContents()); +} + +size_t Size(const Http2String& name, const Http2String& value) { + return name.size() + value.size() + 32; +} + +// To support tests with more than a few of hand crafted changes to the dynamic +// table, we have another, exceedingly simple, implementation of the HPACK +// dynamic table containing FakeHpackEntry instances. We can thus compare the +// contents of the actual table with those in fake_dynamic_table_. + +typedef std::tuple<Http2String, Http2String, size_t> FakeHpackEntry; +const Http2String& Name(const FakeHpackEntry& entry) { + return std::get<0>(entry); +} +const Http2String& Value(const FakeHpackEntry& entry) { + return std::get<1>(entry); +} +size_t Size(const FakeHpackEntry& entry) { + return std::get<2>(entry); +} + +class HpackDecoderTablesTest : public HpackDecoderStaticTableTest { + protected: + const HpackStringPair* Lookup(size_t index) override { + return tables_.Lookup(index); + } + + size_t dynamic_size_limit() const { + return tables_.header_table_size_limit(); + } + size_t current_dynamic_size() const { + return tables_.current_header_table_size(); + } + size_t num_dynamic_entries() const { + return HpackDecoderTablesPeer::num_dynamic_entries(tables_); + } + + // Insert the name and value into fake_dynamic_table_. + void FakeInsert(const Http2String& name, const Http2String& value) { + FakeHpackEntry entry(name, value, Size(name, value)); + fake_dynamic_table_.insert(fake_dynamic_table_.begin(), entry); + } + + // Add up the size of all entries in fake_dynamic_table_. + size_t FakeSize() { + size_t sz = 0; + for (const auto& entry : fake_dynamic_table_) { + sz += Size(entry); + } + return sz; + } + + // If the total size of the fake_dynamic_table_ is greater than limit, + // keep the first N entries such that those N entries have a size not + // greater than limit, and such that keeping entry N+1 would have a size + // greater than limit. Returns the count of removed bytes. + size_t FakeTrim(size_t limit) { + size_t original_size = FakeSize(); + size_t total_size = 0; + for (size_t ndx = 0; ndx < fake_dynamic_table_.size(); ++ndx) { + total_size += Size(fake_dynamic_table_[ndx]); + if (total_size > limit) { + // Need to get rid of ndx and all following entries. + fake_dynamic_table_.erase(fake_dynamic_table_.begin() + ndx, + fake_dynamic_table_.end()); + return original_size - FakeSize(); + } + } + return 0; + } + + // Verify that the contents of the actual dynamic table match those in + // fake_dynamic_table_. + AssertionResult VerifyDynamicTableContents() { + VERIFY_EQ(current_dynamic_size(), FakeSize()); + VERIFY_EQ(num_dynamic_entries(), fake_dynamic_table_.size()); + + for (size_t ndx = 0; ndx < fake_dynamic_table_.size(); ++ndx) { + const HpackStringPair* found = Lookup(ndx + kFirstDynamicTableIndex); + VERIFY_NE(found, nullptr); + + const auto& expected = fake_dynamic_table_[ndx]; + VERIFY_EQ(Name(expected), found->name); + VERIFY_EQ(Value(expected), found->value); + } + + // Make sure there are no more entries. + VERIFY_EQ(nullptr, + Lookup(fake_dynamic_table_.size() + kFirstDynamicTableIndex)); + return AssertionSuccess(); + } + + // Apply an update to the limit on the maximum size of the dynamic table. + AssertionResult DynamicTableSizeUpdate(size_t size_limit) { + VERIFY_EQ(current_dynamic_size(), FakeSize()); + if (size_limit < current_dynamic_size()) { + // Will need to trim the dynamic table's oldest entries. + tables_.DynamicTableSizeUpdate(size_limit); + FakeTrim(size_limit); + return VerifyDynamicTableContents(); + } + // Shouldn't change the size. + tables_.DynamicTableSizeUpdate(size_limit); + return VerifyDynamicTableContents(); + } + + // Insert an entry into the dynamic table, confirming that trimming of entries + // occurs if the total size is greater than the limit, and that older entries + // move up by 1 index. + AssertionResult Insert(const Http2String& name, const Http2String& value) { + size_t old_count = num_dynamic_entries(); + if (tables_.Insert(HpackString(name), HpackString(value))) { + VERIFY_GT(current_dynamic_size(), 0u); + VERIFY_GT(num_dynamic_entries(), 0u); + } else { + VERIFY_EQ(current_dynamic_size(), 0u); + VERIFY_EQ(num_dynamic_entries(), 0u); + } + FakeInsert(name, value); + VERIFY_EQ(old_count + 1, fake_dynamic_table_.size()); + FakeTrim(dynamic_size_limit()); + VERIFY_EQ(current_dynamic_size(), FakeSize()); + VERIFY_EQ(num_dynamic_entries(), fake_dynamic_table_.size()); + return VerifyDynamicTableContents(); + } + + private: + HpackDecoderTables tables_; + + std::vector<FakeHpackEntry> fake_dynamic_table_; +}; + +TEST_F(HpackDecoderTablesTest, StaticTableContents) { + EXPECT_TRUE(VerifyStaticTableContents()); +} + +// Generate a bunch of random header entries, insert them, and confirm they +// present, as required by the RFC, using VerifyDynamicTableContents above on +// each Insert. Also apply various resizings of the dynamic table. +TEST_F(HpackDecoderTablesTest, RandomDynamicTable) { + EXPECT_EQ(0u, current_dynamic_size()); + EXPECT_TRUE(VerifyStaticTableContents()); + EXPECT_TRUE(VerifyDynamicTableContents()); + + std::vector<size_t> table_sizes; + table_sizes.push_back(dynamic_size_limit()); + table_sizes.push_back(0); + table_sizes.push_back(dynamic_size_limit() / 2); + table_sizes.push_back(dynamic_size_limit()); + table_sizes.push_back(dynamic_size_limit() / 2); + table_sizes.push_back(0); + table_sizes.push_back(dynamic_size_limit()); + + for (size_t limit : table_sizes) { + ASSERT_TRUE(DynamicTableSizeUpdate(limit)); + for (int insert_count = 0; insert_count < 100; ++insert_count) { + Http2String name = + GenerateHttp2HeaderName(random_.UniformInRange(2, 40), RandomPtr()); + Http2String value = + GenerateWebSafeString(random_.UniformInRange(2, 600), RandomPtr()); + ASSERT_TRUE(Insert(name, value)); + } + EXPECT_TRUE(VerifyStaticTableContents()); + } +} + +} // namespace +} // namespace test +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_decoder_test.cc b/http2/hpack/decoder/hpack_decoder_test.cc new file mode 100644 index 0000000..563e977 --- /dev/null +++ b/http2/hpack/decoder/hpack_decoder_test.cc
@@ -0,0 +1,1219 @@ +// Copyright 2017 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_decoder.h" + +// Tests of HpackDecoder. + +#include <tuple> +#include <utility> +#include <vector> + +#include "base/logging.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "net/third_party/quiche/src/http2/decoder/decode_buffer.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_decoder_listener.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_decoder_state.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_decoder_tables.h" +#include "net/third_party/quiche/src/http2/hpack/hpack_string.h" +#include "net/third_party/quiche/src/http2/hpack/http2_hpack_constants.h" +#include "net/third_party/quiche/src/http2/hpack/tools/hpack_block_builder.h" +#include "net/third_party/quiche/src/http2/hpack/tools/hpack_example.h" +#include "net/third_party/quiche/src/http2/http2_constants.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_test_helpers.h" +#include "net/third_party/quiche/src/http2/test_tools/http2_random.h" +#include "net/third_party/quiche/src/http2/tools/random_util.h" + +using ::testing::AssertionFailure; +using ::testing::AssertionResult; +using ::testing::AssertionSuccess; +using ::testing::ElementsAreArray; +using ::testing::HasSubstr; + +namespace http2 { +namespace test { +class HpackDecoderStatePeer { + public: + static HpackDecoderTables* GetDecoderTables(HpackDecoderState* state) { + return &state->decoder_tables_; + } + static void set_listener(HpackDecoderState* state, + HpackDecoderListener* listener) { + state->listener_ = listener; + } +}; +class HpackDecoderPeer { + public: + static HpackDecoderState* GetDecoderState(HpackDecoder* decoder) { + return &decoder->decoder_state_; + } + static HpackDecoderTables* GetDecoderTables(HpackDecoder* decoder) { + return HpackDecoderStatePeer::GetDecoderTables(GetDecoderState(decoder)); + } +}; + +namespace { + +typedef std::tuple<HpackEntryType, Http2String, Http2String> HpackHeaderEntry; +typedef std::vector<HpackHeaderEntry> HpackHeaderEntries; + +// TODO(jamessynge): Create a ...test_utils.h file with the mock listener +// and with VerifyDynamicTableContents. +class MockHpackDecoderListener : public HpackDecoderListener { + public: + MOCK_METHOD0(OnHeaderListStart, void()); + MOCK_METHOD3(OnHeader, + void(HpackEntryType entry_type, + const HpackString& name, + const HpackString& value)); + MOCK_METHOD0(OnHeaderListEnd, void()); + MOCK_METHOD1(OnHeaderErrorDetected, void(Http2StringPiece error_message)); +}; + +class HpackDecoderTest : public ::testing::TestWithParam<bool>, + public HpackDecoderListener { + protected: + // Note that we initialize the random number generator with the same seed + // for each individual test, therefore the order in which the tests are + // executed does not effect the sequence produced by the RNG within any + // one test. + HpackDecoderTest() : decoder_(this, 4096) { + fragment_the_hpack_block_ = GetParam(); + } + ~HpackDecoderTest() override = default; + + void OnHeaderListStart() override { + ASSERT_FALSE(saw_start_); + ASSERT_FALSE(saw_end_); + saw_start_ = true; + header_entries_.clear(); + } + + // Called for each header name-value pair that is decoded, in the order they + // appear in the HPACK block. Multiple values for a given key will be emitted + // as multiple calls to OnHeader. + void OnHeader(HpackEntryType entry_type, + const HpackString& name, + const HpackString& value) override { + ASSERT_TRUE(saw_start_); + ASSERT_FALSE(saw_end_); + // header_entries_.push_back({entry_type, name.ToString(), + // value.ToString()}); + header_entries_.emplace_back(entry_type, name.ToString(), value.ToString()); + } + + // OnHeaderBlockEnd is called after successfully decoding an HPACK block. Will + // only be called once per block, even if it extends into CONTINUATION frames. + // A callback method which notifies when the parser finishes handling a + // header block (i.e. the containing frame has the END_STREAM flag set). + // Also indicates the total number of bytes in this block. + void OnHeaderListEnd() override { + ASSERT_TRUE(saw_start_); + ASSERT_FALSE(saw_end_); + ASSERT_TRUE(error_messages_.empty()); + saw_end_ = true; + } + + // OnHeaderErrorDetected is called if an error is detected while decoding. + // error_message may be used in a GOAWAY frame as the Opaque Data. + void OnHeaderErrorDetected(Http2StringPiece error_message) override { + ASSERT_TRUE(saw_start_); + error_messages_.push_back(Http2String(error_message)); + // No further callbacks should be made at this point, so replace 'this' as + // the listener with mock_listener_, which is a strict mock, so will + // generate an error for any calls. + HpackDecoderStatePeer::set_listener( + HpackDecoderPeer::GetDecoderState(&decoder_), &mock_listener_); + } + + AssertionResult DecodeBlock(Http2StringPiece block) { + VLOG(1) << "HpackDecoderTest::DecodeBlock"; + + VERIFY_FALSE(decoder_.error_detected()); + VERIFY_TRUE(error_messages_.empty()); + VERIFY_FALSE(saw_start_); + VERIFY_FALSE(saw_end_); + header_entries_.clear(); + + VERIFY_FALSE(decoder_.error_detected()); + VERIFY_TRUE(decoder_.StartDecodingBlock()); + VERIFY_FALSE(decoder_.error_detected()); + + if (fragment_the_hpack_block_) { + // See note in ctor regarding RNG. + while (!block.empty()) { + size_t fragment_size = random_.RandomSizeSkewedLow(block.size()); + DecodeBuffer db(block.substr(0, fragment_size)); + VERIFY_TRUE(decoder_.DecodeFragment(&db)); + VERIFY_EQ(0u, db.Remaining()); + block.remove_prefix(fragment_size); + } + } else { + DecodeBuffer db(block); + VERIFY_TRUE(decoder_.DecodeFragment(&db)); + VERIFY_EQ(0u, db.Remaining()); + } + VERIFY_FALSE(decoder_.error_detected()); + + VERIFY_TRUE(decoder_.EndDecodingBlock()); + if (saw_end_) { + VERIFY_FALSE(decoder_.error_detected()); + VERIFY_TRUE(error_messages_.empty()); + } else { + VERIFY_TRUE(decoder_.error_detected()); + VERIFY_FALSE(error_messages_.empty()); + } + + saw_start_ = saw_end_ = false; + return AssertionSuccess(); + } + + const HpackDecoderTables& GetDecoderTables() { + return *HpackDecoderPeer::GetDecoderTables(&decoder_); + } + const HpackStringPair* Lookup(size_t index) { + return GetDecoderTables().Lookup(index); + } + size_t current_header_table_size() { + return GetDecoderTables().current_header_table_size(); + } + size_t header_table_size_limit() { + return GetDecoderTables().header_table_size_limit(); + } + void set_header_table_size_limit(size_t size) { + HpackDecoderPeer::GetDecoderTables(&decoder_)->DynamicTableSizeUpdate(size); + } + + // dynamic_index is one-based, because that is the way RFC 7541 shows it. + AssertionResult VerifyEntry(size_t dynamic_index, + const char* name, + const char* value) { + const HpackStringPair* entry = + Lookup(dynamic_index + kFirstDynamicTableIndex - 1); + VERIFY_NE(entry, nullptr); + VERIFY_EQ(entry->name.ToStringPiece(), name); + VERIFY_EQ(entry->value.ToStringPiece(), value); + return AssertionSuccess(); + } + AssertionResult VerifyNoEntry(size_t dynamic_index) { + const HpackStringPair* entry = + Lookup(dynamic_index + kFirstDynamicTableIndex - 1); + VERIFY_EQ(entry, nullptr); + return AssertionSuccess(); + } + AssertionResult VerifyDynamicTableContents( + const std::vector<std::pair<const char*, const char*>>& entries) { + size_t index = 1; + for (const auto& entry : entries) { + VERIFY_SUCCESS(VerifyEntry(index, entry.first, entry.second)); + ++index; + } + VERIFY_SUCCESS(VerifyNoEntry(index)); + return AssertionSuccess(); + } + + Http2Random random_; + HpackDecoder decoder_; + testing::StrictMock<MockHpackDecoderListener> mock_listener_; + HpackHeaderEntries header_entries_; + std::vector<Http2String> error_messages_; + bool fragment_the_hpack_block_; + bool saw_start_ = false; + bool saw_end_ = false; +}; +INSTANTIATE_TEST_CASE_P(AllWays, HpackDecoderTest, ::testing::Bool()); + +// Test based on RFC 7541, section C.3: Request Examples without Huffman Coding. +// This section shows several consecutive header lists, corresponding to HTTP +// requests, on the same connection. +// http://httpwg.org/specs/rfc7541.html#rfc.section.C.3 +TEST_P(HpackDecoderTest, C3_RequestExamples) { + // C.3.1 First Request + Http2String hpack_block = HpackExampleToStringOrDie(R"( + 82 | == Indexed - Add == + | idx = 2 + | -> :method: GET + 86 | == Indexed - Add == + | idx = 6 + | -> :scheme: http + 84 | == Indexed - Add == + | idx = 4 + | -> :path: / + 41 | == Literal indexed == + | Indexed name (idx = 1) + | :authority + 0f | Literal value (len = 15) + 7777 772e 6578 616d 706c 652e 636f 6d | www.example.com + | -> :authority: + | www.example.com + )"); + EXPECT_TRUE(DecodeBlock(hpack_block)); + ASSERT_THAT( + header_entries_, + ElementsAreArray({ + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":method", "GET"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":scheme", "http"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":path", "/"}, + HpackHeaderEntry{HpackEntryType::kIndexedLiteralHeader, ":authority", + "www.example.com"}, + })); + + // Dynamic Table (after decoding): + // + // [ 1] (s = 57) :authority: www.example.com + // Table size: 57 + ASSERT_TRUE(VerifyDynamicTableContents({{":authority", "www.example.com"}})); + ASSERT_EQ(57u, current_header_table_size()); + + // C.3.2 Second Request + hpack_block = HpackExampleToStringOrDie(R"( + 82 | == Indexed - Add == + | idx = 2 + | -> :method: GET + 86 | == Indexed - Add == + | idx = 6 + | -> :scheme: http + 84 | == Indexed - Add == + | idx = 4 + | -> :path: / + be | == Indexed - Add == + | idx = 62 + | -> :authority: + | www.example.com + 58 | == Literal indexed == + | Indexed name (idx = 24) + | cache-control + 08 | Literal value (len = 8) + 6e6f 2d63 6163 6865 | no-cache + | -> cache-control: no-cache + )"); + EXPECT_TRUE(DecodeBlock(hpack_block)); + ASSERT_THAT( + header_entries_, + ElementsAreArray({ + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":method", "GET"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":scheme", "http"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":path", "/"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":authority", + "www.example.com"}, + HpackHeaderEntry{HpackEntryType::kIndexedLiteralHeader, + "cache-control", "no-cache"}, + })); + + // Dynamic Table (after decoding): + // + // [ 1] (s = 53) cache-control: no-cache + // [ 2] (s = 57) :authority: www.example.com + // Table size: 110 + ASSERT_TRUE(VerifyDynamicTableContents( + {{"cache-control", "no-cache"}, {":authority", "www.example.com"}})); + ASSERT_EQ(110u, current_header_table_size()); + + // C.3.2 Third Request + hpack_block = HpackExampleToStringOrDie(R"( + 82 | == Indexed - Add == + | idx = 2 + | -> :method: GET + 87 | == Indexed - Add == + | idx = 7 + | -> :scheme: https + 85 | == Indexed - Add == + | idx = 5 + | -> :path: /index.html + bf | == Indexed - Add == + | idx = 63 + | -> :authority: + | www.example.com + 40 | == Literal indexed == + 0a | Literal name (len = 10) + 6375 7374 6f6d 2d6b 6579 | custom-key + 0c | Literal value (len = 12) + 6375 7374 6f6d 2d76 616c 7565 | custom-value + | -> custom-key: + | custom-value + )"); + EXPECT_TRUE(DecodeBlock(hpack_block)); + ASSERT_THAT( + header_entries_, + ElementsAreArray({ + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":method", "GET"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":scheme", "https"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":path", + "/index.html"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":authority", + "www.example.com"}, + HpackHeaderEntry{HpackEntryType::kIndexedLiteralHeader, "custom-key", + "custom-value"}, + })); + + // Dynamic Table (after decoding): + // + // [ 1] (s = 54) custom-key: custom-value + // [ 2] (s = 53) cache-control: no-cache + // [ 3] (s = 57) :authority: www.example.com + // Table size: 164 + ASSERT_TRUE(VerifyDynamicTableContents({{"custom-key", "custom-value"}, + {"cache-control", "no-cache"}, + {":authority", "www.example.com"}})); + ASSERT_EQ(164u, current_header_table_size()); +} + +// Test based on RFC 7541, section C.4 Request Examples with Huffman Coding. +// This section shows the same examples as the previous section but uses +// Huffman encoding for the literal values. +// http://httpwg.org/specs/rfc7541.html#rfc.section.C.4 +TEST_P(HpackDecoderTest, C4_RequestExamplesWithHuffmanEncoding) { + // C.4.1 First Request + Http2String hpack_block = HpackExampleToStringOrDie(R"( + 82 | == Indexed - Add == + | idx = 2 + | -> :method: GET + 86 | == Indexed - Add == + | idx = 6 + | -> :scheme: http + 84 | == Indexed - Add == + | idx = 4 + | -> :path: / + 41 | == Literal indexed == + | Indexed name (idx = 1) + | :authority + 8c | Literal value (len = 12) + | Huffman encoded: + f1e3 c2e5 f23a 6ba0 ab90 f4ff | .....:k..... + | Decoded: + | www.example.com + | -> :authority: + | www.example.com + )"); + EXPECT_TRUE(DecodeBlock(hpack_block)); + ASSERT_THAT( + header_entries_, + ElementsAreArray({ + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":method", "GET"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":scheme", "http"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":path", "/"}, + HpackHeaderEntry{HpackEntryType::kIndexedLiteralHeader, ":authority", + "www.example.com"}, + })); + + // Dynamic Table (after decoding): + // + // [ 1] (s = 57) :authority: www.example.com + // Table size: 57 + ASSERT_TRUE(VerifyDynamicTableContents({{":authority", "www.example.com"}})); + ASSERT_EQ(57u, current_header_table_size()); + + // C.4.2 Second Request + hpack_block = HpackExampleToStringOrDie(R"( + 82 | == Indexed - Add == + | idx = 2 + | -> :method: GET + 86 | == Indexed - Add == + | idx = 6 + | -> :scheme: http + 84 | == Indexed - Add == + | idx = 4 + | -> :path: / + be | == Indexed - Add == + | idx = 62 + | -> :authority: + | www.example.com + 58 | == Literal indexed == + | Indexed name (idx = 24) + | cache-control + 86 | Literal value (len = 6) + | Huffman encoded: + a8eb 1064 9cbf | ...d.. + | Decoded: + | no-cache + | -> cache-control: no-cache + )"); + EXPECT_TRUE(DecodeBlock(hpack_block)); + ASSERT_THAT( + header_entries_, + ElementsAreArray({ + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":method", "GET"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":scheme", "http"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":path", "/"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":authority", + "www.example.com"}, + HpackHeaderEntry{HpackEntryType::kIndexedLiteralHeader, + "cache-control", "no-cache"}, + })); + + // Dynamic Table (after decoding): + // + // [ 1] (s = 53) cache-control: no-cache + // [ 2] (s = 57) :authority: www.example.com + // Table size: 110 + ASSERT_TRUE(VerifyDynamicTableContents( + {{"cache-control", "no-cache"}, {":authority", "www.example.com"}})); + ASSERT_EQ(110u, current_header_table_size()); + + // C.4.2 Third Request + hpack_block = HpackExampleToStringOrDie(R"( + 82 | == Indexed - Add == + | idx = 2 + | -> :method: GET + 87 | == Indexed - Add == + | idx = 7 + | -> :scheme: https + 85 | == Indexed - Add == + | idx = 5 + | -> :path: /index.html + bf | == Indexed - Add == + | idx = 63 + | -> :authority: + | www.example.com + 40 | == Literal indexed == + 88 | Literal name (len = 8) + | Huffman encoded: + 25a8 49e9 5ba9 7d7f | %.I.[.}. + | Decoded: + | custom-key + 89 | Literal value (len = 9) + | Huffman encoded: + 25a8 49e9 5bb8 e8b4 bf | %.I.[.... + | Decoded: + | custom-value + | -> custom-key: + | custom-value + )"); + EXPECT_TRUE(DecodeBlock(hpack_block)); + ASSERT_THAT( + header_entries_, + ElementsAreArray({ + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":method", "GET"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":scheme", "https"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":path", + "/index.html"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":authority", + "www.example.com"}, + HpackHeaderEntry{HpackEntryType::kIndexedLiteralHeader, "custom-key", + "custom-value"}, + })); + + // Dynamic Table (after decoding): + // + // [ 1] (s = 54) custom-key: custom-value + // [ 2] (s = 53) cache-control: no-cache + // [ 3] (s = 57) :authority: www.example.com + // Table size: 164 + ASSERT_TRUE(VerifyDynamicTableContents({{"custom-key", "custom-value"}, + {"cache-control", "no-cache"}, + {":authority", "www.example.com"}})); + ASSERT_EQ(164u, current_header_table_size()); +} + +// Test based on RFC 7541, section C.5: Response Examples without Huffman +// Coding. This section shows several consecutive header lists, corresponding +// to HTTP responses, on the same connection. The HTTP/2 setting parameter +// SETTINGS_HEADER_TABLE_SIZE is set to the value of 256 octets, causing +// some evictions to occur. +// http://httpwg.org/specs/rfc7541.html#rfc.section.C.5 +TEST_P(HpackDecoderTest, C5_ResponseExamples) { + set_header_table_size_limit(256); + + // C.5.1 First Response + // + // Header list to encode: + // + // :status: 302 + // cache-control: private + // date: Mon, 21 Oct 2013 20:13:21 GMT + // location: https://www.example.com + + Http2String hpack_block = HpackExampleToStringOrDie(R"( + 48 | == Literal indexed == + | Indexed name (idx = 8) + | :status + 03 | Literal value (len = 3) + 3330 32 | 302 + | -> :status: 302 + 58 | == Literal indexed == + | Indexed name (idx = 24) + | cache-control + 07 | Literal value (len = 7) + 7072 6976 6174 65 | private + | -> cache-control: private + 61 | == Literal indexed == + | Indexed name (idx = 33) + | date + 1d | Literal value (len = 29) + 4d6f 6e2c 2032 3120 4f63 7420 3230 3133 | Mon, 21 Oct 2013 + 2032 303a 3133 3a32 3120 474d 54 | 20:13:21 GMT + | -> date: Mon, 21 Oct 2013 + | 20:13:21 GMT + 6e | == Literal indexed == + | Indexed name (idx = 46) + | location + 17 | Literal value (len = 23) + 6874 7470 733a 2f2f 7777 772e 6578 616d | https://www.exam + 706c 652e 636f 6d | ple.com + | -> location: + | https://www.example.com + )"); + EXPECT_TRUE(DecodeBlock(hpack_block)); + ASSERT_THAT(header_entries_, + ElementsAreArray({ + HpackHeaderEntry{HpackEntryType::kIndexedLiteralHeader, + ":status", "302"}, + HpackHeaderEntry{HpackEntryType::kIndexedLiteralHeader, + "cache-control", "private"}, + HpackHeaderEntry{HpackEntryType::kIndexedLiteralHeader, + "date", "Mon, 21 Oct 2013 20:13:21 GMT"}, + HpackHeaderEntry{HpackEntryType::kIndexedLiteralHeader, + "location", "https://www.example.com"}, + })); + + // Dynamic Table (after decoding): + // + // [ 1] (s = 63) location: https://www.example.com + // [ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT + // [ 3] (s = 52) cache-control: private + // [ 4] (s = 42) :status: 302 + // Table size: 222 + ASSERT_TRUE( + VerifyDynamicTableContents({{"location", "https://www.example.com"}, + {"date", "Mon, 21 Oct 2013 20:13:21 GMT"}, + {"cache-control", "private"}, + {":status", "302"}})); + ASSERT_EQ(222u, current_header_table_size()); + + // C.5.2 Second Response + // + // The (":status", "302") header field is evicted from the dynamic table to + // free space to allow adding the (":status", "307") header field. + // + // Header list to encode: + // + // :status: 307 + // cache-control: private + // date: Mon, 21 Oct 2013 20:13:21 GMT + // location: https://www.example.com + + hpack_block = HpackExampleToStringOrDie(R"( + 48 | == Literal indexed == + | Indexed name (idx = 8) + | :status + 03 | Literal value (len = 3) + 3330 37 | 307 + | - evict: :status: 302 + | -> :status: 307 + c1 | == Indexed - Add == + | idx = 65 + | -> cache-control: private + c0 | == Indexed - Add == + | idx = 64 + | -> date: Mon, 21 Oct 2013 + | 20:13:21 GMT + bf | == Indexed - Add == + | idx = 63 + | -> location: + | https://www.example.com + )"); + EXPECT_TRUE(DecodeBlock(hpack_block)); + ASSERT_THAT(header_entries_, + ElementsAreArray({ + HpackHeaderEntry{HpackEntryType::kIndexedLiteralHeader, + ":status", "307"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, + "cache-control", "private"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, "date", + "Mon, 21 Oct 2013 20:13:21 GMT"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, "location", + "https://www.example.com"}, + })); + + // Dynamic Table (after decoding): + // + // [ 1] (s = 42) :status: 307 + // [ 2] (s = 63) location: https://www.example.com + // [ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT + // [ 4] (s = 52) cache-control: private + // Table size: 222 + + ASSERT_TRUE( + VerifyDynamicTableContents({{":status", "307"}, + {"location", "https://www.example.com"}, + {"date", "Mon, 21 Oct 2013 20:13:21 GMT"}, + {"cache-control", "private"}})); + ASSERT_EQ(222u, current_header_table_size()); + + // C.5.3 Third Response + // + // Several header fields are evicted from the dynamic table during the + // processing of this header list. + // + // Header list to encode: + // + // :status: 200 + // cache-control: private + // date: Mon, 21 Oct 2013 20:13:22 GMT + // location: https://www.example.com + // content-encoding: gzip + // set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1 + hpack_block = HpackExampleToStringOrDie(R"( + 88 | == Indexed - Add == + | idx = 8 + | -> :status: 200 + c1 | == Indexed - Add == + | idx = 65 + | -> cache-control: private + 61 | == Literal indexed == + | Indexed name (idx = 33) + | date + 1d | Literal value (len = 29) + 4d6f 6e2c 2032 3120 4f63 7420 3230 3133 | Mon, 21 Oct 2013 + 2032 303a 3133 3a32 3220 474d 54 | 20:13:22 GMT + | - evict: cache-control: + | private + | -> date: Mon, 21 Oct 2013 + | 20:13:22 GMT + c0 | == Indexed - Add == + | idx = 64 + | -> location: + | https://www.example.com + 5a | == Literal indexed == + | Indexed name (idx = 26) + | content-encoding + 04 | Literal value (len = 4) + 677a 6970 | gzip + | - evict: date: Mon, 21 Oct + | 2013 20:13:21 GMT + | -> content-encoding: gzip + 77 | == Literal indexed == + | Indexed name (idx = 55) + | set-cookie + 38 | Literal value (len = 56) + 666f 6f3d 4153 444a 4b48 514b 425a 584f | foo=ASDJKHQKBZXO + 5157 454f 5049 5541 5851 5745 4f49 553b | QWEOPIUAXQWEOIU; + 206d 6178 2d61 6765 3d33 3630 303b 2076 | max-age=3600; v + 6572 7369 6f6e 3d31 | ersion=1 + | - evict: location: + | https://www.example.com + | - evict: :status: 307 + | -> set-cookie: foo=ASDJKHQ + | KBZXOQWEOPIUAXQWEOIU; ma + | x-age=3600; version=1 + )"); + EXPECT_TRUE(DecodeBlock(hpack_block)); + ASSERT_THAT( + header_entries_, + ElementsAreArray({ + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":status", "200"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, "cache-control", + "private"}, + HpackHeaderEntry{HpackEntryType::kIndexedLiteralHeader, "date", + "Mon, 21 Oct 2013 20:13:22 GMT"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, "location", + "https://www.example.com"}, + HpackHeaderEntry{HpackEntryType::kIndexedLiteralHeader, + "content-encoding", "gzip"}, + HpackHeaderEntry{ + HpackEntryType::kIndexedLiteralHeader, "set-cookie", + "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"}, + })); + + // Dynamic Table (after decoding): + // + // [ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; + // max-age=3600; version=1 + // [ 2] (s = 52) content-encoding: gzip + // [ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT + // Table size: 215 + ASSERT_TRUE(VerifyDynamicTableContents( + {{"set-cookie", + "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"}, + {"content-encoding", "gzip"}, + {"date", "Mon, 21 Oct 2013 20:13:22 GMT"}})); + ASSERT_EQ(215u, current_header_table_size()); +} + +// Test based on RFC 7541, section C.6: Response Examples with Huffman Coding. +// This section shows the same examples as the previous section but uses Huffman +// encoding for the literal values. The HTTP/2 setting parameter +// SETTINGS_HEADER_TABLE_SIZE is set to the value of 256 octets, causing some +// evictions to occur. The eviction mechanism uses the length of the decoded +// literal values, so the same evictions occur as in the previous section. +// http://httpwg.org/specs/rfc7541.html#rfc.section.C.6 +TEST_P(HpackDecoderTest, C6_ResponseExamplesWithHuffmanEncoding) { + set_header_table_size_limit(256); + + // C.5.1 First Response + // + // Header list to encode: + // + // :status: 302 + // cache-control: private + // date: Mon, 21 Oct 2013 20:13:21 GMT + // location: https://www.example.com + Http2String hpack_block = HpackExampleToStringOrDie(R"( + 48 | == Literal indexed == + | Indexed name (idx = 8) + | :status + 03 | Literal value (len = 3) + 3330 32 | 302 + | -> :status: 302 + 58 | == Literal indexed == + | Indexed name (idx = 24) + | cache-control + 07 | Literal value (len = 7) + 7072 6976 6174 65 | private + | -> cache-control: private + 61 | == Literal indexed == + | Indexed name (idx = 33) + | date + 1d | Literal value (len = 29) + 4d6f 6e2c 2032 3120 4f63 7420 3230 3133 | Mon, 21 Oct 2013 + 2032 303a 3133 3a32 3120 474d 54 | 20:13:21 GMT + | -> date: Mon, 21 Oct 2013 + | 20:13:21 GMT + 6e | == Literal indexed == + | Indexed name (idx = 46) + | location + 17 | Literal value (len = 23) + 6874 7470 733a 2f2f 7777 772e 6578 616d | https://www.exam + 706c 652e 636f 6d | ple.com + | -> location: + | https://www.example.com + )"); + EXPECT_TRUE(DecodeBlock(hpack_block)); + ASSERT_THAT(header_entries_, + ElementsAreArray({ + HpackHeaderEntry{HpackEntryType::kIndexedLiteralHeader, + ":status", "302"}, + HpackHeaderEntry{HpackEntryType::kIndexedLiteralHeader, + "cache-control", "private"}, + HpackHeaderEntry{HpackEntryType::kIndexedLiteralHeader, + "date", "Mon, 21 Oct 2013 20:13:21 GMT"}, + HpackHeaderEntry{HpackEntryType::kIndexedLiteralHeader, + "location", "https://www.example.com"}, + })); + + // Dynamic Table (after decoding): + // + // [ 1] (s = 63) location: https://www.example.com + // [ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT + // [ 3] (s = 52) cache-control: private + // [ 4] (s = 42) :status: 302 + // Table size: 222 + ASSERT_TRUE( + VerifyDynamicTableContents({{"location", "https://www.example.com"}, + {"date", "Mon, 21 Oct 2013 20:13:21 GMT"}, + {"cache-control", "private"}, + {":status", "302"}})); + ASSERT_EQ(222u, current_header_table_size()); + + // C.5.2 Second Response + // + // The (":status", "302") header field is evicted from the dynamic table to + // free space to allow adding the (":status", "307") header field. + // + // Header list to encode: + // + // :status: 307 + // cache-control: private + // date: Mon, 21 Oct 2013 20:13:21 GMT + // location: https://www.example.com + hpack_block = HpackExampleToStringOrDie(R"( + 48 | == Literal indexed == + | Indexed name (idx = 8) + | :status + 03 | Literal value (len = 3) + 3330 37 | 307 + | - evict: :status: 302 + | -> :status: 307 + c1 | == Indexed - Add == + | idx = 65 + | -> cache-control: private + c0 | == Indexed - Add == + | idx = 64 + | -> date: Mon, 21 Oct 2013 + | 20:13:21 GMT + bf | == Indexed - Add == + | idx = 63 + | -> location: + | https://www.example.com + )"); + EXPECT_TRUE(DecodeBlock(hpack_block)); + ASSERT_THAT(header_entries_, + ElementsAreArray({ + HpackHeaderEntry{HpackEntryType::kIndexedLiteralHeader, + ":status", "307"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, + "cache-control", "private"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, "date", + "Mon, 21 Oct 2013 20:13:21 GMT"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, "location", + "https://www.example.com"}, + })); + + // Dynamic Table (after decoding): + // + // [ 1] (s = 42) :status: 307 + // [ 2] (s = 63) location: https://www.example.com + // [ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT + // [ 4] (s = 52) cache-control: private + // Table size: 222 + ASSERT_TRUE( + VerifyDynamicTableContents({{":status", "307"}, + {"location", "https://www.example.com"}, + {"date", "Mon, 21 Oct 2013 20:13:21 GMT"}, + {"cache-control", "private"}})); + ASSERT_EQ(222u, current_header_table_size()); + + // C.5.3 Third Response + // + // Several header fields are evicted from the dynamic table during the + // processing of this header list. + // + // Header list to encode: + // + // :status: 200 + // cache-control: private + // date: Mon, 21 Oct 2013 20:13:22 GMT + // location: https://www.example.com + // content-encoding: gzip + // set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1 + hpack_block = HpackExampleToStringOrDie(R"( + 88 | == Indexed - Add == + | idx = 8 + | -> :status: 200 + c1 | == Indexed - Add == + | idx = 65 + | -> cache-control: private + 61 | == Literal indexed == + | Indexed name (idx = 33) + | date + 1d | Literal value (len = 29) + 4d6f 6e2c 2032 3120 4f63 7420 3230 3133 | Mon, 21 Oct 2013 + 2032 303a 3133 3a32 3220 474d 54 | 20:13:22 GMT + | - evict: cache-control: + | private + | -> date: Mon, 21 Oct 2013 + | 20:13:22 GMT + c0 | == Indexed - Add == + | idx = 64 + | -> location: + | https://www.example.com + 5a | == Literal indexed == + | Indexed name (idx = 26) + | content-encoding + 04 | Literal value (len = 4) + 677a 6970 | gzip + | - evict: date: Mon, 21 Oct + | 2013 20:13:21 GMT + | -> content-encoding: gzip + 77 | == Literal indexed == + | Indexed name (idx = 55) + | set-cookie + 38 | Literal value (len = 56) + 666f 6f3d 4153 444a 4b48 514b 425a 584f | foo=ASDJKHQKBZXO + 5157 454f 5049 5541 5851 5745 4f49 553b | QWEOPIUAXQWEOIU; + 206d 6178 2d61 6765 3d33 3630 303b 2076 | max-age=3600; v + 6572 7369 6f6e 3d31 | ersion=1 + | - evict: location: + | https://www.example.com + | - evict: :status: 307 + | -> set-cookie: foo=ASDJKHQ + | KBZXOQWEOPIUAXQWEOIU; ma + | x-age=3600; version=1 + )"); + EXPECT_TRUE(DecodeBlock(hpack_block)); + ASSERT_THAT( + header_entries_, + ElementsAreArray({ + HpackHeaderEntry{HpackEntryType::kIndexedHeader, ":status", "200"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, "cache-control", + "private"}, + HpackHeaderEntry{HpackEntryType::kIndexedLiteralHeader, "date", + "Mon, 21 Oct 2013 20:13:22 GMT"}, + HpackHeaderEntry{HpackEntryType::kIndexedHeader, "location", + "https://www.example.com"}, + HpackHeaderEntry{HpackEntryType::kIndexedLiteralHeader, + "content-encoding", "gzip"}, + HpackHeaderEntry{ + HpackEntryType::kIndexedLiteralHeader, "set-cookie", + "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"}, + })); + + // Dynamic Table (after decoding): + // + // [ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; + // max-age=3600; version=1 + // [ 2] (s = 52) content-encoding: gzip + // [ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT + // Table size: 215 + ASSERT_TRUE(VerifyDynamicTableContents( + {{"set-cookie", + "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"}, + {"content-encoding", "gzip"}, + {"date", "Mon, 21 Oct 2013 20:13:22 GMT"}})); + ASSERT_EQ(215u, current_header_table_size()); +} + +// Confirm that the table size can be changed, but at most twice. +TEST_P(HpackDecoderTest, ProcessesOptionalTableSizeUpdates) { + EXPECT_EQ(Http2SettingsInfo::DefaultHeaderTableSize(), + header_table_size_limit()); + // One update allowed. + { + HpackBlockBuilder hbb; + hbb.AppendDynamicTableSizeUpdate(3000); + EXPECT_TRUE(DecodeBlock(hbb.buffer())); + EXPECT_EQ(3000u, header_table_size_limit()); + EXPECT_EQ(0u, current_header_table_size()); + EXPECT_TRUE(header_entries_.empty()); + } + // Two updates allowed. + { + HpackBlockBuilder hbb; + hbb.AppendDynamicTableSizeUpdate(2000); + hbb.AppendDynamicTableSizeUpdate(2500); + EXPECT_TRUE(DecodeBlock(hbb.buffer())); + EXPECT_EQ(2500u, header_table_size_limit()); + EXPECT_EQ(0u, current_header_table_size()); + EXPECT_TRUE(header_entries_.empty()); + } + // A third update in the same HPACK block is rejected, so the final + // size is 1000, not 500. + { + HpackBlockBuilder hbb; + hbb.AppendDynamicTableSizeUpdate(1500); + hbb.AppendDynamicTableSizeUpdate(1000); + hbb.AppendDynamicTableSizeUpdate(500); + EXPECT_FALSE(DecodeBlock(hbb.buffer())); + EXPECT_EQ(1u, error_messages_.size()); + EXPECT_THAT(error_messages_[0], HasSubstr("size update not allowed")); + EXPECT_EQ(1000u, header_table_size_limit()); + EXPECT_EQ(0u, current_header_table_size()); + EXPECT_TRUE(header_entries_.empty()); + } + // An error has been detected, so calls to HpackDecoder::DecodeFragment + // should return immediately. + DecodeBuffer db("\x80"); + EXPECT_FALSE(decoder_.DecodeFragment(&db)); + EXPECT_EQ(0u, db.Offset()); + EXPECT_EQ(1u, error_messages_.size()); +} + +// Confirm that the table size can be changed when required, but at most twice. +TEST_P(HpackDecoderTest, ProcessesRequiredTableSizeUpdate) { + // One update required, two allowed, one provided, followed by a header. + decoder_.ApplyHeaderTableSizeSetting(1024); + decoder_.ApplyHeaderTableSizeSetting(2048); + EXPECT_EQ(Http2SettingsInfo::DefaultHeaderTableSize(), + header_table_size_limit()); + { + HpackBlockBuilder hbb; + hbb.AppendDynamicTableSizeUpdate(1024); + hbb.AppendIndexedHeader(4); // :path: / + EXPECT_TRUE(DecodeBlock(hbb.buffer())); + EXPECT_THAT(header_entries_, + ElementsAreArray({HpackHeaderEntry{ + HpackEntryType::kIndexedHeader, ":path", "/"}})); + EXPECT_EQ(1024u, header_table_size_limit()); + EXPECT_EQ(0u, current_header_table_size()); + } + // One update required, two allowed, two provided, followed by a header. + decoder_.ApplyHeaderTableSizeSetting(1000); + decoder_.ApplyHeaderTableSizeSetting(1500); + { + HpackBlockBuilder hbb; + hbb.AppendDynamicTableSizeUpdate(500); + hbb.AppendDynamicTableSizeUpdate(1250); + hbb.AppendIndexedHeader(5); // :path: /index.html + EXPECT_TRUE(DecodeBlock(hbb.buffer())); + EXPECT_THAT(header_entries_, + ElementsAreArray({HpackHeaderEntry{ + HpackEntryType::kIndexedHeader, ":path", "/index.html"}})); + EXPECT_EQ(1250u, header_table_size_limit()); + EXPECT_EQ(0u, current_header_table_size()); + } + // One update required, two allowed, three provided, followed by a header. + // The third update is rejected, so the final size is 1000, not 500. + decoder_.ApplyHeaderTableSizeSetting(500); + decoder_.ApplyHeaderTableSizeSetting(1000); + { + HpackBlockBuilder hbb; + hbb.AppendDynamicTableSizeUpdate(200); + hbb.AppendDynamicTableSizeUpdate(700); + hbb.AppendDynamicTableSizeUpdate(900); + hbb.AppendIndexedHeader(5); // Not decoded. + EXPECT_FALSE(DecodeBlock(hbb.buffer())); + EXPECT_FALSE(saw_end_); + EXPECT_EQ(1u, error_messages_.size()); + EXPECT_THAT(error_messages_[0], HasSubstr("size update not allowed")); + EXPECT_EQ(700u, header_table_size_limit()); + EXPECT_EQ(0u, current_header_table_size()); + EXPECT_TRUE(header_entries_.empty()); + } + // Now that an error has been detected, StartDecodingBlock should return + // false. + EXPECT_FALSE(decoder_.StartDecodingBlock()); +} + +// Confirm that required size updates are validated. +TEST_P(HpackDecoderTest, InvalidRequiredSizeUpdate) { + // Require a size update, but provide one that isn't small enough (must be + // zero or one, in this case). + decoder_.ApplyHeaderTableSizeSetting(1); + decoder_.ApplyHeaderTableSizeSetting(1024); + HpackBlockBuilder hbb; + hbb.AppendDynamicTableSizeUpdate(2); + EXPECT_TRUE(decoder_.StartDecodingBlock()); + DecodeBuffer db(hbb.buffer()); + EXPECT_FALSE(decoder_.DecodeFragment(&db)); + EXPECT_FALSE(saw_end_); + EXPECT_EQ(1u, error_messages_.size()); + EXPECT_THAT(error_messages_[0], HasSubstr("above low water mark")); + EXPECT_EQ(Http2SettingsInfo::DefaultHeaderTableSize(), + header_table_size_limit()); +} + +// Confirm that required size updates are indeed required before the end. +TEST_P(HpackDecoderTest, RequiredTableSizeChangeBeforeEnd) { + decoder_.ApplyHeaderTableSizeSetting(1024); + EXPECT_FALSE(DecodeBlock("")); + EXPECT_EQ(1u, error_messages_.size()); + EXPECT_THAT(error_messages_[0], + HasSubstr("Missing dynamic table size update")); + EXPECT_FALSE(saw_end_); +} + +// Confirm that required size updates are indeed required before an +// indexed header. +TEST_P(HpackDecoderTest, RequiredTableSizeChangeBeforeIndexedHeader) { + decoder_.ApplyHeaderTableSizeSetting(1024); + HpackBlockBuilder hbb; + hbb.AppendIndexedHeader(1); + EXPECT_FALSE(DecodeBlock(hbb.buffer())); + EXPECT_EQ(1u, error_messages_.size()); + EXPECT_THAT(error_messages_[0], + HasSubstr("Missing dynamic table size update")); + EXPECT_FALSE(saw_end_); + EXPECT_TRUE(header_entries_.empty()); +} + +// Confirm that required size updates are indeed required before an indexed +// header name. +// TODO(jamessynge): Move some of these to hpack_decoder_state_test.cc. +TEST_P(HpackDecoderTest, RequiredTableSizeChangeBeforeIndexedHeaderName) { + decoder_.ApplyHeaderTableSizeSetting(1024); + HpackBlockBuilder hbb; + hbb.AppendNameIndexAndLiteralValue(HpackEntryType::kIndexedLiteralHeader, 2, + false, "PUT"); + EXPECT_FALSE(DecodeBlock(hbb.buffer())); + EXPECT_EQ(1u, error_messages_.size()); + EXPECT_THAT(error_messages_[0], + HasSubstr("Missing dynamic table size update")); + EXPECT_FALSE(saw_end_); + EXPECT_TRUE(header_entries_.empty()); +} + +// Confirm that required size updates are indeed required before a literal +// header name. +TEST_P(HpackDecoderTest, RequiredTableSizeChangeBeforeLiteralName) { + decoder_.ApplyHeaderTableSizeSetting(1024); + HpackBlockBuilder hbb; + hbb.AppendLiteralNameAndValue(HpackEntryType::kNeverIndexedLiteralHeader, + false, "name", false, "some data."); + EXPECT_FALSE(DecodeBlock(hbb.buffer())); + EXPECT_EQ(1u, error_messages_.size()); + EXPECT_THAT(error_messages_[0], + HasSubstr("Missing dynamic table size update")); + EXPECT_FALSE(saw_end_); + EXPECT_TRUE(header_entries_.empty()); +} + +// Confirm that an excessively long varint is detected, in this case an +// index of 127, but with lots of additional high-order 0 bits provided, +// too many to be allowed. +TEST_P(HpackDecoderTest, InvalidIndexedHeaderVarint) { + EXPECT_TRUE(decoder_.StartDecodingBlock()); + DecodeBuffer db("\xff\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x00"); + EXPECT_FALSE(decoder_.DecodeFragment(&db)); + EXPECT_TRUE(decoder_.error_detected()); + EXPECT_FALSE(saw_end_); + EXPECT_EQ(1u, error_messages_.size()); + EXPECT_THAT(error_messages_[0], HasSubstr("malformed")); + EXPECT_TRUE(header_entries_.empty()); + // Now that an error has been detected, EndDecodingBlock should not succeed. + EXPECT_FALSE(decoder_.EndDecodingBlock()); +} + +// Confirm that an invalid index into the tables is detected, in this case an +// index of 0. +TEST_P(HpackDecoderTest, InvalidIndex) { + EXPECT_TRUE(decoder_.StartDecodingBlock()); + DecodeBuffer db("\x80"); + EXPECT_FALSE(decoder_.DecodeFragment(&db)); + EXPECT_TRUE(decoder_.error_detected()); + EXPECT_FALSE(saw_end_); + EXPECT_EQ(1u, error_messages_.size()); + EXPECT_THAT(error_messages_[0], HasSubstr("Invalid index")); + EXPECT_TRUE(header_entries_.empty()); + // Now that an error has been detected, EndDecodingBlock should not succeed. + EXPECT_FALSE(decoder_.EndDecodingBlock()); +} + +// Confirm that EndDecodingBlock detects a truncated HPACK block. +TEST_P(HpackDecoderTest, TruncatedBlock) { + HpackBlockBuilder hbb; + hbb.AppendDynamicTableSizeUpdate(3000); + EXPECT_EQ(3u, hbb.size()); + hbb.AppendDynamicTableSizeUpdate(4000); + EXPECT_EQ(6u, hbb.size()); + // Decodes this block if the whole thing is provided. + EXPECT_TRUE(DecodeBlock(hbb.buffer())); + EXPECT_EQ(4000u, header_table_size_limit()); + // Multiple times even. + EXPECT_TRUE(DecodeBlock(hbb.buffer())); + EXPECT_EQ(4000u, header_table_size_limit()); + // But not if the block is truncated. + EXPECT_FALSE(DecodeBlock(hbb.buffer().substr(0, hbb.size() - 1))); + EXPECT_FALSE(saw_end_); + EXPECT_EQ(1u, error_messages_.size()); + EXPECT_THAT(error_messages_[0], HasSubstr("truncated")); + // The first update was decoded. + EXPECT_EQ(3000u, header_table_size_limit()); + EXPECT_EQ(0u, current_header_table_size()); + EXPECT_TRUE(header_entries_.empty()); +} + +// Confirm that an oversized string is detected, ending decoding. +TEST_P(HpackDecoderTest, OversizeStringDetected) { + HpackBlockBuilder hbb; + hbb.AppendLiteralNameAndValue(HpackEntryType::kNeverIndexedLiteralHeader, + false, "name", false, "some data."); + hbb.AppendLiteralNameAndValue(HpackEntryType::kUnindexedLiteralHeader, false, + "name2", false, "longer data"); + + // Normally able to decode this block. + EXPECT_TRUE(DecodeBlock(hbb.buffer())); + EXPECT_THAT(header_entries_, + ElementsAreArray( + {HpackHeaderEntry{HpackEntryType::kNeverIndexedLiteralHeader, + "name", "some data."}, + HpackHeaderEntry{HpackEntryType::kUnindexedLiteralHeader, + "name2", "longer data"}})); + + // But not if the maximum size of strings is less than the longest string. + decoder_.set_max_string_size_bytes(10); + EXPECT_FALSE(DecodeBlock(hbb.buffer())); + EXPECT_THAT( + header_entries_, + ElementsAreArray({HpackHeaderEntry{ + HpackEntryType::kNeverIndexedLiteralHeader, "name", "some data."}})); + EXPECT_FALSE(saw_end_); + EXPECT_EQ(1u, error_messages_.size()); + EXPECT_THAT(error_messages_[0], HasSubstr("too long")); +} + +} // namespace +} // namespace test +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_entry_collector.cc b/http2/hpack/decoder/hpack_entry_collector.cc new file mode 100644 index 0000000..9d17465 --- /dev/null +++ b/http2/hpack/decoder/hpack_entry_collector.cc
@@ -0,0 +1,301 @@ +// Copyright 2016 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_entry_collector.h" + +#include "base/logging.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_string_collector.h" +#include "net/third_party/quiche/src/http2/hpack/http2_hpack_constants.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string_utils.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_test_helpers.h" + +using ::testing::AssertionResult; + +namespace http2 { +namespace test { +namespace { + +const HpackEntryType kInvalidHeaderType = static_cast<HpackEntryType>(99); +const size_t kInvalidIndex = 99999999; + +} // namespace + +HpackEntryCollector::HpackEntryCollector() { + Clear(); +} + +HpackEntryCollector::HpackEntryCollector(const HpackEntryCollector& other) = + default; + +HpackEntryCollector::HpackEntryCollector(HpackEntryType type, + size_t index_or_size) + : header_type_(type), index_(index_or_size), started_(true), ended_(true) {} +HpackEntryCollector::HpackEntryCollector(HpackEntryType type, + size_t index, + bool value_huffman, + const Http2String& value) + : header_type_(type), + index_(index), + value_(value, value_huffman), + started_(true), + ended_(true) {} +HpackEntryCollector::HpackEntryCollector(HpackEntryType type, + bool name_huffman, + const Http2String& name, + bool value_huffman, + const Http2String& value) + : header_type_(type), + index_(0), + name_(name, name_huffman), + value_(value, value_huffman), + started_(true), + ended_(true) {} + +HpackEntryCollector::~HpackEntryCollector() = default; + +void HpackEntryCollector::OnIndexedHeader(size_t index) { + ASSERT_FALSE(started_); + ASSERT_TRUE(IsClear()) << ToString(); + Init(HpackEntryType::kIndexedHeader, index); + ended_ = true; +} +void HpackEntryCollector::OnStartLiteralHeader(HpackEntryType header_type, + size_t maybe_name_index) { + ASSERT_FALSE(started_); + ASSERT_TRUE(IsClear()) << ToString(); + Init(header_type, maybe_name_index); +} +void HpackEntryCollector::OnNameStart(bool huffman_encoded, size_t len) { + ASSERT_TRUE(started_); + ASSERT_FALSE(ended_); + ASSERT_FALSE(IsClear()); + ASSERT_TRUE(LiteralNameExpected()) << ToString(); + name_.OnStringStart(huffman_encoded, len); +} +void HpackEntryCollector::OnNameData(const char* data, size_t len) { + ASSERT_TRUE(started_); + ASSERT_FALSE(ended_); + ASSERT_TRUE(LiteralNameExpected()) << ToString(); + ASSERT_TRUE(name_.IsInProgress()); + name_.OnStringData(data, len); +} +void HpackEntryCollector::OnNameEnd() { + ASSERT_TRUE(started_); + ASSERT_FALSE(ended_); + ASSERT_TRUE(LiteralNameExpected()) << ToString(); + ASSERT_TRUE(name_.IsInProgress()); + name_.OnStringEnd(); +} +void HpackEntryCollector::OnValueStart(bool huffman_encoded, size_t len) { + ASSERT_TRUE(started_); + ASSERT_FALSE(ended_); + if (LiteralNameExpected()) { + ASSERT_TRUE(name_.HasEnded()); + } + ASSERT_TRUE(LiteralValueExpected()) << ToString(); + ASSERT_TRUE(value_.IsClear()) << value_.ToString(); + value_.OnStringStart(huffman_encoded, len); +} +void HpackEntryCollector::OnValueData(const char* data, size_t len) { + ASSERT_TRUE(started_); + ASSERT_FALSE(ended_); + ASSERT_TRUE(LiteralValueExpected()) << ToString(); + ASSERT_TRUE(value_.IsInProgress()); + value_.OnStringData(data, len); +} +void HpackEntryCollector::OnValueEnd() { + ASSERT_TRUE(started_); + ASSERT_FALSE(ended_); + ASSERT_TRUE(LiteralValueExpected()) << ToString(); + ASSERT_TRUE(value_.IsInProgress()); + value_.OnStringEnd(); + ended_ = true; +} +void HpackEntryCollector::OnDynamicTableSizeUpdate(size_t size) { + ASSERT_FALSE(started_); + ASSERT_TRUE(IsClear()) << ToString(); + Init(HpackEntryType::kDynamicTableSizeUpdate, size); + ended_ = true; +} + +void HpackEntryCollector::Clear() { + header_type_ = kInvalidHeaderType; + index_ = kInvalidIndex; + name_.Clear(); + value_.Clear(); + started_ = ended_ = false; +} +bool HpackEntryCollector::IsClear() const { + return header_type_ == kInvalidHeaderType && index_ == kInvalidIndex && + name_.IsClear() && value_.IsClear() && !started_ && !ended_; +} +bool HpackEntryCollector::IsComplete() const { + return started_ && ended_; +} +bool HpackEntryCollector::LiteralNameExpected() const { + switch (header_type_) { + case HpackEntryType::kIndexedLiteralHeader: + case HpackEntryType::kUnindexedLiteralHeader: + case HpackEntryType::kNeverIndexedLiteralHeader: + return index_ == 0; + default: + return false; + } +} +bool HpackEntryCollector::LiteralValueExpected() const { + switch (header_type_) { + case HpackEntryType::kIndexedLiteralHeader: + case HpackEntryType::kUnindexedLiteralHeader: + case HpackEntryType::kNeverIndexedLiteralHeader: + return true; + default: + return false; + } +} +AssertionResult HpackEntryCollector::ValidateIndexedHeader( + size_t expected_index) const { + VERIFY_TRUE(started_); + VERIFY_TRUE(ended_); + VERIFY_EQ(HpackEntryType::kIndexedHeader, header_type_); + VERIFY_EQ(expected_index, index_); + return ::testing::AssertionSuccess(); +} +AssertionResult HpackEntryCollector::ValidateLiteralValueHeader( + HpackEntryType expected_type, + size_t expected_index, + bool expected_value_huffman, + Http2StringPiece expected_value) const { + VERIFY_TRUE(started_); + VERIFY_TRUE(ended_); + VERIFY_EQ(expected_type, header_type_); + VERIFY_NE(0u, expected_index); + VERIFY_EQ(expected_index, index_); + VERIFY_TRUE(name_.IsClear()); + VERIFY_SUCCESS(value_.Collected(expected_value, expected_value_huffman)); + return ::testing::AssertionSuccess(); +} +AssertionResult HpackEntryCollector::ValidateLiteralNameValueHeader( + HpackEntryType expected_type, + bool expected_name_huffman, + Http2StringPiece expected_name, + bool expected_value_huffman, + Http2StringPiece expected_value) const { + VERIFY_TRUE(started_); + VERIFY_TRUE(ended_); + VERIFY_EQ(expected_type, header_type_); + VERIFY_EQ(0u, index_); + VERIFY_SUCCESS(name_.Collected(expected_name, expected_name_huffman)); + VERIFY_SUCCESS(value_.Collected(expected_value, expected_value_huffman)); + return ::testing::AssertionSuccess(); +} +AssertionResult HpackEntryCollector::ValidateDynamicTableSizeUpdate( + size_t size) const { + VERIFY_TRUE(started_); + VERIFY_TRUE(ended_); + VERIFY_EQ(HpackEntryType::kDynamicTableSizeUpdate, header_type_); + VERIFY_EQ(index_, size); + return ::testing::AssertionSuccess(); +} + +void HpackEntryCollector::AppendToHpackBlockBuilder( + HpackBlockBuilder* hbb) const { + ASSERT_TRUE(started_ && ended_) << *this; + switch (header_type_) { + case HpackEntryType::kIndexedHeader: + hbb->AppendIndexedHeader(index_); + return; + + case HpackEntryType::kDynamicTableSizeUpdate: + hbb->AppendDynamicTableSizeUpdate(index_); + return; + + case HpackEntryType::kIndexedLiteralHeader: + case HpackEntryType::kUnindexedLiteralHeader: + case HpackEntryType::kNeverIndexedLiteralHeader: + ASSERT_TRUE(value_.HasEnded()) << *this; + if (index_ != 0) { + CHECK(name_.IsClear()); + hbb->AppendNameIndexAndLiteralValue(header_type_, index_, + value_.huffman_encoded, value_.s); + } else { + CHECK(name_.HasEnded()) << *this; + hbb->AppendLiteralNameAndValue(header_type_, name_.huffman_encoded, + name_.s, value_.huffman_encoded, + value_.s); + } + return; + + default: + ADD_FAILURE() << *this; + } +} + +Http2String HpackEntryCollector::ToString() const { + Http2String result("Type="); + switch (header_type_) { + case HpackEntryType::kIndexedHeader: + result += "IndexedHeader"; + break; + case HpackEntryType::kDynamicTableSizeUpdate: + result += "DynamicTableSizeUpdate"; + break; + case HpackEntryType::kIndexedLiteralHeader: + result += "IndexedLiteralHeader"; + break; + case HpackEntryType::kUnindexedLiteralHeader: + result += "UnindexedLiteralHeader"; + break; + case HpackEntryType::kNeverIndexedLiteralHeader: + result += "NeverIndexedLiteralHeader"; + break; + default: + if (header_type_ == kInvalidHeaderType) { + result += "<unset>"; + } else { + Http2StrAppend(&result, header_type_); + } + } + if (index_ != 0) { + Http2StrAppend(&result, " Index=", index_); + } + if (!name_.IsClear()) { + Http2StrAppend(&result, " Name", name_.ToString()); + } + if (!value_.IsClear()) { + Http2StrAppend(&result, " Value", value_.ToString()); + } + if (!started_) { + EXPECT_FALSE(ended_); + Http2StrAppend(&result, " !started"); + } else if (!ended_) { + Http2StrAppend(&result, " !ended"); + } else { + Http2StrAppend(&result, " Complete"); + } + return result; +} + +void HpackEntryCollector::Init(HpackEntryType type, size_t maybe_index) { + ASSERT_TRUE(IsClear()) << ToString(); + header_type_ = type; + index_ = maybe_index; + started_ = true; +} + +bool operator==(const HpackEntryCollector& a, const HpackEntryCollector& b) { + return a.name() == b.name() && a.value() == b.value() && + a.index() == b.index() && a.header_type() == b.header_type() && + a.started() == b.started() && a.ended() == b.ended(); +} +bool operator!=(const HpackEntryCollector& a, const HpackEntryCollector& b) { + return !(a == b); +} + +std::ostream& operator<<(std::ostream& out, const HpackEntryCollector& v) { + return out << v.ToString(); +} + +} // namespace test +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_entry_collector.h b/http2/hpack/decoder/hpack_entry_collector.h new file mode 100644 index 0000000..c2c5fe5 --- /dev/null +++ b/http2/hpack/decoder/hpack_entry_collector.h
@@ -0,0 +1,155 @@ +// Copyright 2016 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. + +#ifndef QUICHE_HTTP2_HPACK_DECODER_HPACK_ENTRY_COLLECTOR_H_ +#define QUICHE_HTTP2_HPACK_DECODER_HPACK_ENTRY_COLLECTOR_H_ + +// HpackEntryCollector records calls to HpackEntryDecoderListener in support +// of tests of HpackEntryDecoder, or which use it. Can only record the callbacks +// for the decoding of a single entry; call Clear() between decoding successive +// entries or use a distinct HpackEntryCollector for each entry. + +#include <stddef.h> + +#include <iosfwd> + +#include "testing/gtest/include/gtest/gtest.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_entry_decoder_listener.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_string_collector.h" +#include "net/third_party/quiche/src/http2/hpack/http2_hpack_constants.h" +#include "net/third_party/quiche/src/http2/hpack/tools/hpack_block_builder.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string_piece.h" + +namespace http2 { +namespace test { + +class HpackEntryCollector : public HpackEntryDecoderListener { + public: + HpackEntryCollector(); + HpackEntryCollector(const HpackEntryCollector& other); + + // These next three constructors are intended for use in tests that create + // an HpackEntryCollector "manually", and then compare it against another + // that is populated via calls to the HpackEntryDecoderListener methods. + HpackEntryCollector(HpackEntryType type, size_t index_or_size); + HpackEntryCollector(HpackEntryType type, + size_t index, + bool value_huffman, + const Http2String& value); + HpackEntryCollector(HpackEntryType type, + bool name_huffman, + const Http2String& name, + bool value_huffman, + const Http2String& value); + + ~HpackEntryCollector() override; + + // Methods defined by HpackEntryDecoderListener. + void OnIndexedHeader(size_t index) override; + void OnStartLiteralHeader(HpackEntryType header_type, + size_t maybe_name_index) override; + void OnNameStart(bool huffman_encoded, size_t len) override; + void OnNameData(const char* data, size_t len) override; + void OnNameEnd() override; + void OnValueStart(bool huffman_encoded, size_t len) override; + void OnValueData(const char* data, size_t len) override; + void OnValueEnd() override; + void OnDynamicTableSizeUpdate(size_t size) override; + + // Clears the fields of the collector so that it is ready to start collecting + // another HPACK block entry. + void Clear(); + + // Is the collector ready to start collecting another HPACK block entry. + bool IsClear() const; + + // Has a complete entry been collected? + bool IsComplete() const; + + // Based on the HpackEntryType, is a literal name expected? + bool LiteralNameExpected() const; + + // Based on the HpackEntryType, is a literal value expected? + bool LiteralValueExpected() const; + + // Returns success if collected an Indexed Header (i.e. OnIndexedHeader was + // called). + ::testing::AssertionResult ValidateIndexedHeader(size_t expected_index) const; + + // Returns success if collected a Header with an indexed name and literal + // value (i.e. OnStartLiteralHeader was called with a non-zero index for + // the name, which must match expected_index). + ::testing::AssertionResult ValidateLiteralValueHeader( + HpackEntryType expected_type, + size_t expected_index, + bool expected_value_huffman, + Http2StringPiece expected_value) const; + + // Returns success if collected a Header with an literal name and literal + // value. + ::testing::AssertionResult ValidateLiteralNameValueHeader( + HpackEntryType expected_type, + bool expected_name_huffman, + Http2StringPiece expected_name, + bool expected_value_huffman, + Http2StringPiece expected_value) const; + + // Returns success if collected a Dynamic Table Size Update, + // with the specified size. + ::testing::AssertionResult ValidateDynamicTableSizeUpdate( + size_t expected_size) const; + + void set_header_type(HpackEntryType v) { header_type_ = v; } + HpackEntryType header_type() const { return header_type_; } + + void set_index(size_t v) { index_ = v; } + size_t index() const { return index_; } + + void set_name(const HpackStringCollector& v) { name_ = v; } + const HpackStringCollector& name() const { return name_; } + + void set_value(const HpackStringCollector& v) { value_ = v; } + const HpackStringCollector& value() const { return value_; } + + void set_started(bool v) { started_ = v; } + bool started() const { return started_; } + + void set_ended(bool v) { ended_ = v; } + bool ended() const { return ended_; } + + void AppendToHpackBlockBuilder(HpackBlockBuilder* hbb) const; + + // Returns a debug string. + Http2String ToString() const; + + private: + void Init(HpackEntryType type, size_t maybe_index); + + HpackEntryType header_type_; + size_t index_; + + HpackStringCollector name_; + HpackStringCollector value_; + + // True if has received a call to an HpackEntryDecoderListener method + // indicating the start of decoding an HPACK entry; for example, + // OnIndexedHeader set it true, but OnNameStart does not change it. + bool started_ = false; + + // True if has received a call to an HpackEntryDecoderListener method + // indicating the end of decoding an HPACK entry; for example, + // OnIndexedHeader and OnValueEnd both set it true, but OnNameEnd does + // not change it. + bool ended_ = false; +}; + +bool operator==(const HpackEntryCollector& a, const HpackEntryCollector& b); +bool operator!=(const HpackEntryCollector& a, const HpackEntryCollector& b); +std::ostream& operator<<(std::ostream& out, const HpackEntryCollector& v); + +} // namespace test +} // namespace http2 + +#endif // QUICHE_HTTP2_HPACK_DECODER_HPACK_ENTRY_COLLECTOR_H_
diff --git a/http2/hpack/decoder/hpack_entry_decoder.cc b/http2/hpack/decoder/hpack_entry_decoder.cc new file mode 100644 index 0000000..89562a9 --- /dev/null +++ b/http2/hpack/decoder/hpack_entry_decoder.cc
@@ -0,0 +1,267 @@ +// Copyright 2016 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_entry_decoder.h" + +#include <stddef.h> + +#include <cstdint> + +#include "base/logging.h" +#include "base/macros.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_bug_tracker.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_macros.h" + +namespace http2 { +namespace { +// Converts calls from HpackStringDecoder when decoding a header name into the +// appropriate HpackEntryDecoderListener::OnName* calls. +class NameDecoderListener { + public: + explicit NameDecoderListener(HpackEntryDecoderListener* listener) + : listener_(listener) {} + bool OnStringStart(bool huffman_encoded, size_t len) { + listener_->OnNameStart(huffman_encoded, len); + return true; + } + void OnStringData(const char* data, size_t len) { + listener_->OnNameData(data, len); + } + void OnStringEnd() { listener_->OnNameEnd(); } + + private: + HpackEntryDecoderListener* listener_; +}; + +// Converts calls from HpackStringDecoder when decoding a header value into +// the appropriate HpackEntryDecoderListener::OnValue* calls. +class ValueDecoderListener { + public: + explicit ValueDecoderListener(HpackEntryDecoderListener* listener) + : listener_(listener) {} + bool OnStringStart(bool huffman_encoded, size_t len) { + listener_->OnValueStart(huffman_encoded, len); + return true; + } + void OnStringData(const char* data, size_t len) { + listener_->OnValueData(data, len); + } + void OnStringEnd() { listener_->OnValueEnd(); } + + private: + HpackEntryDecoderListener* listener_; +}; +} // namespace + +DecodeStatus HpackEntryDecoder::Start(DecodeBuffer* db, + HpackEntryDecoderListener* listener) { + DCHECK(db != nullptr); + DCHECK(listener != nullptr); + DCHECK(db->HasData()); + DecodeStatus status = entry_type_decoder_.Start(db); + switch (status) { + case DecodeStatus::kDecodeDone: + // The type of the entry and its varint fit into the current decode + // buffer. + if (entry_type_decoder_.entry_type() == HpackEntryType::kIndexedHeader) { + // The entry consists solely of the entry type and varint. + // This is by far the most common case in practice. + listener->OnIndexedHeader(entry_type_decoder_.varint()); + return DecodeStatus::kDecodeDone; + } + state_ = EntryDecoderState::kDecodedType; + return Resume(db, listener); + case DecodeStatus::kDecodeInProgress: + // Hit the end of the decode buffer before fully decoding + // the entry type and varint. + DCHECK_EQ(0u, db->Remaining()); + state_ = EntryDecoderState::kResumeDecodingType; + return status; + case DecodeStatus::kDecodeError: + // The varint must have been invalid (too long). + return status; + } + + HTTP2_BUG << "Unreachable"; + return DecodeStatus::kDecodeError; +} + +DecodeStatus HpackEntryDecoder::Resume(DecodeBuffer* db, + HpackEntryDecoderListener* listener) { + DCHECK(db != nullptr); + DCHECK(listener != nullptr); + + DecodeStatus status; + + do { + switch (state_) { + case EntryDecoderState::kResumeDecodingType: + // entry_type_decoder_ returned kDecodeInProgress when last called. + DVLOG(1) << "kResumeDecodingType: db->Remaining=" << db->Remaining(); + status = entry_type_decoder_.Resume(db); + if (status != DecodeStatus::kDecodeDone) { + return status; + } + state_ = EntryDecoderState::kDecodedType; + HTTP2_FALLTHROUGH; + + case EntryDecoderState::kDecodedType: + // entry_type_decoder_ returned kDecodeDone, now need to decide how + // to proceed. + DVLOG(1) << "kDecodedType: db->Remaining=" << db->Remaining(); + if (DispatchOnType(listener)) { + // All done. + return DecodeStatus::kDecodeDone; + } + continue; + + case EntryDecoderState::kStartDecodingName: + DVLOG(1) << "kStartDecodingName: db->Remaining=" << db->Remaining(); + { + NameDecoderListener ncb(listener); + status = string_decoder_.Start(db, &ncb); + } + if (status != DecodeStatus::kDecodeDone) { + // On the assumption that the status is kDecodeInProgress, set + // state_ accordingly; unnecessary if status is kDecodeError, but + // that will only happen if the varint encoding the name's length + // is too long. + state_ = EntryDecoderState::kResumeDecodingName; + return status; + } + state_ = EntryDecoderState::kStartDecodingValue; + HTTP2_FALLTHROUGH; + + case EntryDecoderState::kStartDecodingValue: + DVLOG(1) << "kStartDecodingValue: db->Remaining=" << db->Remaining(); + { + ValueDecoderListener vcb(listener); + status = string_decoder_.Start(db, &vcb); + } + if (status == DecodeStatus::kDecodeDone) { + // Done with decoding the literal value, so we've reached the + // end of the header entry. + return status; + } + // On the assumption that the status is kDecodeInProgress, set + // state_ accordingly; unnecessary if status is kDecodeError, but + // that will only happen if the varint encoding the value's length + // is too long. + state_ = EntryDecoderState::kResumeDecodingValue; + return status; + + case EntryDecoderState::kResumeDecodingName: + // The literal name was split across decode buffers. + DVLOG(1) << "kResumeDecodingName: db->Remaining=" << db->Remaining(); + { + NameDecoderListener ncb(listener); + status = string_decoder_.Resume(db, &ncb); + } + if (status != DecodeStatus::kDecodeDone) { + // On the assumption that the status is kDecodeInProgress, set + // state_ accordingly; unnecessary if status is kDecodeError, but + // that will only happen if the varint encoding the name's length + // is too long. + state_ = EntryDecoderState::kResumeDecodingName; + return status; + } + state_ = EntryDecoderState::kStartDecodingValue; + break; + + case EntryDecoderState::kResumeDecodingValue: + // The literal value was split across decode buffers. + DVLOG(1) << "kResumeDecodingValue: db->Remaining=" << db->Remaining(); + { + ValueDecoderListener vcb(listener); + status = string_decoder_.Resume(db, &vcb); + } + if (status == DecodeStatus::kDecodeDone) { + // Done with decoding the value, therefore the entry as a whole. + return status; + } + // On the assumption that the status is kDecodeInProgress, set + // state_ accordingly; unnecessary if status is kDecodeError, but + // that will only happen if the varint encoding the value's length + // is too long. + state_ = EntryDecoderState::kResumeDecodingValue; + return status; + } + } while (true); +} + +bool HpackEntryDecoder::DispatchOnType(HpackEntryDecoderListener* listener) { + const HpackEntryType entry_type = entry_type_decoder_.entry_type(); + const uint32_t varint = entry_type_decoder_.varint(); + switch (entry_type) { + case HpackEntryType::kIndexedHeader: + // The entry consists solely of the entry type and varint. See: + // http://httpwg.org/specs/rfc7541.html#indexed.header.representation + listener->OnIndexedHeader(varint); + return true; + + case HpackEntryType::kIndexedLiteralHeader: + case HpackEntryType::kUnindexedLiteralHeader: + case HpackEntryType::kNeverIndexedLiteralHeader: + // The entry has a literal value, and if the varint is zero also has a + // literal name preceding the value. See: + // http://httpwg.org/specs/rfc7541.html#literal.header.representation + listener->OnStartLiteralHeader(entry_type, varint); + if (varint == 0) { + state_ = EntryDecoderState::kStartDecodingName; + } else { + state_ = EntryDecoderState::kStartDecodingValue; + } + return false; + + case HpackEntryType::kDynamicTableSizeUpdate: + // The entry consists solely of the entry type and varint. FWIW, I've + // never seen this type of entry in production (primarily browser + // traffic) so if you're designing an HPACK successor someday, consider + // dropping it or giving it a much longer prefix. See: + // http://httpwg.org/specs/rfc7541.html#encoding.context.update + listener->OnDynamicTableSizeUpdate(varint); + return true; + } + + HTTP2_BUG << "Unreachable, entry_type=" << entry_type; + return true; +} + +void HpackEntryDecoder::OutputDebugString(std::ostream& out) const { + out << "HpackEntryDecoder(state=" << state_ << ", " << entry_type_decoder_ + << ", " << string_decoder_ << ")"; +} + +Http2String HpackEntryDecoder::DebugString() const { + std::stringstream s; + s << *this; + return s.str(); +} + +std::ostream& operator<<(std::ostream& out, const HpackEntryDecoder& v) { + v.OutputDebugString(out); + return out; +} + +std::ostream& operator<<(std::ostream& out, + HpackEntryDecoder::EntryDecoderState state) { + typedef HpackEntryDecoder::EntryDecoderState EntryDecoderState; + switch (state) { + case EntryDecoderState::kResumeDecodingType: + return out << "kResumeDecodingType"; + case EntryDecoderState::kDecodedType: + return out << "kDecodedType"; + case EntryDecoderState::kStartDecodingName: + return out << "kStartDecodingName"; + case EntryDecoderState::kResumeDecodingName: + return out << "kResumeDecodingName"; + case EntryDecoderState::kStartDecodingValue: + return out << "kStartDecodingValue"; + case EntryDecoderState::kResumeDecodingValue: + return out << "kResumeDecodingValue"; + } + return out << static_cast<int>(state); +} + +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_entry_decoder.h b/http2/hpack/decoder/hpack_entry_decoder.h new file mode 100644 index 0000000..fe59d96 --- /dev/null +++ b/http2/hpack/decoder/hpack_entry_decoder.h
@@ -0,0 +1,85 @@ +// Copyright 2016 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. + +#ifndef QUICHE_HTTP2_HPACK_DECODER_HPACK_ENTRY_DECODER_H_ +#define QUICHE_HTTP2_HPACK_DECODER_HPACK_ENTRY_DECODER_H_ + +// HpackEntryDecoder decodes a single HPACK entry (i.e. one header or one +// dynamic table size update), in a resumable fashion. The first call, Start(), +// must provide a non-empty decode buffer. Continue with calls to Resume() if +// Start, and any subsequent calls to Resume, returns kDecodeInProgress. + +#include "base/logging.h" +#include "net/third_party/quiche/src/http2/decoder/decode_buffer.h" +#include "net/third_party/quiche/src/http2/decoder/decode_status.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_entry_decoder_listener.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_entry_type_decoder.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_string_decoder.h" +#include "net/third_party/quiche/src/http2/hpack/http2_hpack_constants.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_export.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string.h" + +namespace http2 { + +class HTTP2_EXPORT_PRIVATE HpackEntryDecoder { + public: + enum class EntryDecoderState { + // Have started decoding the type/varint, but didn't finish on the previous + // attempt. Next state is kResumeDecodingType or kDecodedType. + kResumeDecodingType, + + // Have just finished decoding the type/varint. Final state if the type is + // kIndexedHeader or kDynamicTableSizeUpdate. Otherwise, the next state is + // kStartDecodingName (if the varint is 0), else kStartDecodingValue. + kDecodedType, + + // Ready to start decoding the literal name of a header entry. Next state + // is kResumeDecodingName (if the name is split across decode buffers), + // else kStartDecodingValue. + kStartDecodingName, + + // Resume decoding the literal name of a header that is split across decode + // buffers. + kResumeDecodingName, + + // Ready to start decoding the literal value of a header entry. Final state + // if the value string is entirely in the decode buffer, else the next state + // is kResumeDecodingValue. + kStartDecodingValue, + + // Resume decoding the literal value of a header that is split across decode + // buffers. + kResumeDecodingValue, + }; + + // Only call when the decode buffer has data (i.e. HpackBlockDecoder must + // not call until there is data). + DecodeStatus Start(DecodeBuffer* db, HpackEntryDecoderListener* listener); + + // Only call Resume if the previous call (Start or Resume) returned + // kDecodeInProgress; Resume is also called from Start when it has succeeded + // in decoding the entry type and its varint. + DecodeStatus Resume(DecodeBuffer* db, HpackEntryDecoderListener* listener); + + Http2String DebugString() const; + void OutputDebugString(std::ostream& out) const; + + private: + // Implements handling state kDecodedType. + bool DispatchOnType(HpackEntryDecoderListener* listener); + + HpackEntryTypeDecoder entry_type_decoder_; + HpackStringDecoder string_decoder_; + EntryDecoderState state_ = EntryDecoderState(); +}; + +HTTP2_EXPORT_PRIVATE std::ostream& operator<<(std::ostream& out, + const HpackEntryDecoder& v); +HTTP2_EXPORT_PRIVATE std::ostream& operator<<( + std::ostream& out, + HpackEntryDecoder::EntryDecoderState state); + +} // namespace http2 + +#endif // QUICHE_HTTP2_HPACK_DECODER_HPACK_ENTRY_DECODER_H_
diff --git a/http2/hpack/decoder/hpack_entry_decoder_listener.cc b/http2/hpack/decoder/hpack_entry_decoder_listener.cc new file mode 100644 index 0000000..b783b15 --- /dev/null +++ b/http2/hpack/decoder/hpack_entry_decoder_listener.cc
@@ -0,0 +1,81 @@ +// Copyright 2016 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_entry_decoder_listener.h" + +#include "base/logging.h" + +namespace http2 { + +void HpackEntryDecoderVLoggingListener::OnIndexedHeader(size_t index) { + VLOG(1) << "OnIndexedHeader, index=" << index; + if (wrapped_) { + wrapped_->OnIndexedHeader(index); + } +} + +void HpackEntryDecoderVLoggingListener::OnStartLiteralHeader( + HpackEntryType entry_type, + size_t maybe_name_index) { + VLOG(1) << "OnStartLiteralHeader: entry_type=" << entry_type + << ", maybe_name_index=" << maybe_name_index; + if (wrapped_) { + wrapped_->OnStartLiteralHeader(entry_type, maybe_name_index); + } +} + +void HpackEntryDecoderVLoggingListener::OnNameStart(bool huffman_encoded, + size_t len) { + VLOG(1) << "OnNameStart: H=" << huffman_encoded << ", len=" << len; + if (wrapped_) { + wrapped_->OnNameStart(huffman_encoded, len); + } +} + +void HpackEntryDecoderVLoggingListener::OnNameData(const char* data, + size_t len) { + VLOG(1) << "OnNameData: len=" << len; + if (wrapped_) { + wrapped_->OnNameData(data, len); + } +} + +void HpackEntryDecoderVLoggingListener::OnNameEnd() { + VLOG(1) << "OnNameEnd"; + if (wrapped_) { + wrapped_->OnNameEnd(); + } +} + +void HpackEntryDecoderVLoggingListener::OnValueStart(bool huffman_encoded, + size_t len) { + VLOG(1) << "OnValueStart: H=" << huffman_encoded << ", len=" << len; + if (wrapped_) { + wrapped_->OnValueStart(huffman_encoded, len); + } +} + +void HpackEntryDecoderVLoggingListener::OnValueData(const char* data, + size_t len) { + VLOG(1) << "OnValueData: len=" << len; + if (wrapped_) { + wrapped_->OnValueData(data, len); + } +} + +void HpackEntryDecoderVLoggingListener::OnValueEnd() { + VLOG(1) << "OnValueEnd"; + if (wrapped_) { + wrapped_->OnValueEnd(); + } +} + +void HpackEntryDecoderVLoggingListener::OnDynamicTableSizeUpdate(size_t size) { + VLOG(1) << "OnDynamicTableSizeUpdate: size=" << size; + if (wrapped_) { + wrapped_->OnDynamicTableSizeUpdate(size); + } +} + +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_entry_decoder_listener.h b/http2/hpack/decoder/hpack_entry_decoder_listener.h new file mode 100644 index 0000000..fd11f59 --- /dev/null +++ b/http2/hpack/decoder/hpack_entry_decoder_listener.h
@@ -0,0 +1,110 @@ +// Copyright 2016 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. + +#ifndef QUICHE_HTTP2_HPACK_DECODER_HPACK_ENTRY_DECODER_LISTENER_H_ +#define QUICHE_HTTP2_HPACK_DECODER_HPACK_ENTRY_DECODER_LISTENER_H_ + +// Defines HpackEntryDecoderListener, the base class of listeners that +// HpackEntryDecoder calls. Also defines HpackEntryDecoderVLoggingListener +// which logs before calling another HpackEntryDecoderListener implementation. + +#include <stddef.h> + +#include "net/third_party/quiche/src/http2/hpack/http2_hpack_constants.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_export.h" + +namespace http2 { + +class HTTP2_EXPORT_PRIVATE HpackEntryDecoderListener { + public: + virtual ~HpackEntryDecoderListener() {} + + // Called when an indexed header (i.e. one in the static or dynamic table) has + // been decoded from an HPACK block. index is supposed to be non-zero, but + // that has not been checked by the caller. + virtual void OnIndexedHeader(size_t index) = 0; + + // Called when the start of a header with a literal value, and maybe a literal + // name, has been decoded. maybe_name_index is zero if the header has a + // literal name, else it is a reference into the static or dynamic table, from + // which the name should be determined. When the name is literal, the next + // call will be to OnNameStart; else it will be to OnValueStart. entry_type + // indicates whether the peer has added the entry to its dynamic table, and + // whether a proxy is permitted to do so when forwarding the entry. + virtual void OnStartLiteralHeader(HpackEntryType entry_type, + size_t maybe_name_index) = 0; + + // Called when the encoding (Huffman compressed or plain text) and the encoded + // length of a literal name has been decoded. OnNameData will be called next, + // and repeatedly until the sum of lengths passed to OnNameData is len. + virtual void OnNameStart(bool huffman_encoded, size_t len) = 0; + + // Called when len bytes of an encoded header name have been decoded. + virtual void OnNameData(const char* data, size_t len) = 0; + + // Called after the entire name has been passed to OnNameData. + // OnValueStart will be called next. + virtual void OnNameEnd() = 0; + + // Called when the encoding (Huffman compressed or plain text) and the encoded + // length of a literal value has been decoded. OnValueData will be called + // next, and repeatedly until the sum of lengths passed to OnValueData is len. + virtual void OnValueStart(bool huffman_encoded, size_t len) = 0; + + // Called when len bytes of an encoded header value have been decoded. + virtual void OnValueData(const char* data, size_t len) = 0; + + // Called after the entire value has been passed to OnValueData, marking the + // end of a header entry with a literal value, and maybe a literal name. + virtual void OnValueEnd() = 0; + + // Called when an update to the size of the peer's dynamic table has been + // decoded. + virtual void OnDynamicTableSizeUpdate(size_t size) = 0; +}; + +class HTTP2_EXPORT_PRIVATE HpackEntryDecoderVLoggingListener + : public HpackEntryDecoderListener { + public: + HpackEntryDecoderVLoggingListener() : wrapped_(nullptr) {} + explicit HpackEntryDecoderVLoggingListener(HpackEntryDecoderListener* wrapped) + : wrapped_(wrapped) {} + ~HpackEntryDecoderVLoggingListener() override {} + + void OnIndexedHeader(size_t index) override; + void OnStartLiteralHeader(HpackEntryType entry_type, + size_t maybe_name_index) override; + void OnNameStart(bool huffman_encoded, size_t len) override; + void OnNameData(const char* data, size_t len) override; + void OnNameEnd() override; + void OnValueStart(bool huffman_encoded, size_t len) override; + void OnValueData(const char* data, size_t len) override; + void OnValueEnd() override; + void OnDynamicTableSizeUpdate(size_t size) override; + + private: + HpackEntryDecoderListener* const wrapped_; +}; + +// A no-op implementation of HpackEntryDecoderListener. +class HTTP2_EXPORT_PRIVATE HpackEntryDecoderNoOpListener + : public HpackEntryDecoderListener { + public: + ~HpackEntryDecoderNoOpListener() override {} + + void OnIndexedHeader(size_t index) override {} + void OnStartLiteralHeader(HpackEntryType entry_type, + size_t maybe_name_index) override {} + void OnNameStart(bool huffman_encoded, size_t len) override {} + void OnNameData(const char* data, size_t len) override {} + void OnNameEnd() override {} + void OnValueStart(bool huffman_encoded, size_t len) override {} + void OnValueData(const char* data, size_t len) override {} + void OnValueEnd() override {} + void OnDynamicTableSizeUpdate(size_t size) override {} +}; + +} // namespace http2 + +#endif // QUICHE_HTTP2_HPACK_DECODER_HPACK_ENTRY_DECODER_LISTENER_H_
diff --git a/http2/hpack/decoder/hpack_entry_decoder_test.cc b/http2/hpack/decoder/hpack_entry_decoder_test.cc new file mode 100644 index 0000000..7249bd5 --- /dev/null +++ b/http2/hpack/decoder/hpack_entry_decoder_test.cc
@@ -0,0 +1,211 @@ +// Copyright 2016 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_entry_decoder.h" + +// Tests of HpackEntryDecoder. + +#include <cstdint> + +#include "testing/gtest/include/gtest/gtest.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_entry_collector.h" +#include "net/third_party/quiche/src/http2/hpack/tools/hpack_block_builder.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_test_helpers.h" +#include "net/third_party/quiche/src/http2/test_tools/http2_random.h" +#include "net/third_party/quiche/src/http2/tools/random_decoder_test.h" + +using ::testing::AssertionResult; + +namespace http2 { +namespace test { +namespace { + +class HpackEntryDecoderTest : public RandomDecoderTest { + protected: + HpackEntryDecoderTest() : listener_(&collector_) {} + + DecodeStatus StartDecoding(DecodeBuffer* b) override { + collector_.Clear(); + return decoder_.Start(b, &listener_); + } + + DecodeStatus ResumeDecoding(DecodeBuffer* b) override { + return decoder_.Resume(b, &listener_); + } + + AssertionResult DecodeAndValidateSeveralWays(DecodeBuffer* db, + const Validator& validator) { + // StartDecoding, above, requires the DecodeBuffer be non-empty so that it + // can call Start with the prefix byte. + bool return_non_zero_on_first = true; + return RandomDecoderTest::DecodeAndValidateSeveralWays( + db, return_non_zero_on_first, validator); + } + + AssertionResult DecodeAndValidateSeveralWays(const HpackBlockBuilder& hbb, + const Validator& validator) { + DecodeBuffer db(hbb.buffer()); + return DecodeAndValidateSeveralWays(&db, validator); + } + + HpackEntryDecoder decoder_; + HpackEntryCollector collector_; + HpackEntryDecoderVLoggingListener listener_; +}; + +TEST_F(HpackEntryDecoderTest, IndexedHeader_Literals) { + { + const char input[] = {'\x82'}; // == Index 2 == + DecodeBuffer b(input); + auto do_check = [this]() { + VERIFY_AND_RETURN_SUCCESS(collector_.ValidateIndexedHeader(2)); + }; + EXPECT_TRUE( + DecodeAndValidateSeveralWays(&b, ValidateDoneAndEmpty(do_check))); + EXPECT_TRUE(do_check()); + } + collector_.Clear(); + { + const char input[] = {'\xfe'}; // == Index 126 == + DecodeBuffer b(input); + auto do_check = [this]() { + VERIFY_AND_RETURN_SUCCESS(collector_.ValidateIndexedHeader(126)); + }; + EXPECT_TRUE( + DecodeAndValidateSeveralWays(&b, ValidateDoneAndEmpty(do_check))); + EXPECT_TRUE(do_check()); + } + collector_.Clear(); + { + const char input[] = {'\xff', '\x00'}; // == Index 127 == + DecodeBuffer b(input); + auto do_check = [this]() { + VERIFY_AND_RETURN_SUCCESS(collector_.ValidateIndexedHeader(127)); + }; + EXPECT_TRUE( + DecodeAndValidateSeveralWays(&b, ValidateDoneAndEmpty(do_check))); + EXPECT_TRUE(do_check()); + } +} + +TEST_F(HpackEntryDecoderTest, IndexedHeader_Various) { + // Indices chosen to hit encoding and table boundaries. + for (const uint32_t ndx : {1, 2, 61, 62, 63, 126, 127, 254, 255, 256}) { + HpackBlockBuilder hbb; + hbb.AppendIndexedHeader(ndx); + + auto do_check = [this, ndx]() { + VERIFY_AND_RETURN_SUCCESS(collector_.ValidateIndexedHeader(ndx)); + }; + EXPECT_TRUE( + DecodeAndValidateSeveralWays(hbb, ValidateDoneAndEmpty(do_check))); + EXPECT_TRUE(do_check()); + } +} + +TEST_F(HpackEntryDecoderTest, IndexedLiteralValue_Literal) { + const char input[] = + "\x7f" // == Literal indexed, name index 0x40 == + "\x01" // 2nd byte of name index (0x01 + 0x3f == 0x40) + "\x0d" // Value length (13) + "custom-header"; // Value + DecodeBuffer b(input, sizeof input - 1); + auto do_check = [this]() { + VERIFY_AND_RETURN_SUCCESS(collector_.ValidateLiteralValueHeader( + HpackEntryType::kIndexedLiteralHeader, 0x40, false, "custom-header")); + }; + EXPECT_TRUE(DecodeAndValidateSeveralWays(&b, ValidateDoneAndEmpty(do_check))); + EXPECT_TRUE(do_check()); +} + +TEST_F(HpackEntryDecoderTest, IndexedLiteralNameValue_Literal) { + const char input[] = + "\x40" // == Literal indexed == + "\x0a" // Name length (10) + "custom-key" // Name + "\x0d" // Value length (13) + "custom-header"; // Value + + DecodeBuffer b(input, sizeof input - 1); + auto do_check = [this]() { + VERIFY_AND_RETURN_SUCCESS(collector_.ValidateLiteralNameValueHeader( + HpackEntryType::kIndexedLiteralHeader, false, "custom-key", false, + "custom-header")); + }; + EXPECT_TRUE(DecodeAndValidateSeveralWays(&b, ValidateDoneAndEmpty(do_check))); + EXPECT_TRUE(do_check()); +} + +TEST_F(HpackEntryDecoderTest, DynamicTableSizeUpdate_Literal) { + // Size update, length 31. + const char input[] = "\x3f\x00"; + DecodeBuffer b(input, 2); + auto do_check = [this]() { + VERIFY_AND_RETURN_SUCCESS(collector_.ValidateDynamicTableSizeUpdate(31)); + }; + EXPECT_TRUE(DecodeAndValidateSeveralWays(&b, ValidateDoneAndEmpty(do_check))); + EXPECT_TRUE(do_check()); +} + +class HpackLiteralEntryDecoderTest + : public HpackEntryDecoderTest, + public ::testing::WithParamInterface<HpackEntryType> { + protected: + HpackLiteralEntryDecoderTest() : entry_type_(GetParam()) {} + + const HpackEntryType entry_type_; +}; + +INSTANTIATE_TEST_CASE_P( + AllLiteralTypes, + HpackLiteralEntryDecoderTest, + testing::Values(HpackEntryType::kIndexedLiteralHeader, + HpackEntryType::kUnindexedLiteralHeader, + HpackEntryType::kNeverIndexedLiteralHeader)); + +TEST_P(HpackLiteralEntryDecoderTest, RandNameIndexAndLiteralValue) { + for (int n = 0; n < 10; n++) { + const uint32_t ndx = 1 + Random().Rand8(); + const bool value_is_huffman_encoded = (n % 2) == 0; + const Http2String value = Random().RandString(Random().Rand8()); + HpackBlockBuilder hbb; + hbb.AppendNameIndexAndLiteralValue(entry_type_, ndx, + value_is_huffman_encoded, value); + auto do_check = [this, ndx, value_is_huffman_encoded, + value]() -> AssertionResult { + VERIFY_AND_RETURN_SUCCESS(collector_.ValidateLiteralValueHeader( + entry_type_, ndx, value_is_huffman_encoded, value)); + }; + EXPECT_TRUE( + DecodeAndValidateSeveralWays(hbb, ValidateDoneAndEmpty(do_check))); + EXPECT_TRUE(do_check()); + } +} + +TEST_P(HpackLiteralEntryDecoderTest, RandLiteralNameAndValue) { + for (int n = 0; n < 10; n++) { + const bool name_is_huffman_encoded = (n & 1) == 0; + const int name_len = 1 + Random().Rand8(); + const Http2String name = Random().RandString(name_len); + const bool value_is_huffman_encoded = (n & 2) == 0; + const int value_len = Random().Skewed(10); + const Http2String value = Random().RandString(value_len); + HpackBlockBuilder hbb; + hbb.AppendLiteralNameAndValue(entry_type_, name_is_huffman_encoded, name, + value_is_huffman_encoded, value); + auto do_check = [this, name_is_huffman_encoded, name, + value_is_huffman_encoded, value]() -> AssertionResult { + VERIFY_AND_RETURN_SUCCESS(collector_.ValidateLiteralNameValueHeader( + entry_type_, name_is_huffman_encoded, name, value_is_huffman_encoded, + value)); + }; + EXPECT_TRUE( + DecodeAndValidateSeveralWays(hbb, ValidateDoneAndEmpty(do_check))); + EXPECT_TRUE(do_check()); + } +} + +} // namespace +} // namespace test +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_entry_type_decoder.cc b/http2/hpack/decoder/hpack_entry_type_decoder.cc new file mode 100644 index 0000000..121e3d0 --- /dev/null +++ b/http2/hpack/decoder/hpack_entry_type_decoder.cc
@@ -0,0 +1,358 @@ +// Copyright 2016 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_entry_type_decoder.h" + +#include "base/logging.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_bug_tracker.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string_utils.h" + +namespace http2 { + +Http2String HpackEntryTypeDecoder::DebugString() const { + return Http2StrCat( + "HpackEntryTypeDecoder(varint_decoder=", varint_decoder_.DebugString(), + ", entry_type=", entry_type_, ")"); +} + +std::ostream& operator<<(std::ostream& out, const HpackEntryTypeDecoder& v) { + return out << v.DebugString(); +} + +// This ridiculous looking function turned out to be the winner in benchmarking +// of several very different alternative implementations. It would be even +// faster (~7%) if inlined in the header file, but I'm not sure if that is +// worth doing... yet. +// TODO(jamessynge): Benchmark again at a higher level (e.g. at least at the +// full HTTP/2 decoder level, but preferably still higher) to determine if the +// alternatives that take less code/data space are preferable in that situation. +DecodeStatus HpackEntryTypeDecoder::Start(DecodeBuffer* db) { + DCHECK(db != nullptr); + DCHECK(db->HasData()); + + // The high four bits (nibble) of first byte of the entry determine the type + // of the entry, and may also be the initial bits of the varint that + // represents an index or table size. Note the use of the word 'initial' + // rather than 'high'; the HPACK encoding of varints is not in network + // order (i.e. not big-endian, the high-order byte isn't first), nor in + // little-endian order. See: + // http://httpwg.org/specs/rfc7541.html#integer.representation + uint8_t byte = db->DecodeUInt8(); + switch (byte) { + case 0b00000000: + case 0b00000001: + case 0b00000010: + case 0b00000011: + case 0b00000100: + case 0b00000101: + case 0b00000110: + case 0b00000111: + case 0b00001000: + case 0b00001001: + case 0b00001010: + case 0b00001011: + case 0b00001100: + case 0b00001101: + case 0b00001110: + // The low 4 bits of |byte| are the initial bits of the varint. + // One of those bits is 0, so the varint is only one byte long. + entry_type_ = HpackEntryType::kUnindexedLiteralHeader; + varint_decoder_.set_value(byte); + return DecodeStatus::kDecodeDone; + + case 0b00001111: + // The low 4 bits of |byte| are the initial bits of the varint. All 4 + // are 1, so the varint extends into another byte. + entry_type_ = HpackEntryType::kUnindexedLiteralHeader; + return varint_decoder_.StartExtended(4, db); + + case 0b00010000: + case 0b00010001: + case 0b00010010: + case 0b00010011: + case 0b00010100: + case 0b00010101: + case 0b00010110: + case 0b00010111: + case 0b00011000: + case 0b00011001: + case 0b00011010: + case 0b00011011: + case 0b00011100: + case 0b00011101: + case 0b00011110: + // The low 4 bits of |byte| are the initial bits of the varint. + // One of those bits is 0, so the varint is only one byte long. + entry_type_ = HpackEntryType::kNeverIndexedLiteralHeader; + varint_decoder_.set_value(byte & 0x0f); + return DecodeStatus::kDecodeDone; + + case 0b00011111: + // The low 4 bits of |byte| are the initial bits of the varint. + // All of those bits are 1, so the varint extends into another byte. + entry_type_ = HpackEntryType::kNeverIndexedLiteralHeader; + return varint_decoder_.StartExtended(4, db); + + case 0b00100000: + case 0b00100001: + case 0b00100010: + case 0b00100011: + case 0b00100100: + case 0b00100101: + case 0b00100110: + case 0b00100111: + case 0b00101000: + case 0b00101001: + case 0b00101010: + case 0b00101011: + case 0b00101100: + case 0b00101101: + case 0b00101110: + case 0b00101111: + case 0b00110000: + case 0b00110001: + case 0b00110010: + case 0b00110011: + case 0b00110100: + case 0b00110101: + case 0b00110110: + case 0b00110111: + case 0b00111000: + case 0b00111001: + case 0b00111010: + case 0b00111011: + case 0b00111100: + case 0b00111101: + case 0b00111110: + entry_type_ = HpackEntryType::kDynamicTableSizeUpdate; + // The low 5 bits of |byte| are the initial bits of the varint. + // One of those bits is 0, so the varint is only one byte long. + varint_decoder_.set_value(byte & 0x01f); + return DecodeStatus::kDecodeDone; + + case 0b00111111: + entry_type_ = HpackEntryType::kDynamicTableSizeUpdate; + // The low 5 bits of |byte| are the initial bits of the varint. + // All of those bits are 1, so the varint extends into another byte. + return varint_decoder_.StartExtended(5, db); + + case 0b01000000: + case 0b01000001: + case 0b01000010: + case 0b01000011: + case 0b01000100: + case 0b01000101: + case 0b01000110: + case 0b01000111: + case 0b01001000: + case 0b01001001: + case 0b01001010: + case 0b01001011: + case 0b01001100: + case 0b01001101: + case 0b01001110: + case 0b01001111: + case 0b01010000: + case 0b01010001: + case 0b01010010: + case 0b01010011: + case 0b01010100: + case 0b01010101: + case 0b01010110: + case 0b01010111: + case 0b01011000: + case 0b01011001: + case 0b01011010: + case 0b01011011: + case 0b01011100: + case 0b01011101: + case 0b01011110: + case 0b01011111: + case 0b01100000: + case 0b01100001: + case 0b01100010: + case 0b01100011: + case 0b01100100: + case 0b01100101: + case 0b01100110: + case 0b01100111: + case 0b01101000: + case 0b01101001: + case 0b01101010: + case 0b01101011: + case 0b01101100: + case 0b01101101: + case 0b01101110: + case 0b01101111: + case 0b01110000: + case 0b01110001: + case 0b01110010: + case 0b01110011: + case 0b01110100: + case 0b01110101: + case 0b01110110: + case 0b01110111: + case 0b01111000: + case 0b01111001: + case 0b01111010: + case 0b01111011: + case 0b01111100: + case 0b01111101: + case 0b01111110: + entry_type_ = HpackEntryType::kIndexedLiteralHeader; + // The low 6 bits of |byte| are the initial bits of the varint. + // One of those bits is 0, so the varint is only one byte long. + varint_decoder_.set_value(byte & 0x03f); + return DecodeStatus::kDecodeDone; + + case 0b01111111: + entry_type_ = HpackEntryType::kIndexedLiteralHeader; + // The low 6 bits of |byte| are the initial bits of the varint. + // All of those bits are 1, so the varint extends into another byte. + return varint_decoder_.StartExtended(6, db); + + case 0b10000000: + case 0b10000001: + case 0b10000010: + case 0b10000011: + case 0b10000100: + case 0b10000101: + case 0b10000110: + case 0b10000111: + case 0b10001000: + case 0b10001001: + case 0b10001010: + case 0b10001011: + case 0b10001100: + case 0b10001101: + case 0b10001110: + case 0b10001111: + case 0b10010000: + case 0b10010001: + case 0b10010010: + case 0b10010011: + case 0b10010100: + case 0b10010101: + case 0b10010110: + case 0b10010111: + case 0b10011000: + case 0b10011001: + case 0b10011010: + case 0b10011011: + case 0b10011100: + case 0b10011101: + case 0b10011110: + case 0b10011111: + case 0b10100000: + case 0b10100001: + case 0b10100010: + case 0b10100011: + case 0b10100100: + case 0b10100101: + case 0b10100110: + case 0b10100111: + case 0b10101000: + case 0b10101001: + case 0b10101010: + case 0b10101011: + case 0b10101100: + case 0b10101101: + case 0b10101110: + case 0b10101111: + case 0b10110000: + case 0b10110001: + case 0b10110010: + case 0b10110011: + case 0b10110100: + case 0b10110101: + case 0b10110110: + case 0b10110111: + case 0b10111000: + case 0b10111001: + case 0b10111010: + case 0b10111011: + case 0b10111100: + case 0b10111101: + case 0b10111110: + case 0b10111111: + case 0b11000000: + case 0b11000001: + case 0b11000010: + case 0b11000011: + case 0b11000100: + case 0b11000101: + case 0b11000110: + case 0b11000111: + case 0b11001000: + case 0b11001001: + case 0b11001010: + case 0b11001011: + case 0b11001100: + case 0b11001101: + case 0b11001110: + case 0b11001111: + case 0b11010000: + case 0b11010001: + case 0b11010010: + case 0b11010011: + case 0b11010100: + case 0b11010101: + case 0b11010110: + case 0b11010111: + case 0b11011000: + case 0b11011001: + case 0b11011010: + case 0b11011011: + case 0b11011100: + case 0b11011101: + case 0b11011110: + case 0b11011111: + case 0b11100000: + case 0b11100001: + case 0b11100010: + case 0b11100011: + case 0b11100100: + case 0b11100101: + case 0b11100110: + case 0b11100111: + case 0b11101000: + case 0b11101001: + case 0b11101010: + case 0b11101011: + case 0b11101100: + case 0b11101101: + case 0b11101110: + case 0b11101111: + case 0b11110000: + case 0b11110001: + case 0b11110010: + case 0b11110011: + case 0b11110100: + case 0b11110101: + case 0b11110110: + case 0b11110111: + case 0b11111000: + case 0b11111001: + case 0b11111010: + case 0b11111011: + case 0b11111100: + case 0b11111101: + case 0b11111110: + entry_type_ = HpackEntryType::kIndexedHeader; + // The low 7 bits of |byte| are the initial bits of the varint. + // One of those bits is 0, so the varint is only one byte long. + varint_decoder_.set_value(byte & 0x07f); + return DecodeStatus::kDecodeDone; + + case 0b11111111: + entry_type_ = HpackEntryType::kIndexedHeader; + // The low 7 bits of |byte| are the initial bits of the varint. + // All of those bits are 1, so the varint extends into another byte. + return varint_decoder_.StartExtended(7, db); + } + HTTP2_BUG << "Unreachable, byte=" << std::hex << static_cast<uint32_t>(byte); + return DecodeStatus::kDecodeError; +} + +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_entry_type_decoder.h b/http2/hpack/decoder/hpack_entry_type_decoder.h new file mode 100644 index 0000000..1c0f2ac --- /dev/null +++ b/http2/hpack/decoder/hpack_entry_type_decoder.h
@@ -0,0 +1,57 @@ +// Copyright 2016 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. + +#ifndef QUICHE_HTTP2_HPACK_DECODER_HPACK_ENTRY_TYPE_DECODER_H_ +#define QUICHE_HTTP2_HPACK_DECODER_HPACK_ENTRY_TYPE_DECODER_H_ + +// Decodes the type of an HPACK entry, and the variable length integer whose +// prefix is in the low-order bits of the same byte, "below" the type bits. +// The integer represents an index into static or dynamic table, which may be +// zero, or is the new size limit of the dynamic table. + +#include <cstdint> + +#include "base/logging.h" +#include "net/third_party/quiche/src/http2/decoder/decode_buffer.h" +#include "net/third_party/quiche/src/http2/decoder/decode_status.h" +#include "net/third_party/quiche/src/http2/hpack/http2_hpack_constants.h" +#include "net/third_party/quiche/src/http2/hpack/varint/hpack_varint_decoder.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_export.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string.h" + +namespace http2 { + +class HTTP2_EXPORT_PRIVATE HpackEntryTypeDecoder { + public: + // Only call when the decode buffer has data (i.e. HpackEntryDecoder must + // not call until there is data). + DecodeStatus Start(DecodeBuffer* db); + + // Only call Resume if the previous call (Start or Resume) returned + // DecodeStatus::kDecodeInProgress. + DecodeStatus Resume(DecodeBuffer* db) { return varint_decoder_.Resume(db); } + + // Returns the decoded entry type. Only call if the preceding call to Start + // or Resume returned kDecodeDone. + HpackEntryType entry_type() const { return entry_type_; } + + // Returns the decoded variable length integer. Only call if the + // preceding call to Start or Resume returned kDecodeDone. + uint32_t varint() const { return varint_decoder_.value(); } + + Http2String DebugString() const; + + private: + HpackVarintDecoder varint_decoder_; + + // This field is initialized just to keep ASAN happy about reading it + // from DebugString(). + HpackEntryType entry_type_ = HpackEntryType::kIndexedHeader; +}; + +HTTP2_EXPORT_PRIVATE std::ostream& operator<<(std::ostream& out, + const HpackEntryTypeDecoder& v); + +} // namespace http2 +#endif // QUICHE_HTTP2_HPACK_DECODER_HPACK_ENTRY_TYPE_DECODER_H_
diff --git a/http2/hpack/decoder/hpack_entry_type_decoder_test.cc b/http2/hpack/decoder/hpack_entry_type_decoder_test.cc new file mode 100644 index 0000000..afff7cc --- /dev/null +++ b/http2/hpack/decoder/hpack_entry_type_decoder_test.cc
@@ -0,0 +1,87 @@ +// Copyright 2016 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_entry_type_decoder.h" + +#include <vector> + +#include "base/logging.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "net/third_party/quiche/src/http2/hpack/tools/hpack_block_builder.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_test_helpers.h" +#include "net/third_party/quiche/src/http2/tools/random_decoder_test.h" + +using ::testing::AssertionFailure; +using ::testing::AssertionResult; +using ::testing::AssertionSuccess; + +namespace http2 { +namespace test { +namespace { +const bool kReturnNonZeroOnFirst = true; + +class HpackEntryTypeDecoderTest : public RandomDecoderTest { + protected: + DecodeStatus StartDecoding(DecodeBuffer* b) override { + CHECK_LT(0u, b->Remaining()); + return decoder_.Start(b); + } + + DecodeStatus ResumeDecoding(DecodeBuffer* b) override { + return decoder_.Resume(b); + } + + HpackEntryTypeDecoder decoder_; +}; + +TEST_F(HpackEntryTypeDecoderTest, DynamicTableSizeUpdate) { + for (uint32_t size = 0; size < 1000 * 1000; size += 256) { + HpackBlockBuilder bb; + bb.AppendDynamicTableSizeUpdate(size); + DecodeBuffer db(bb.buffer()); + auto validator = [size, this]() -> AssertionResult { + VERIFY_EQ(HpackEntryType::kDynamicTableSizeUpdate, decoder_.entry_type()); + VERIFY_EQ(size, decoder_.varint()); + return AssertionSuccess(); + }; + EXPECT_TRUE(DecodeAndValidateSeveralWays(&db, kReturnNonZeroOnFirst, + ValidateDoneAndEmpty(validator))) + << "\nentry_type=kDynamicTableSizeUpdate, size=" << size; + // Run the validator again to make sure that DecodeAndValidateSeveralWays + // did the right thing. + EXPECT_TRUE(validator()); + } +} + +TEST_F(HpackEntryTypeDecoderTest, HeaderWithIndex) { + std::vector<HpackEntryType> entry_types = { + HpackEntryType::kIndexedHeader, + HpackEntryType::kIndexedLiteralHeader, + HpackEntryType::kUnindexedLiteralHeader, + HpackEntryType::kNeverIndexedLiteralHeader, + }; + for (const HpackEntryType entry_type : entry_types) { + const uint32_t first = entry_type == HpackEntryType::kIndexedHeader ? 1 : 0; + for (uint32_t index = first; index < 1000; ++index) { + HpackBlockBuilder bb; + bb.AppendEntryTypeAndVarint(entry_type, index); + DecodeBuffer db(bb.buffer()); + auto validator = [entry_type, index, this]() -> AssertionResult { + VERIFY_EQ(entry_type, decoder_.entry_type()); + VERIFY_EQ(index, decoder_.varint()); + return AssertionSuccess(); + }; + EXPECT_TRUE(DecodeAndValidateSeveralWays(&db, kReturnNonZeroOnFirst, + ValidateDoneAndEmpty(validator))) + << "\nentry_type=" << entry_type << ", index=" << index; + // Run the validator again to make sure that DecodeAndValidateSeveralWays + // did the right thing. + EXPECT_TRUE(validator()); + } + } +} + +} // namespace +} // namespace test +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_string_collector.cc b/http2/hpack/decoder/hpack_string_collector.cc new file mode 100644 index 0000000..247ce9c --- /dev/null +++ b/http2/hpack/decoder/hpack_string_collector.cc
@@ -0,0 +1,123 @@ +// Copyright 2016 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_string_collector.h" + +#include <stddef.h> + +#include <iosfwd> +#include <ostream> + +#include "testing/gtest/include/gtest/gtest.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string_utils.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_test_helpers.h" + +namespace http2 { +namespace test { +namespace { + +std::ostream& operator<<(std::ostream& out, + HpackStringCollector::CollectorState v) { + switch (v) { + case HpackStringCollector::CollectorState::kGenesis: + return out << "kGenesis"; + case HpackStringCollector::CollectorState::kStarted: + return out << "kStarted"; + case HpackStringCollector::CollectorState::kEnded: + return out << "kEnded"; + } + return out << "UnknownCollectorState"; +} + +} // namespace + +HpackStringCollector::HpackStringCollector() { + Clear(); +} + +HpackStringCollector::HpackStringCollector(const Http2String& str, bool huffman) + : s(str), len(str.size()), huffman_encoded(huffman), state(kEnded) {} + +void HpackStringCollector::Clear() { + s = ""; + len = 0; + huffman_encoded = false; + state = kGenesis; +} + +bool HpackStringCollector::IsClear() const { + return s.empty() && len == 0 && huffman_encoded == false && state == kGenesis; +} + +bool HpackStringCollector::IsInProgress() const { + return state == kStarted; +} + +bool HpackStringCollector::HasEnded() const { + return state == kEnded; +} + +void HpackStringCollector::OnStringStart(bool huffman, size_t length) { + EXPECT_TRUE(IsClear()) << ToString(); + state = kStarted; + huffman_encoded = huffman; + len = length; +} + +void HpackStringCollector::OnStringData(const char* data, size_t length) { + Http2StringPiece sp(data, length); + EXPECT_TRUE(IsInProgress()) << ToString(); + EXPECT_LE(sp.size(), len) << ToString(); + Http2StrAppend(&s, sp); + EXPECT_LE(s.size(), len) << ToString(); +} + +void HpackStringCollector::OnStringEnd() { + EXPECT_TRUE(IsInProgress()) << ToString(); + EXPECT_EQ(s.size(), len) << ToString(); + state = kEnded; +} + +::testing::AssertionResult HpackStringCollector::Collected( + Http2StringPiece str, + bool is_huffman_encoded) const { + VERIFY_TRUE(HasEnded()); + VERIFY_EQ(str.size(), len); + VERIFY_EQ(is_huffman_encoded, huffman_encoded); + VERIFY_EQ(str, s); + return ::testing::AssertionSuccess(); +} + +Http2String HpackStringCollector::ToString() const { + std::stringstream ss; + ss << *this; + return ss.str(); +} + +bool operator==(const HpackStringCollector& a, const HpackStringCollector& b) { + return a.s == b.s && a.len == b.len && + a.huffman_encoded == b.huffman_encoded && a.state == b.state; +} + +bool operator!=(const HpackStringCollector& a, const HpackStringCollector& b) { + return !(a == b); +} + +std::ostream& operator<<(std::ostream& out, const HpackStringCollector& v) { + out << "HpackStringCollector(state=" << v.state; + if (v.state == HpackStringCollector::kGenesis) { + return out << ")"; + } + if (v.huffman_encoded) { + out << ", Huffman Encoded"; + } + out << ", Length=" << v.len; + if (!v.s.empty() && v.len != v.s.size()) { + out << " (" << v.s.size() << ")"; + } + return out << ", String=\"" << Http2HexEscape(v.s) << "\")"; +} + +} // namespace test +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_string_collector.h b/http2/hpack/decoder/hpack_string_collector.h new file mode 100644 index 0000000..76be13b --- /dev/null +++ b/http2/hpack/decoder/hpack_string_collector.h
@@ -0,0 +1,63 @@ +// Copyright 2016 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. + +#ifndef QUICHE_HTTP2_HPACK_DECODER_HPACK_STRING_COLLECTOR_H_ +#define QUICHE_HTTP2_HPACK_DECODER_HPACK_STRING_COLLECTOR_H_ + +// Supports tests of decoding HPACK strings. + +#include <stddef.h> + +#include <iosfwd> + +#include "testing/gtest/include/gtest/gtest.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_string_decoder_listener.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string_piece.h" + +namespace http2 { +namespace test { + +// Records the callbacks associated with a decoding a string; must +// call Clear() between decoding successive strings. +struct HpackStringCollector : public HpackStringDecoderListener { + enum CollectorState { + kGenesis, + kStarted, + kEnded, + }; + + HpackStringCollector(); + HpackStringCollector(const Http2String& str, bool huffman); + + void Clear(); + bool IsClear() const; + bool IsInProgress() const; + bool HasEnded() const; + + void OnStringStart(bool huffman, size_t length) override; + void OnStringData(const char* data, size_t length) override; + void OnStringEnd() override; + + ::testing::AssertionResult Collected(Http2StringPiece str, + bool is_huffman_encoded) const; + + Http2String ToString() const; + + Http2String s; + size_t len; + bool huffman_encoded; + CollectorState state; +}; + +bool operator==(const HpackStringCollector& a, const HpackStringCollector& b); + +bool operator!=(const HpackStringCollector& a, const HpackStringCollector& b); + +std::ostream& operator<<(std::ostream& out, const HpackStringCollector& v); + +} // namespace test +} // namespace http2 + +#endif // QUICHE_HTTP2_HPACK_DECODER_HPACK_STRING_COLLECTOR_H_
diff --git a/http2/hpack/decoder/hpack_string_decoder.cc b/http2/hpack/decoder/hpack_string_decoder.cc new file mode 100644 index 0000000..0b9eb59 --- /dev/null +++ b/http2/hpack/decoder/hpack_string_decoder.cc
@@ -0,0 +1,35 @@ +// Copyright 2016 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_string_decoder.h" + +#include "net/third_party/quiche/src/http2/platform/api/http2_string_utils.h" + +namespace http2 { + +Http2String HpackStringDecoder::DebugString() const { + return Http2StrCat("HpackStringDecoder(state=", StateToString(state_), + ", length=", length_decoder_.DebugString(), + ", remaining=", remaining_, + ", huffman=", huffman_encoded_ ? "true)" : "false)"); +} + +// static +Http2String HpackStringDecoder::StateToString(StringDecoderState v) { + switch (v) { + case kStartDecodingLength: + return "kStartDecodingLength"; + case kDecodingString: + return "kDecodingString"; + case kResumeDecodingLength: + return "kResumeDecodingLength"; + } + return Http2StrCat("UNKNOWN_STATE(", static_cast<uint32_t>(v), ")"); +} + +std::ostream& operator<<(std::ostream& out, const HpackStringDecoder& v) { + return out << v.DebugString(); +} + +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_string_decoder.h b/http2/hpack/decoder/hpack_string_decoder.h new file mode 100644 index 0000000..8ec0169 --- /dev/null +++ b/http2/hpack/decoder/hpack_string_decoder.h
@@ -0,0 +1,208 @@ +// Copyright 2016 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. + +#ifndef QUICHE_HTTP2_HPACK_DECODER_HPACK_STRING_DECODER_H_ +#define QUICHE_HTTP2_HPACK_DECODER_HPACK_STRING_DECODER_H_ + +// HpackStringDecoder decodes strings encoded per the HPACK spec; this does +// not mean decompressing Huffman encoded strings, just identifying the length, +// encoding and contents for a listener. + +#include <stddef.h> + +#include <algorithm> +#include <cstdint> + +#include "base/logging.h" +#include "base/macros.h" +#include "net/third_party/quiche/src/http2/decoder/decode_buffer.h" +#include "net/third_party/quiche/src/http2/decoder/decode_status.h" +#include "net/third_party/quiche/src/http2/hpack/varint/hpack_varint_decoder.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_export.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_macros.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string.h" + +namespace http2 { + +// Decodes a single string in an HPACK header entry. The high order bit of +// the first byte of the length is the H (Huffman) bit indicating whether +// the value is Huffman encoded, and the remainder of the byte is the first +// 7 bits of an HPACK varint. +// +// Call Start() to begin decoding; if it returns kDecodeInProgress, then call +// Resume() when more input is available, repeating until kDecodeInProgress is +// not returned. If kDecodeDone or kDecodeError is returned, then Resume() must +// not be called until Start() has been called to start decoding a new string. +class HTTP2_EXPORT_PRIVATE HpackStringDecoder { + public: + enum StringDecoderState { + kStartDecodingLength, + kDecodingString, + kResumeDecodingLength, + }; + + template <class Listener> + DecodeStatus Start(DecodeBuffer* db, Listener* cb) { + // Fast decode path is used if the string is under 127 bytes and the + // entire length of the string is in the decode buffer. More than 83% of + // string lengths are encoded in just one byte. + if (db->HasData() && (*db->cursor() & 0x7f) != 0x7f) { + // The string is short. + uint8_t h_and_prefix = db->DecodeUInt8(); + uint8_t length = h_and_prefix & 0x7f; + bool huffman_encoded = (h_and_prefix & 0x80) == 0x80; + cb->OnStringStart(huffman_encoded, length); + if (length <= db->Remaining()) { + // Yeah, we've got the whole thing in the decode buffer. + // Ideally this will be the common case. Note that we don't + // update any of the member variables in this path. + cb->OnStringData(db->cursor(), length); + db->AdvanceCursor(length); + cb->OnStringEnd(); + return DecodeStatus::kDecodeDone; + } + // Not all in the buffer. + huffman_encoded_ = huffman_encoded; + remaining_ = length; + // Call Resume to decode the string body, which is only partially + // in the decode buffer (or not at all). + state_ = kDecodingString; + return Resume(db, cb); + } + // Call Resume to decode the string length, which is either not in + // the decode buffer, or spans multiple bytes. + state_ = kStartDecodingLength; + return Resume(db, cb); + } + + template <class Listener> + DecodeStatus Resume(DecodeBuffer* db, Listener* cb) { + DecodeStatus status; + while (true) { + switch (state_) { + case kStartDecodingLength: + DVLOG(2) << "kStartDecodingLength: db->Remaining=" << db->Remaining(); + if (!StartDecodingLength(db, cb, &status)) { + // The length is split across decode buffers. + return status; + } + // We've finished decoding the length, which spanned one or more + // bytes. Approximately 17% of strings have a length that is greater + // than 126 bytes, and thus the length is encoded in more than one + // byte, and so doesn't get the benefit of the optimization in + // Start() for single byte lengths. But, we still expect that most + // of such strings will be contained entirely in a single decode + // buffer, and hence this fall through skips another trip through the + // switch above and more importantly skips setting the state_ variable + // again in those cases where we don't need it. + HTTP2_FALLTHROUGH; + + case kDecodingString: + DVLOG(2) << "kDecodingString: db->Remaining=" << db->Remaining() + << " remaining_=" << remaining_; + return DecodeString(db, cb); + + case kResumeDecodingLength: + DVLOG(2) << "kResumeDecodingLength: db->Remaining=" + << db->Remaining(); + if (!ResumeDecodingLength(db, cb, &status)) { + return status; + } + } + } + } + + Http2String DebugString() const; + + private: + static Http2String StateToString(StringDecoderState v); + + // Returns true if the length is fully decoded and the listener wants the + // decoding to continue, false otherwise; status is set to the status from + // the varint decoder. + // If the length is not fully decoded, case state_ is set appropriately + // for the next call to Resume. + template <class Listener> + bool StartDecodingLength(DecodeBuffer* db, + Listener* cb, + DecodeStatus* status) { + if (db->Empty()) { + *status = DecodeStatus::kDecodeInProgress; + state_ = kStartDecodingLength; + return false; + } + uint8_t h_and_prefix = db->DecodeUInt8(); + huffman_encoded_ = (h_and_prefix & 0x80) == 0x80; + *status = length_decoder_.Start(h_and_prefix, 7, db); + if (*status == DecodeStatus::kDecodeDone) { + OnStringStart(cb, status); + return true; + } + // Set the state to cover the DecodeStatus::kDecodeInProgress case. + // Won't be needed if the status is kDecodeError. + state_ = kResumeDecodingLength; + return false; + } + + // Returns true if the length is fully decoded and the listener wants the + // decoding to continue, false otherwise; status is set to the status from + // the varint decoder; state_ is updated when fully decoded. + // If the length is not fully decoded, case state_ is set appropriately + // for the next call to Resume. + template <class Listener> + bool ResumeDecodingLength(DecodeBuffer* db, + Listener* cb, + DecodeStatus* status) { + DCHECK_EQ(state_, kResumeDecodingLength); + *status = length_decoder_.Resume(db); + if (*status == DecodeStatus::kDecodeDone) { + state_ = kDecodingString; + OnStringStart(cb, status); + return true; + } + return false; + } + + // Returns true if the listener wants the decoding to continue, and + // false otherwise, in which case status set. + template <class Listener> + void OnStringStart(Listener* cb, DecodeStatus* status) { + remaining_ = length_decoder_.value(); + // Make callback so consumer knows what is coming. + cb->OnStringStart(huffman_encoded_, remaining_); + } + + // Passes the available portion of the string to the listener, and signals + // the end of the string when it is reached. Returns kDecodeDone or + // kDecodeInProgress as appropriate. + template <class Listener> + DecodeStatus DecodeString(DecodeBuffer* db, Listener* cb) { + size_t len = std::min(remaining_, db->Remaining()); + if (len > 0) { + cb->OnStringData(db->cursor(), len); + db->AdvanceCursor(len); + remaining_ -= len; + } + if (remaining_ == 0) { + cb->OnStringEnd(); + return DecodeStatus::kDecodeDone; + } + state_ = kDecodingString; + return DecodeStatus::kDecodeInProgress; + } + + HpackVarintDecoder length_decoder_; + + // These fields are initialized just to keep ASAN happy about reading + // them from DebugString(). + size_t remaining_ = 0; + StringDecoderState state_ = kStartDecodingLength; + bool huffman_encoded_ = false; +}; + +HTTP2_EXPORT_PRIVATE std::ostream& operator<<(std::ostream& out, + const HpackStringDecoder& v); + +} // namespace http2 +#endif // QUICHE_HTTP2_HPACK_DECODER_HPACK_STRING_DECODER_H_
diff --git a/http2/hpack/decoder/hpack_string_decoder_listener.cc b/http2/hpack/decoder/hpack_string_decoder_listener.cc new file mode 100644 index 0000000..e0fbc65 --- /dev/null +++ b/http2/hpack/decoder/hpack_string_decoder_listener.cc
@@ -0,0 +1,36 @@ +// Copyright 2016 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_string_decoder_listener.h" + +#include "base/logging.h" + +namespace http2 { +namespace test { + +void HpackStringDecoderVLoggingListener::OnStringStart(bool huffman_encoded, + size_t len) { + VLOG(1) << "OnStringStart: H=" << huffman_encoded << ", len=" << len; + if (wrapped_) { + wrapped_->OnStringStart(huffman_encoded, len); + } +} + +void HpackStringDecoderVLoggingListener::OnStringData(const char* data, + size_t len) { + VLOG(1) << "OnStringData: len=" << len; + if (wrapped_) { + return wrapped_->OnStringData(data, len); + } +} + +void HpackStringDecoderVLoggingListener::OnStringEnd() { + VLOG(1) << "OnStringEnd"; + if (wrapped_) { + return wrapped_->OnStringEnd(); + } +} + +} // namespace test +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_string_decoder_listener.h b/http2/hpack/decoder/hpack_string_decoder_listener.h new file mode 100644 index 0000000..35a0417 --- /dev/null +++ b/http2/hpack/decoder/hpack_string_decoder_listener.h
@@ -0,0 +1,62 @@ +// Copyright 2016 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. + +#ifndef QUICHE_HTTP2_HPACK_DECODER_HPACK_STRING_DECODER_LISTENER_H_ +#define QUICHE_HTTP2_HPACK_DECODER_HPACK_STRING_DECODER_LISTENER_H_ + +// Defines HpackStringDecoderListener which defines the methods required by an +// HpackStringDecoder. Also defines HpackStringDecoderVLoggingListener which +// logs before calling another HpackStringDecoderListener implementation. +// For now these are only used by tests, so placed in the test namespace. + +#include <stddef.h> + +#include "net/third_party/quiche/src/http2/platform/api/http2_export.h" + +namespace http2 { +namespace test { + +// HpackStringDecoder methods require a listener that implements the methods +// below, but it is NOT necessary to extend this class because the methods +// are templates. +class HTTP2_EXPORT_PRIVATE HpackStringDecoderListener { + public: + virtual ~HpackStringDecoderListener() {} + + // Called at the start of decoding an HPACK string. The encoded length of the + // string is |len| bytes, which may be zero. The string is Huffman encoded + // if huffman_encoded is true, else it is plain text (i.e. the encoded length + // is then the plain text length). + virtual void OnStringStart(bool huffman_encoded, size_t len) = 0; + + // Called when some data is available, or once when the string length is zero + // (to simplify the decoder, it doesn't have a special case for len==0). + virtual void OnStringData(const char* data, size_t len) = 0; + + // Called after OnStringData has provided all of the encoded bytes of the + // string. + virtual void OnStringEnd() = 0; +}; + +class HTTP2_EXPORT_PRIVATE HpackStringDecoderVLoggingListener + : public HpackStringDecoderListener { + public: + HpackStringDecoderVLoggingListener() : wrapped_(nullptr) {} + explicit HpackStringDecoderVLoggingListener( + HpackStringDecoderListener* wrapped) + : wrapped_(wrapped) {} + ~HpackStringDecoderVLoggingListener() override {} + + void OnStringStart(bool huffman_encoded, size_t len) override; + void OnStringData(const char* data, size_t len) override; + void OnStringEnd() override; + + private: + HpackStringDecoderListener* const wrapped_; +}; + +} // namespace test +} // namespace http2 + +#endif // QUICHE_HTTP2_HPACK_DECODER_HPACK_STRING_DECODER_LISTENER_H_
diff --git a/http2/hpack/decoder/hpack_string_decoder_test.cc b/http2/hpack/decoder/hpack_string_decoder_test.cc new file mode 100644 index 0000000..349b649 --- /dev/null +++ b/http2/hpack/decoder/hpack_string_decoder_test.cc
@@ -0,0 +1,155 @@ +// Copyright 2016 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_string_decoder.h" + +// Tests of HpackStringDecoder. + +#include "testing/gtest/include/gtest/gtest.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_string_collector.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_string_decoder_listener.h" +#include "net/third_party/quiche/src/http2/hpack/tools/hpack_block_builder.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string_piece.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_test_helpers.h" +#include "net/third_party/quiche/src/http2/test_tools/http2_random.h" +#include "net/third_party/quiche/src/http2/tools/random_decoder_test.h" + +using ::testing::AssertionResult; + +namespace http2 { +namespace test { +namespace { + +const bool kMayReturnZeroOnFirst = false; +const bool kCompressed = true; +const bool kUncompressed = false; + +class HpackStringDecoderTest : public RandomDecoderTest { + protected: + HpackStringDecoderTest() : listener_(&collector_) {} + + DecodeStatus StartDecoding(DecodeBuffer* b) override { + ++start_decoding_calls_; + collector_.Clear(); + return decoder_.Start(b, &listener_); + } + + DecodeStatus ResumeDecoding(DecodeBuffer* b) override { + // Provides coverage of DebugString and StateToString. + // Not validating output. + VLOG(1) << decoder_.DebugString(); + VLOG(2) << collector_; + return decoder_.Resume(b, &listener_); + } + + AssertionResult Collected(Http2StringPiece s, bool huffman_encoded) { + VLOG(1) << collector_; + return collector_.Collected(s, huffman_encoded); + } + + // expected_str is a Http2String rather than a const Http2String& or + // Http2StringPiece so that the lambda makes a copy of the string, and thus + // the string to be passed to Collected outlives the call to MakeValidator. + Validator MakeValidator(const Http2String& expected_str, + bool expected_huffman) { + return + [expected_str, expected_huffman, this]( + const DecodeBuffer& input, DecodeStatus status) -> AssertionResult { + AssertionResult result = Collected(expected_str, expected_huffman); + if (result) { + VERIFY_EQ(collector_, + HpackStringCollector(expected_str, expected_huffman)); + } else { + VERIFY_NE(collector_, + HpackStringCollector(expected_str, expected_huffman)); + } + VLOG(2) << collector_.ToString(); + collector_.Clear(); + VLOG(2) << collector_; + return result; + }; + } + + HpackStringDecoder decoder_; + HpackStringCollector collector_; + HpackStringDecoderVLoggingListener listener_; + size_t start_decoding_calls_ = 0; +}; + +TEST_F(HpackStringDecoderTest, DecodeEmptyString) { + { + Validator validator = ValidateDoneAndEmpty(MakeValidator("", kCompressed)); + const char kData[] = {'\x80'}; + DecodeBuffer b(kData); + EXPECT_TRUE( + DecodeAndValidateSeveralWays(&b, kMayReturnZeroOnFirst, validator)); + } + { + // Make sure it stops after decoding the empty string. + Validator validator = + ValidateDoneAndOffset(1, MakeValidator("", kUncompressed)); + const char kData[] = {'\x00', '\xff'}; + DecodeBuffer b(kData); + EXPECT_EQ(2u, b.Remaining()); + EXPECT_TRUE( + DecodeAndValidateSeveralWays(&b, kMayReturnZeroOnFirst, validator)); + EXPECT_EQ(1u, b.Remaining()); + } +} + +TEST_F(HpackStringDecoderTest, DecodeShortString) { + { + // Make sure it stops after decoding the non-empty string. + Validator validator = + ValidateDoneAndOffset(11, MakeValidator("start end.", kCompressed)); + const char kData[] = "\x8astart end.Don't peek at this."; + DecodeBuffer b(kData); + EXPECT_TRUE( + DecodeAndValidateSeveralWays(&b, kMayReturnZeroOnFirst, validator)); + } + { + Validator validator = + ValidateDoneAndOffset(11, MakeValidator("start end.", kUncompressed)); + Http2StringPiece data("\x0astart end."); + DecodeBuffer b(data); + EXPECT_TRUE( + DecodeAndValidateSeveralWays(&b, kMayReturnZeroOnFirst, validator)); + } +} + +TEST_F(HpackStringDecoderTest, DecodeLongStrings) { + Http2String name = Random().RandString(1024); + Http2String value = Random().RandString(65536); + HpackBlockBuilder hbb; + + hbb.AppendString(false, name); + uint32_t offset_after_name = hbb.size(); + EXPECT_EQ(3 + name.size(), offset_after_name); + + hbb.AppendString(true, value); + uint32_t offset_after_value = hbb.size(); + EXPECT_EQ(3 + name.size() + 4 + value.size(), offset_after_value); + + DecodeBuffer b(hbb.buffer()); + + // Decode the name... + EXPECT_TRUE(DecodeAndValidateSeveralWays( + &b, kMayReturnZeroOnFirst, + ValidateDoneAndOffset(offset_after_name, + MakeValidator(name, kUncompressed)))); + EXPECT_EQ(offset_after_name, b.Offset()); + EXPECT_EQ(offset_after_value - offset_after_name, b.Remaining()); + + // Decode the value... + EXPECT_TRUE(DecodeAndValidateSeveralWays( + &b, kMayReturnZeroOnFirst, + ValidateDoneAndOffset(offset_after_value - offset_after_name, + MakeValidator(value, kCompressed)))); + EXPECT_EQ(offset_after_value, b.Offset()); + EXPECT_EQ(0u, b.Remaining()); +} + +} // namespace +} // namespace test +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_whole_entry_buffer.cc b/http2/hpack/decoder/hpack_whole_entry_buffer.cc new file mode 100644 index 0000000..b3e29b1 --- /dev/null +++ b/http2/hpack/decoder/hpack_whole_entry_buffer.cc
@@ -0,0 +1,139 @@ +// Copyright 2016 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_whole_entry_buffer.h" + +#include "base/logging.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_estimate_memory_usage.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_macros.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string_utils.h" + +namespace http2 { + +HpackWholeEntryBuffer::HpackWholeEntryBuffer(HpackWholeEntryListener* listener, + size_t max_string_size_bytes) + : max_string_size_bytes_(max_string_size_bytes) { + set_listener(listener); +} +HpackWholeEntryBuffer::~HpackWholeEntryBuffer() = default; + +void HpackWholeEntryBuffer::set_listener(HpackWholeEntryListener* listener) { + listener_ = HTTP2_DIE_IF_NULL(listener); +} + +void HpackWholeEntryBuffer::set_max_string_size_bytes( + size_t max_string_size_bytes) { + max_string_size_bytes_ = max_string_size_bytes; +} + +void HpackWholeEntryBuffer::BufferStringsIfUnbuffered() { + name_.BufferStringIfUnbuffered(); + value_.BufferStringIfUnbuffered(); +} + +size_t HpackWholeEntryBuffer::EstimateMemoryUsage() const { + return Http2EstimateMemoryUsage(name_) + Http2EstimateMemoryUsage(value_); +} + +void HpackWholeEntryBuffer::OnIndexedHeader(size_t index) { + DVLOG(2) << "HpackWholeEntryBuffer::OnIndexedHeader: index=" << index; + listener_->OnIndexedHeader(index); +} + +void HpackWholeEntryBuffer::OnStartLiteralHeader(HpackEntryType entry_type, + size_t maybe_name_index) { + DVLOG(2) << "HpackWholeEntryBuffer::OnStartLiteralHeader: entry_type=" + << entry_type << ", maybe_name_index=" << maybe_name_index; + entry_type_ = entry_type; + maybe_name_index_ = maybe_name_index; +} + +void HpackWholeEntryBuffer::OnNameStart(bool huffman_encoded, size_t len) { + DVLOG(2) << "HpackWholeEntryBuffer::OnNameStart: huffman_encoded=" + << (huffman_encoded ? "true" : "false") << ", len=" << len; + DCHECK_EQ(maybe_name_index_, 0u); + if (!error_detected_) { + if (len > max_string_size_bytes_) { + DVLOG(1) << "Name length (" << len << ") is longer than permitted (" + << max_string_size_bytes_ << ")"; + ReportError("HPACK entry name size is too long."); + return; + } + name_.OnStart(huffman_encoded, len); + } +} + +void HpackWholeEntryBuffer::OnNameData(const char* data, size_t len) { + DVLOG(2) << "HpackWholeEntryBuffer::OnNameData: len=" << len << " data:\n" + << Http2HexDump(Http2StringPiece(data, len)); + DCHECK_EQ(maybe_name_index_, 0u); + if (!error_detected_ && !name_.OnData(data, len)) { + ReportError("Error decoding HPACK entry name."); + } +} + +void HpackWholeEntryBuffer::OnNameEnd() { + DVLOG(2) << "HpackWholeEntryBuffer::OnNameEnd"; + DCHECK_EQ(maybe_name_index_, 0u); + if (!error_detected_ && !name_.OnEnd()) { + ReportError("Error decoding HPACK entry name."); + } +} + +void HpackWholeEntryBuffer::OnValueStart(bool huffman_encoded, size_t len) { + DVLOG(2) << "HpackWholeEntryBuffer::OnValueStart: huffman_encoded=" + << (huffman_encoded ? "true" : "false") << ", len=" << len; + if (!error_detected_) { + if (len > max_string_size_bytes_) { + DVLOG(1) << "Value length (" << len << ") is longer than permitted (" + << max_string_size_bytes_ << ")"; + ReportError("HPACK entry value size is too long."); + return; + } + value_.OnStart(huffman_encoded, len); + } +} + +void HpackWholeEntryBuffer::OnValueData(const char* data, size_t len) { + DVLOG(2) << "HpackWholeEntryBuffer::OnValueData: len=" << len << " data:\n" + << Http2HexDump(Http2StringPiece(data, len)); + if (!error_detected_ && !value_.OnData(data, len)) { + ReportError("Error decoding HPACK entry value."); + } +} + +void HpackWholeEntryBuffer::OnValueEnd() { + DVLOG(2) << "HpackWholeEntryBuffer::OnValueEnd"; + if (error_detected_) { + return; + } + if (!value_.OnEnd()) { + ReportError("Error decoding HPACK entry value."); + return; + } + if (maybe_name_index_ == 0) { + listener_->OnLiteralNameAndValue(entry_type_, &name_, &value_); + name_.Reset(); + } else { + listener_->OnNameIndexAndLiteralValue(entry_type_, maybe_name_index_, + &value_); + } + value_.Reset(); +} + +void HpackWholeEntryBuffer::OnDynamicTableSizeUpdate(size_t size) { + DVLOG(2) << "HpackWholeEntryBuffer::OnDynamicTableSizeUpdate: size=" << size; + listener_->OnDynamicTableSizeUpdate(size); +} + +void HpackWholeEntryBuffer::ReportError(Http2StringPiece error_message) { + if (!error_detected_) { + DVLOG(1) << "HpackWholeEntryBuffer::ReportError: " << error_message; + error_detected_ = true; + listener_->OnHpackDecodeError(error_message); + listener_ = HpackWholeEntryNoOpListener::NoOpListener(); + } +} + +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_whole_entry_buffer.h b/http2/hpack/decoder/hpack_whole_entry_buffer.h new file mode 100644 index 0000000..61bf583 --- /dev/null +++ b/http2/hpack/decoder/hpack_whole_entry_buffer.h
@@ -0,0 +1,104 @@ +// Copyright 2016 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. + +#ifndef QUICHE_HTTP2_HPACK_DECODER_HPACK_WHOLE_ENTRY_BUFFER_H_ +#define QUICHE_HTTP2_HPACK_DECODER_HPACK_WHOLE_ENTRY_BUFFER_H_ + +// HpackWholeEntryBuffer isolates a listener from the fact that an entry may +// be split across multiple input buffers, providing one callback per entry. +// HpackWholeEntryBuffer requires that the HpackEntryDecoderListener be made in +// the correct order, which is tested by hpack_entry_decoder_test.cc. + +#include <stddef.h> + +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_decoder_string_buffer.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_entry_decoder_listener.h" +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_whole_entry_listener.h" +#include "net/third_party/quiche/src/http2/hpack/http2_hpack_constants.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_export.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string_piece.h" + +namespace http2 { + +// TODO(jamessynge): Consider renaming HpackEntryDecoderListener to +// HpackEntryPartsListener or HpackEntryFragmentsListener. +class HTTP2_EXPORT_PRIVATE HpackWholeEntryBuffer + : public HpackEntryDecoderListener { + public: + // max_string_size specifies the maximum size of an on-the-wire string (name + // or value, plain or Huffman encoded) that will be accepted. See sections + // 5.1 and 5.2 of RFC 7541. This is a defense against OOM attacks; HTTP/2 + // allows a decoder to enforce any limit of the size of the header lists + // that it is willing decode, including less than the MAX_HEADER_LIST_SIZE + // setting, a setting that is initially unlimited. For example, we might + // choose to send a MAX_HEADER_LIST_SIZE of 64KB, and to use that same value + // as the upper bound for individual strings. + HpackWholeEntryBuffer(HpackWholeEntryListener* listener, + size_t max_string_size); + ~HpackWholeEntryBuffer() override; + + HpackWholeEntryBuffer(const HpackWholeEntryBuffer&) = delete; + HpackWholeEntryBuffer& operator=(const HpackWholeEntryBuffer&) = delete; + + // Set the listener to be notified when a whole entry has been decoded. + // The listener may be changed at any time. + void set_listener(HpackWholeEntryListener* listener); + + // Set how much encoded data this decoder is willing to buffer. + // TODO(jamessynge): Come up with consistent semantics for this protection + // across the various decoders; e.g. should it be for a single string or + // a single header entry? + void set_max_string_size_bytes(size_t max_string_size_bytes); + + // Ensure that decoded strings pointed to by the HpackDecoderStringBuffer + // instances name_ and value_ are buffered, which allows any underlying + // transport buffer to be freed or reused without overwriting the decoded + // strings. This is needed only when an HPACK entry is split across transport + // buffers. See HpackDecoder::DecodeFragment. + void BufferStringsIfUnbuffered(); + + // Was an error detected? After an error has been detected and reported, + // no further callbacks will be made to the listener. + bool error_detected() const { return error_detected_; } + + // Returns the estimate of dynamically allocated memory in bytes. + size_t EstimateMemoryUsage() const; + + // Implement the HpackEntryDecoderListener methods. + + void OnIndexedHeader(size_t index) override; + void OnStartLiteralHeader(HpackEntryType entry_type, + size_t maybe_name_index) override; + void OnNameStart(bool huffman_encoded, size_t len) override; + void OnNameData(const char* data, size_t len) override; + void OnNameEnd() override; + void OnValueStart(bool huffman_encoded, size_t len) override; + void OnValueData(const char* data, size_t len) override; + void OnValueEnd() override; + void OnDynamicTableSizeUpdate(size_t size) override; + + private: + void ReportError(Http2StringPiece error_message); + + HpackWholeEntryListener* listener_; + HpackDecoderStringBuffer name_, value_; + + // max_string_size_bytes_ specifies the maximum allowed size of an on-the-wire + // string. Larger strings will be reported as errors to the listener; the + // endpoint should treat these as COMPRESSION errors, which are CONNECTION + // level errors. + size_t max_string_size_bytes_; + + // The name index (or zero) of the current header entry with a literal value. + size_t maybe_name_index_; + + // The type of the current header entry (with literals) that is being decoded. + HpackEntryType entry_type_; + + bool error_detected_ = false; +}; + +} // namespace http2 + +#endif // QUICHE_HTTP2_HPACK_DECODER_HPACK_WHOLE_ENTRY_BUFFER_H_
diff --git a/http2/hpack/decoder/hpack_whole_entry_buffer_test.cc b/http2/hpack/decoder/hpack_whole_entry_buffer_test.cc new file mode 100644 index 0000000..75b281c --- /dev/null +++ b/http2/hpack/decoder/hpack_whole_entry_buffer_test.cc
@@ -0,0 +1,206 @@ +// Copyright 2016 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_whole_entry_buffer.h" + +// Tests of HpackWholeEntryBuffer: does it buffer correctly, and does it +// detect Huffman decoding errors and oversize string errors? + +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ::testing::AllOf; +using ::testing::HasSubstr; +using ::testing::InSequence; +using ::testing::Property; +using ::testing::StrictMock; + +namespace http2 { +namespace test { +namespace { + +constexpr size_t kMaxStringSize = 20; + +class MockHpackWholeEntryListener : public HpackWholeEntryListener { + public: + ~MockHpackWholeEntryListener() override = default; + + MOCK_METHOD1(OnIndexedHeader, void(size_t index)); + MOCK_METHOD3(OnNameIndexAndLiteralValue, + void(HpackEntryType entry_type, + size_t name_index, + HpackDecoderStringBuffer* value_buffer)); + MOCK_METHOD3(OnLiteralNameAndValue, + void(HpackEntryType entry_type, + HpackDecoderStringBuffer* name_buffer, + HpackDecoderStringBuffer* value_buffer)); + MOCK_METHOD1(OnDynamicTableSizeUpdate, void(size_t size)); + MOCK_METHOD1(OnHpackDecodeError, void(Http2StringPiece error_message)); +}; + +class HpackWholeEntryBufferTest : public ::testing::Test { + protected: + HpackWholeEntryBufferTest() : entry_buffer_(&listener_, kMaxStringSize) {} + ~HpackWholeEntryBufferTest() override = default; + + StrictMock<MockHpackWholeEntryListener> listener_; + HpackWholeEntryBuffer entry_buffer_; +}; + +// OnIndexedHeader is an immediate pass through. +TEST_F(HpackWholeEntryBufferTest, OnIndexedHeader) { + { + InSequence seq; + EXPECT_CALL(listener_, OnIndexedHeader(17)); + entry_buffer_.OnIndexedHeader(17); + } + { + InSequence seq; + EXPECT_CALL(listener_, OnIndexedHeader(62)); + entry_buffer_.OnIndexedHeader(62); + } + { + InSequence seq; + EXPECT_CALL(listener_, OnIndexedHeader(62)); + entry_buffer_.OnIndexedHeader(62); + } + { + InSequence seq; + EXPECT_CALL(listener_, OnIndexedHeader(128)); + entry_buffer_.OnIndexedHeader(128); + } + StrictMock<MockHpackWholeEntryListener> listener2; + entry_buffer_.set_listener(&listener2); + { + InSequence seq; + EXPECT_CALL(listener2, OnIndexedHeader(100)); + entry_buffer_.OnIndexedHeader(100); + } +} + +// OnDynamicTableSizeUpdate is an immediate pass through. +TEST_F(HpackWholeEntryBufferTest, OnDynamicTableSizeUpdate) { + { + InSequence seq; + EXPECT_CALL(listener_, OnDynamicTableSizeUpdate(4096)); + entry_buffer_.OnDynamicTableSizeUpdate(4096); + } + { + InSequence seq; + EXPECT_CALL(listener_, OnDynamicTableSizeUpdate(0)); + entry_buffer_.OnDynamicTableSizeUpdate(0); + } + { + InSequence seq; + EXPECT_CALL(listener_, OnDynamicTableSizeUpdate(1024)); + entry_buffer_.OnDynamicTableSizeUpdate(1024); + } + { + InSequence seq; + EXPECT_CALL(listener_, OnDynamicTableSizeUpdate(1024)); + entry_buffer_.OnDynamicTableSizeUpdate(1024); + } + StrictMock<MockHpackWholeEntryListener> listener2; + entry_buffer_.set_listener(&listener2); + { + InSequence seq; + EXPECT_CALL(listener2, OnDynamicTableSizeUpdate(0)); + entry_buffer_.OnDynamicTableSizeUpdate(0); + } +} + +TEST_F(HpackWholeEntryBufferTest, OnNameIndexAndLiteralValue) { + entry_buffer_.OnStartLiteralHeader(HpackEntryType::kNeverIndexedLiteralHeader, + 123); + entry_buffer_.OnValueStart(false, 10); + entry_buffer_.OnValueData("some data.", 10); + + // Force the value to be buffered. + entry_buffer_.BufferStringsIfUnbuffered(); + + EXPECT_CALL( + listener_, + OnNameIndexAndLiteralValue( + HpackEntryType::kNeverIndexedLiteralHeader, 123, + AllOf(Property(&HpackDecoderStringBuffer::str, "some data."), + Property(&HpackDecoderStringBuffer::BufferedLength, 10)))); + + entry_buffer_.OnValueEnd(); +} + +TEST_F(HpackWholeEntryBufferTest, OnLiteralNameAndValue) { + entry_buffer_.OnStartLiteralHeader(HpackEntryType::kIndexedLiteralHeader, 0); + // Force the name to be buffered by delivering it in two pieces. + entry_buffer_.OnNameStart(false, 9); + entry_buffer_.OnNameData("some-", 5); + entry_buffer_.OnNameData("name", 4); + entry_buffer_.OnNameEnd(); + entry_buffer_.OnValueStart(false, 12); + entry_buffer_.OnValueData("Header Value", 12); + + EXPECT_CALL( + listener_, + OnLiteralNameAndValue( + HpackEntryType::kIndexedLiteralHeader, + AllOf(Property(&HpackDecoderStringBuffer::str, "some-name"), + Property(&HpackDecoderStringBuffer::BufferedLength, 9)), + AllOf(Property(&HpackDecoderStringBuffer::str, "Header Value"), + Property(&HpackDecoderStringBuffer::BufferedLength, 0)))); + + entry_buffer_.OnValueEnd(); +} + +// Verify that a name longer than the allowed size generates an error. +TEST_F(HpackWholeEntryBufferTest, NameTooLong) { + entry_buffer_.OnStartLiteralHeader(HpackEntryType::kIndexedLiteralHeader, 0); + EXPECT_CALL(listener_, OnHpackDecodeError(HasSubstr("HPACK entry name"))); + entry_buffer_.OnNameStart(false, kMaxStringSize + 1); +} + +// Verify that a name longer than the allowed size generates an error. +TEST_F(HpackWholeEntryBufferTest, ValueTooLong) { + entry_buffer_.OnStartLiteralHeader(HpackEntryType::kIndexedLiteralHeader, 1); + EXPECT_CALL(listener_, OnHpackDecodeError(HasSubstr("HPACK entry value"))); + entry_buffer_.OnValueStart(false, kMaxStringSize + 1); +} + +// Verify that a Huffman encoded name with an explicit EOS generates an error +// for an explicit EOS. +TEST_F(HpackWholeEntryBufferTest, NameHuffmanError) { + const char data[] = "\xff\xff\xff"; + entry_buffer_.OnStartLiteralHeader(HpackEntryType::kUnindexedLiteralHeader, + 0); + entry_buffer_.OnNameStart(true, 4); + entry_buffer_.OnNameData(data, 3); + + EXPECT_CALL(listener_, OnHpackDecodeError(HasSubstr("HPACK entry name"))); + + entry_buffer_.OnNameData(data, 1); + + // After an error is reported, the listener is not called again. + EXPECT_CALL(listener_, OnDynamicTableSizeUpdate(8096)).Times(0); + entry_buffer_.OnDynamicTableSizeUpdate(8096); +} + +// Verify that a Huffman encoded value that isn't properly terminated with +// a partial EOS symbol generates an error. +TEST_F(HpackWholeEntryBufferTest, ValueeHuffmanError) { + const char data[] = "\x00\x00\x00"; + entry_buffer_.OnStartLiteralHeader(HpackEntryType::kNeverIndexedLiteralHeader, + 61); + entry_buffer_.OnValueStart(true, 3); + entry_buffer_.OnValueData(data, 3); + + EXPECT_CALL(listener_, OnHpackDecodeError(HasSubstr("HPACK entry value"))); + + entry_buffer_.OnValueEnd(); + + // After an error is reported, the listener is not called again. + EXPECT_CALL(listener_, OnIndexedHeader(17)).Times(0); + entry_buffer_.OnIndexedHeader(17); +} + +} // namespace +} // namespace test +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_whole_entry_listener.cc b/http2/hpack/decoder/hpack_whole_entry_listener.cc new file mode 100644 index 0000000..b92e64a --- /dev/null +++ b/http2/hpack/decoder/hpack_whole_entry_listener.cc
@@ -0,0 +1,33 @@ +// Copyright 2016 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 "net/third_party/quiche/src/http2/hpack/decoder/hpack_whole_entry_listener.h" + +namespace http2 { + +HpackWholeEntryListener::~HpackWholeEntryListener() = default; + +HpackWholeEntryNoOpListener::~HpackWholeEntryNoOpListener() = default; + +void HpackWholeEntryNoOpListener::OnIndexedHeader(size_t index) {} +void HpackWholeEntryNoOpListener::OnNameIndexAndLiteralValue( + HpackEntryType entry_type, + size_t name_index, + HpackDecoderStringBuffer* value_buffer) {} +void HpackWholeEntryNoOpListener::OnLiteralNameAndValue( + HpackEntryType entry_type, + HpackDecoderStringBuffer* name_buffer, + HpackDecoderStringBuffer* value_buffer) {} +void HpackWholeEntryNoOpListener::OnDynamicTableSizeUpdate(size_t size) {} +void HpackWholeEntryNoOpListener::OnHpackDecodeError( + Http2StringPiece error_message) {} + +// static +HpackWholeEntryNoOpListener* HpackWholeEntryNoOpListener::NoOpListener() { + static HpackWholeEntryNoOpListener* static_instance = + new HpackWholeEntryNoOpListener(); + return static_instance; +} + +} // namespace http2
diff --git a/http2/hpack/decoder/hpack_whole_entry_listener.h b/http2/hpack/decoder/hpack_whole_entry_listener.h new file mode 100644 index 0000000..2e559ce --- /dev/null +++ b/http2/hpack/decoder/hpack_whole_entry_listener.h
@@ -0,0 +1,80 @@ +// Copyright 2016 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. + +// Defines HpackWholeEntryListener, the base class of listeners for decoded +// complete HPACK entries, as opposed to HpackEntryDecoderListener which +// receives multiple callbacks for some single entries. + +#ifndef QUICHE_HTTP2_HPACK_DECODER_HPACK_WHOLE_ENTRY_LISTENER_H_ +#define QUICHE_HTTP2_HPACK_DECODER_HPACK_WHOLE_ENTRY_LISTENER_H_ + +#include <stddef.h> + +#include "net/third_party/quiche/src/http2/hpack/decoder/hpack_decoder_string_buffer.h" +#include "net/third_party/quiche/src/http2/hpack/http2_hpack_constants.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_export.h" +#include "net/third_party/quiche/src/http2/platform/api/http2_string_piece.h" + +namespace http2 { + +class HTTP2_EXPORT_PRIVATE HpackWholeEntryListener { + public: + virtual ~HpackWholeEntryListener(); + + // Called when an indexed header (i.e. one in the static or dynamic table) has + // been decoded from an HPACK block. index is supposed to be non-zero, but + // that has not been checked by the caller. + virtual void OnIndexedHeader(size_t index) = 0; + + // Called when a header entry with a name index and literal value has + // been fully decoded from an HPACK block. name_index is NOT zero. + // entry_type will be kIndexedLiteralHeader, kUnindexedLiteralHeader, or + // kNeverIndexedLiteralHeader. + virtual void OnNameIndexAndLiteralValue( + HpackEntryType entry_type, + size_t name_index, + HpackDecoderStringBuffer* value_buffer) = 0; + + // Called when a header entry with a literal name and literal value + // has been fully decoded from an HPACK block. entry_type will be + // kIndexedLiteralHeader, kUnindexedLiteralHeader, or + // kNeverIndexedLiteralHeader. + virtual void OnLiteralNameAndValue( + HpackEntryType entry_type, + HpackDecoderStringBuffer* name_buffer, + HpackDecoderStringBuffer* value_buffer) = 0; + + // Called when an update to the size of the peer's dynamic table has been + // decoded. + virtual void OnDynamicTableSizeUpdate(size_t size) = 0; + + // OnHpackDecodeError is called if an error is detected while decoding. + // error_message may be used in a GOAWAY frame as the Opaque Data. + virtual void OnHpackDecodeError(Http2StringPiece error_message) = 0; +}; + +// A no-op implementation of HpackWholeEntryDecoderListener, useful for ignoring +// callbacks once an error is detected. +class HpackWholeEntryNoOpListener : public HpackWholeEntryListener { + public: + ~HpackWholeEntryNoOpListener() override; + + void OnIndexedHeader(size_t index) override; + void OnNameIndexAndLiteralValue( + HpackEntryType entry_type, + size_t name_index, + HpackDecoderStringBuffer* value_buffer) override; + void OnLiteralNameAndValue(HpackEntryType entry_type, + HpackDecoderStringBuffer* name_buffer, + HpackDecoderStringBuffer* value_buffer) override; + void OnDynamicTableSizeUpdate(size_t size) override; + void OnHpackDecodeError(Http2StringPiece error_message) override; + + // Returns a listener that ignores all the calls. + static HpackWholeEntryNoOpListener* NoOpListener(); +}; + +} // namespace http2 + +#endif // QUICHE_HTTP2_HPACK_DECODER_HPACK_WHOLE_ENTRY_LISTENER_H_