// Copyright 2014 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/spdy/hpack_decoder.h" #include #include #include "base/basictypes.h" #include "base/logging.h" #include "base/strings/string_piece.h" #include "net/spdy/hpack_encoder.h" #include "net/spdy/hpack_input_stream.h" #include "net/spdy/hpack_output_stream.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" namespace net { namespace test { using base::StringPiece; using std::string; class HpackEncodingContextPeer { public: explicit HpackEncodingContextPeer(const HpackEncodingContext& context) : context_(context) {} const HpackHeaderTable& header_table() { return context_.header_table_; } private: const HpackEncodingContext& context_; }; class HpackDecoderPeer { public: explicit HpackDecoderPeer(HpackDecoder* decoder) : decoder_(decoder) {} void HandleHeaderRepresentation(StringPiece name, StringPiece value) { decoder_->HandleHeaderRepresentation(name, value); } bool DecodeNextName(HpackInputStream* in, StringPiece* out) { return decoder_->DecodeNextName(in, out); } HpackEncodingContextPeer context_peer() { return HpackEncodingContextPeer(decoder_->context_); } void set_cookie_name(string name) { decoder_->cookie_name_ = name; } string cookie_name() { return decoder_->cookie_name_; } void set_cookie_value(string value) { decoder_->cookie_value_ = value; } string cookie_value() { return decoder_->cookie_value_; } const std::map& decoded_block() const { return decoder_->decoded_block_; } const string& headers_block_buffer() const { return decoder_->headers_block_buffer_; } private: HpackDecoder* decoder_; }; } // namespace test namespace { using base::StringPiece; using std::string; using testing::ElementsAre; using testing::Pair; const size_t kLiteralBound = 1024; class HpackDecoderTest : public ::testing::Test { protected: HpackDecoderTest() : decoder_(ObtainHpackHuffmanTable()), decoder_peer_(&decoder_), context_peer_(decoder_peer_.context_peer()) {} bool DecodeHeaderBlock(StringPiece str) { return decoder_.HandleControlFrameHeadersData(0, str.data(), str.size()) && decoder_.HandleControlFrameHeadersComplete(0); } const std::map& decoded_block() const { // TODO(jgraettinger): HpackDecoderTest should implement // SpdyHeadersHandlerInterface, and collect headers for examination. return decoder_peer_.decoded_block(); } // TODO(jgraettinger): Eliminate uses of this in tests below. Prefer // DecodeHeaderBlock(). const std::map& DecodeUniqueHeaderSet(StringPiece str) { EXPECT_TRUE(DecodeHeaderBlock(str)); return decoded_block(); } HpackDecoder decoder_; test::HpackDecoderPeer decoder_peer_; test::HpackEncodingContextPeer context_peer_; }; TEST_F(HpackDecoderTest, HandleControlFrameHeadersData) { // Strings under threshold are concatenated in the buffer. EXPECT_TRUE(decoder_.HandleControlFrameHeadersData( 0, "small string one", 16)); EXPECT_TRUE(decoder_.HandleControlFrameHeadersData( 0, "small string two", 16)); // A string which would push the buffer over the threshold is refused. EXPECT_FALSE(decoder_.HandleControlFrameHeadersData( 0, "fails", kMaxDecodeBufferSize - 32 + 1)); EXPECT_EQ(decoder_peer_.headers_block_buffer(), "small string onesmall string two"); } TEST_F(HpackDecoderTest, HandleControlFrameHeadersComplete) { // Decode a block which toggles two static headers into the reference set. EXPECT_TRUE(DecodeHeaderBlock("\x82\x86")); decoder_peer_.set_cookie_name("CooKie"); decoder_peer_.set_cookie_value("foobar=baz"); // Headers in the reference set should be emitted. // Incremental cookie buffer should be emitted and cleared. decoder_.HandleControlFrameHeadersData(0, NULL, 0); decoder_.HandleControlFrameHeadersComplete(0); EXPECT_THAT(decoded_block(), ElementsAre( Pair(":method", "GET"), Pair(":path", "/index.html"), Pair("CooKie", "foobar=baz"))); EXPECT_EQ(decoder_peer_.cookie_name(), ""); EXPECT_EQ(decoder_peer_.cookie_value(), ""); } TEST_F(HpackDecoderTest, HandleHeaderRepresentation) { // Casing of first Cookie is retained, but all instances are joined. decoder_peer_.HandleHeaderRepresentation("cOOkie", " part 1"); decoder_peer_.HandleHeaderRepresentation("cookie", "part 2 "); decoder_peer_.HandleHeaderRepresentation("cookie", "part3"); // Already-delimited headers are passed through. decoder_peer_.HandleHeaderRepresentation("passed-through", string("foo\0baz", 7)); // Other headers are joined on \0. Case matters. decoder_peer_.HandleHeaderRepresentation("joined", "not joined"); decoder_peer_.HandleHeaderRepresentation("joineD", "value 1"); decoder_peer_.HandleHeaderRepresentation("joineD", "value 2"); // Empty headers remain empty. decoder_peer_.HandleHeaderRepresentation("empty", ""); // Joined empty headers work as expected. decoder_peer_.HandleHeaderRepresentation("empty-joined", ""); decoder_peer_.HandleHeaderRepresentation("empty-joined", "foo"); decoder_peer_.HandleHeaderRepresentation("empty-joined", ""); decoder_peer_.HandleHeaderRepresentation("empty-joined", ""); // Non-contiguous cookie crumb. decoder_peer_.HandleHeaderRepresentation("Cookie", " fin!"); // Finish and emit all headers. decoder_.HandleControlFrameHeadersComplete(0); EXPECT_THAT(decoded_block(), ElementsAre( Pair("cOOkie", " part 1; part 2 ; part3; fin!"), Pair("empty", ""), Pair("empty-joined", string("\0foo\0\0", 6)), Pair("joineD", string("value 1\0value 2", 15)), Pair("joined", "not joined"), Pair("passed-through", string("foo\0baz", 7)))); } // Decoding an encoded name with a valid string literal should work. TEST_F(HpackDecoderTest, DecodeNextNameLiteral) { HpackInputStream input_stream(kLiteralBound, StringPiece("\x00\x04name", 6)); StringPiece string_piece; EXPECT_TRUE(decoder_peer_.DecodeNextName(&input_stream, &string_piece)); EXPECT_EQ("name", string_piece); EXPECT_FALSE(input_stream.HasMoreData()); } TEST_F(HpackDecoderTest, DecodeNextNameLiteralWithHuffmanEncoding) { char input[] = "\x00\x88\x4e\xb0\x8b\x74\x97\x90\xfa\x7f"; StringPiece foo(input, arraysize(input) - 1); HpackInputStream input_stream(kLiteralBound, foo); StringPiece string_piece; EXPECT_TRUE(decoder_peer_.DecodeNextName(&input_stream, &string_piece)); EXPECT_EQ("custom-key", string_piece); EXPECT_FALSE(input_stream.HasMoreData()); } // Decoding an encoded name with a valid index should work. TEST_F(HpackDecoderTest, DecodeNextNameIndexed) { HpackInputStream input_stream(kLiteralBound, "\x01"); StringPiece string_piece; EXPECT_TRUE(decoder_peer_.DecodeNextName(&input_stream, &string_piece)); EXPECT_EQ(":authority", string_piece); EXPECT_FALSE(input_stream.HasMoreData()); } // Decoding an encoded name with an invalid index should fail. TEST_F(HpackDecoderTest, DecodeNextNameInvalidIndex) { // One more than the number of static table entries. HpackInputStream input_stream(kLiteralBound, "\x3d"); StringPiece string_piece; EXPECT_FALSE(decoder_peer_.DecodeNextName(&input_stream, &string_piece)); } // Decoding an indexed header should toggle the index's presence in // the reference set, making a copy of static table entries if // necessary. It should also emit the header if toggled on (and only // as many times as it was toggled on). TEST_F(HpackDecoderTest, IndexedHeaderBasic) { // Toggle on static table entry #2 (and make a copy at index #1), // then toggle on static table entry #5 (which is now #6 because of // the copy of #2). std::map header_set1 = DecodeUniqueHeaderSet("\x82\x86"); std::map expected_header_set1; expected_header_set1[":method"] = "GET"; expected_header_set1[":path"] = "/index.html"; EXPECT_EQ(expected_header_set1, header_set1); std::map expected_header_set2; expected_header_set2[":path"] = "/index.html"; // Toggle off the copy of static table entry #5. std::map header_set2 = DecodeUniqueHeaderSet("\x82"); EXPECT_EQ(expected_header_set2, header_set2); } // Test a too-large indexed header. TEST_F(HpackDecoderTest, InvalidIndexedHeader) { // High-bit set, and a prefix of one more than the number of static entries. EXPECT_FALSE(DecodeHeaderBlock(StringPiece("\xbd", 1))); } TEST_F(HpackDecoderTest, ContextUpdateMaximumSize) { EXPECT_EQ(kDefaultHeaderTableSizeSetting, context_peer_.header_table().max_size()); { // Maximum-size update with size 126. Succeeds. EXPECT_TRUE(DecodeHeaderBlock(StringPiece("\x80\x7e", 2))); EXPECT_EQ(126u, context_peer_.header_table().max_size()); } string input; { // Maximum-size update with kDefaultHeaderTableSizeSetting. Succeeds. HpackOutputStream output_stream(kuint32max); output_stream.AppendBits(0x80, 8); // Context update. output_stream.AppendBits(0x00, 1); // Size update. output_stream.AppendUint32ForTest(kDefaultHeaderTableSizeSetting); output_stream.TakeString(&input); EXPECT_TRUE(DecodeHeaderBlock(StringPiece(input))); EXPECT_EQ(kDefaultHeaderTableSizeSetting, context_peer_.header_table().max_size()); } { // Maximum-size update with kDefaultHeaderTableSizeSetting + 1. Fails. HpackOutputStream output_stream(kuint32max); output_stream.AppendBits(0x80, 8); // Context update. output_stream.AppendBits(0x00, 1); // Size update. output_stream.AppendUint32ForTest(kDefaultHeaderTableSizeSetting + 1); output_stream.TakeString(&input); EXPECT_FALSE(DecodeHeaderBlock(StringPiece(input))); EXPECT_EQ(kDefaultHeaderTableSizeSetting, context_peer_.header_table().max_size()); } } TEST_F(HpackDecoderTest, ContextUpdateClearReferenceSet) { // Toggle on a couple of headers. std::map header_set1 = DecodeUniqueHeaderSet("\x82\x86"); std::map expected_header_set1; expected_header_set1[":method"] = "GET"; expected_header_set1[":path"] = "/index.html"; EXPECT_EQ(expected_header_set1, header_set1); // Send a context update to clear the reference set. std::map header_set2 = DecodeUniqueHeaderSet("\x80\x80"); std::map expected_header_set2; EXPECT_EQ(expected_header_set2, header_set2); } // Decoding two valid encoded literal headers with no indexing should // work. TEST_F(HpackDecoderTest, LiteralHeaderNoIndexing) { // First header with indexed name, second header with string literal // name. std::map header_set = DecodeUniqueHeaderSet( "\x44\x0c/sample/path\x40\x06:path2\x0e/sample/path/2"); std::map expected_header_set; expected_header_set[":path"] = "/sample/path"; expected_header_set[":path2"] = "/sample/path/2"; EXPECT_EQ(expected_header_set, header_set); } // Decoding two valid encoded literal headers with incremental // indexing and string literal names should work and add the headers // to the reference set. TEST_F(HpackDecoderTest, LiteralHeaderIncrementalIndexing) { std::map header_set = DecodeUniqueHeaderSet( StringPiece("\x04\x0c/sample/path\x00\x06:path2\x0e/sample/path/2", 37)); std::map expected_header_set; expected_header_set[":path"] = "/sample/path"; expected_header_set[":path2"] = "/sample/path/2"; EXPECT_EQ(expected_header_set, header_set); // Decoding an empty string should just return the reference set. std::map header_set2 = DecodeUniqueHeaderSet(""); EXPECT_EQ(expected_header_set, header_set2); } // Decoding literal headers with invalid indices should fail // gracefully. TEST_F(HpackDecoderTest, LiteralHeaderInvalidIndices) { // No indexing. // One more than the number of static table entries. EXPECT_FALSE(DecodeHeaderBlock(StringPiece("\x7d", 1))); EXPECT_FALSE(DecodeHeaderBlock(StringPiece("\x40", 1))); // Incremental indexing. // One more than the number of static table entries. EXPECT_FALSE(DecodeHeaderBlock(StringPiece("\x3d", 1))); EXPECT_FALSE(DecodeHeaderBlock(StringPiece("\x00", 1))); } // Round-tripping the header set from E.2.1 should work. TEST_F(HpackDecoderTest, BasicE21) { HpackEncoder encoder; std::map expected_header_set; expected_header_set[":method"] = "GET"; expected_header_set[":scheme"] = "http"; expected_header_set[":path"] = "/"; expected_header_set[":authority"] = "www.example.com"; string encoded_header_set; EXPECT_TRUE(encoder.EncodeHeaderSet( expected_header_set, &encoded_header_set)); EXPECT_TRUE(DecodeHeaderBlock(encoded_header_set)); EXPECT_EQ(expected_header_set, decoded_block()); } TEST_F(HpackDecoderTest, SectionD3RequestHuffmanExamples) { std::map header_set; // 82 | == Indexed - Add == // | idx = 2 // | -> :method: GET // 87 | == Indexed - Add == // | idx = 7 // | -> :scheme: http // 86 | == Indexed - Add == // | idx = 6 // | -> :path: / // 04 | == Literal indexed == // | Indexed name (idx = 4) // | :authority // 8b | Literal value (len = 15) // | Huffman encoded: // db6d 883e 68d1 cb12 25ba 7f | .m..h...%.. // | Decoded: // | www.example.com // | -> :authority: www.example.com char first[] = "\x82\x87\x86\x04\x8b\xdb\x6d\x88\x3e\x68\xd1\xcb\x12\x25\xba\x7f"; header_set = DecodeUniqueHeaderSet(StringPiece(first, arraysize(first)-1)); // TODO(jgraettinger): Create HpackEncodingContext and // HpackDecoder peers, and inspect the header table here. EXPECT_THAT(header_set, ElementsAre( Pair(":authority", "www.example.com"), Pair(":method", "GET"), Pair(":path", "/"), Pair(":scheme", "http"))); // 1b | == Literal indexed == // | Indexed name (idx = 27) // | cache-control // 86 | Literal value (len = 8) // | Huffman encoded: // 6365 4a13 98ff | ceJ... // | Decoded: // | no-cache // | -> cache-control: no-cache char second[] = "\x1b\x86\x63\x65\x4a\x13\x98\xff"; header_set = DecodeUniqueHeaderSet(StringPiece(second, arraysize(second)-1)); EXPECT_THAT(header_set, ElementsAre( Pair(":authority", "www.example.com"), Pair(":method", "GET"), Pair(":path", "/"), Pair(":scheme", "http"), Pair("cache-control", "no-cache"))); // 8080 | == Empty reference set == // | idx = 0 // | flag = 1 // 85 | == Indexed - Add == // | idx = 5 // | -> :method: GET // 8c | == Indexed - Add == // | idx = 12 // | -> :scheme: https // 8b | == Indexed - Add == // | idx = 11 // | -> :path: /index.html // 84 | == Indexed - Add == // | idx = 4 // | -> :authority: www.example.com // 00 | == Literal indexed == // 88 | Literal name (len = 10) // | Huffman encoded: // 4eb0 8b74 9790 fa7f | N..t.... // | Decoded: // | custom-key // 89 | Literal value (len = 12) // | Huffman encoded: // 4eb0 8b74 979a 17a8 ff | N..t..... // | Decoded: // | custom-value // | -> custom-key: custom-value char third[] = "\x80\x80\x85\x8c\x8b\x84\x00\x88\x4e\xb0\x8b\x74\x97\x90\xfa\x7f\x89" "\x4e\xb0\x8b\x74\x97\x9a\x17\xa8\xff"; header_set = DecodeUniqueHeaderSet(StringPiece(third, arraysize(third)-1)); EXPECT_THAT(header_set, ElementsAre( Pair(":authority", "www.example.com"), Pair(":method", "GET"), Pair(":path", "/index.html"), Pair(":scheme", "https"), Pair("custom-key", "custom-value"))); } TEST_F(HpackDecoderTest, SectionD5ResponseHuffmanExamples) { std::map header_set; decoder_.ApplyHeaderTableSizeSetting(256); // 08 | == Literal indexed == // | Indexed name (idx = 8) // | :status // 82 | Literal value (len = 3) // | Huffman encoded: // 98a7 | .. // | Decoded: // | 302 // | -> :status: 302 // 18 | == Literal indexed == // | Indexed name (idx = 24) // | cache-control // 85 | Literal value (len = 7) // | Huffman encoded: // 73d5 cd11 1f | s.... // | Decoded: // | private // | -> cache-control: private // 22 | == Literal indexed == // | Indexed name (idx = 34) // | date // 98 | Literal value (len = 29) // | Huffman encoded: // ef6b 3a7a 0e6e 8fa2 63d0 729a 6e83 97d8 | .k:z.n..c.r.n... // 69bd 8737 47bb bfc7 | i..7G... // | Decoded: // | Mon, 21 Oct 2013 20:13:21 // | GMT // | -> date: Mon, 21 Oct 2013 // | 20:13:21 GMT // 30 | == Literal indexed == // | Indexed name (idx = 48) // | location // 90 | Literal value (len = 23) // | Huffman encoded: // ce31 743d 801b 6db1 07cd 1a39 6244 b74f | .1t=..m....9bD.O // | Decoded: // | https://www.example.com // | -> location: https://www.e // | xample.com char first[] = "\x08\x82\x98\xa7\x18\x85\x73\xd5\xcd\x11\x1f\x22\x98\xef\x6b" "\x3a\x7a\x0e\x6e\x8f\xa2\x63\xd0\x72\x9a\x6e\x83\x97\xd8\x69\xbd\x87" "\x37\x47\xbb\xbf\xc7\x30\x90\xce\x31\x74\x3d\x80\x1b\x6d\xb1\x07\xcd" "\x1a\x39\x62\x44\xb7\x4f"; header_set = DecodeUniqueHeaderSet(StringPiece(first, arraysize(first)-1)); EXPECT_THAT(header_set, ElementsAre( Pair(":status", "302"), Pair("cache-control", "private"), Pair("date", "Mon, 21 Oct 2013 20:13:21 GMT"), Pair("location", "https://www.example.com"))); // 8c | == Indexed - Add == // | idx = 12 // | - evict: :status: 302 // | -> :status: 200 char second[] = "\x8c"; header_set = DecodeUniqueHeaderSet(StringPiece(second, arraysize(second)-1)); EXPECT_THAT(header_set, ElementsAre( Pair(":status", "200"), Pair("cache-control", "private"), Pair("date", "Mon, 21 Oct 2013 20:13:21 GMT"), Pair("location", "https://www.example.com"))); // 84 | == Indexed - Remove == // | idx = 4 // | -> cache-control: private // 84 | == Indexed - Add == // | idx = 4 // | -> cache-control: private // 03 | == Literal indexed == // | Indexed name (idx = 3) // | date // 98 | Literal value (len = 29) // | Huffman encoded: // ef6b 3a7a 0e6e 8fa2 63d0 729a 6e83 97d8 | .k:z.n..c.r.n... // 69bd 873f 47bb bfc7 | i..?G... // | Decoded: // | Mon, 21 Oct 2013 20:13:22 // | GMT // | - evict: cache-control: pr // | ivate // | -> date: Mon, 21 Oct 2013 // | 20:13:22 GMT // 1d | == Literal indexed == // | Indexed name (idx = 29) // | content-encoding // 83 | Literal value (len = 4) // | Huffman encoded: // cbd5 4e | ..N // | Decoded: // | gzip // | - evict: date: Mon, 21 Oct // | 2013 20:13:21 GMT // | -> content-encoding: gzip // 84 | == Indexed - Remove == // | idx = 4 // | -> location: https://www.e // | xample.com // 84 | == Indexed - Add == // | idx = 4 // | -> location: https://www.e // | xample.com // 83 | == Indexed - Remove == // | idx = 3 // | -> :status: 200 // 83 | == Indexed - Add == // | idx = 3 // | -> :status: 200 // 3a | == Literal indexed == // | Indexed name (idx = 58) // | set-cookie // b3 | Literal value (len = 56) // | Huffman encoded: // c5ad b77f 876f c7fb f7fd bfbe bff3 f7f4 | .....o.......... // fb7e bbbe 9f5f 87e3 7fef edfa eefa 7c3f | ....._........|? // 1d5d 1a23 ce54 6436 cd49 4bd5 d1cc 5f05 | .].#.Td6.IK..._. // 3596 9b | 5.. // | Decoded: // | foo=ASDJKHQKBZXOQWEOPIUAXQ // | WEOIU; max-age=3600; versi // | on=1 // | - evict: location: https:/ // | /www.example.com // | - evict: :status: 200 // | -> set-cookie: foo=ASDJKHQ // | KBZXOQWEOPIUAXQWEOIU; ma // | x-age=3600; version=1 char third[] = "\x84\x84\x03\x98\xef\x6b\x3a\x7a\x0e\x6e\x8f\xa2\x63\xd0\x72" "\x9a\x6e\x83\x97\xd8\x69\xbd\x87\x3f\x47\xbb\xbf\xc7\x1d\x83\xcb\xd5" "\x4e\x84\x84\x83\x83\x3a\xb3\xc5\xad\xb7\x7f\x87\x6f\xc7\xfb\xf7\xfd" "\xbf\xbe\xbf\xf3\xf7\xf4\xfb\x7e\xbb\xbe\x9f\x5f\x87\xe3\x7f\xef\xed" "\xfa\xee\xfa\x7c\x3f\x1d\x5d\x1a\x23\xce\x54\x64\x36\xcd\x49\x4b\xd5" "\xd1\xcc\x5f\x05\x35\x96\x9b"; header_set = DecodeUniqueHeaderSet(StringPiece(third, arraysize(third)-1)); EXPECT_THAT(header_set, ElementsAre( Pair(":status", "200"), Pair("cache-control", "private"), Pair("content-encoding", "gzip"), Pair("date", "Mon, 21 Oct 2013 20:13:22 GMT"), Pair("location", "https://www.example.com"), Pair("set-cookie", "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU;" " max-age=3600; version=1"))); } } // namespace } // namespace net