QUICHE team | a6ef0a6 | 2019-03-07 20:34:33 -0500 | [diff] [blame] | 1 | // Copyright (c) 2018 The Chromium Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | #include "net/third_party/quiche/src/quic/core/qpack/offline/qpack_offline_decoder.h" |
| 6 | |
| 7 | #include <cstdint> |
vasilvv | 872e7a3 | 2019-03-12 16:42:44 -0700 | [diff] [blame] | 8 | #include <string> |
QUICHE team | a6ef0a6 | 2019-03-07 20:34:33 -0500 | [diff] [blame] | 9 | #include <utility> |
| 10 | |
| 11 | #include "net/third_party/quiche/src/quic/core/qpack/qpack_test_utils.h" |
| 12 | #include "net/third_party/quiche/src/quic/core/quic_types.h" |
| 13 | #include "net/third_party/quiche/src/quic/platform/api/quic_endian.h" |
| 14 | #include "net/third_party/quiche/src/quic/platform/api/quic_file_utils.h" |
| 15 | #include "net/third_party/quiche/src/quic/platform/api/quic_logging.h" |
bnc | 13750a8 | 2019-07-17 17:02:53 -0700 | [diff] [blame] | 16 | #include "net/third_party/quiche/src/quic/platform/api/quic_ptr_util.h" |
QUICHE team | a6ef0a6 | 2019-03-07 20:34:33 -0500 | [diff] [blame] | 17 | #include "net/third_party/quiche/src/quic/platform/api/quic_text_utils.h" |
| 18 | |
| 19 | namespace quic { |
| 20 | |
| 21 | QpackOfflineDecoder::QpackOfflineDecoder() |
| 22 | : encoder_stream_error_detected_(false), |
bnc | 13750a8 | 2019-07-17 17:02:53 -0700 | [diff] [blame] | 23 | max_blocked_streams_(0) {} |
QUICHE team | a6ef0a6 | 2019-03-07 20:34:33 -0500 | [diff] [blame] | 24 | |
| 25 | bool QpackOfflineDecoder::DecodeAndVerifyOfflineData( |
| 26 | QuicStringPiece input_filename, |
| 27 | QuicStringPiece expected_headers_filename) { |
| 28 | if (!ParseInputFilename(input_filename)) { |
| 29 | QUIC_LOG(ERROR) << "Error parsing input filename " << input_filename; |
| 30 | return false; |
| 31 | } |
| 32 | |
| 33 | if (!DecodeHeaderBlocksFromFile(input_filename)) { |
| 34 | QUIC_LOG(ERROR) << "Error decoding header blocks in " << input_filename; |
| 35 | return false; |
| 36 | } |
| 37 | |
| 38 | if (!VerifyDecodedHeaderLists(expected_headers_filename)) { |
| 39 | QUIC_LOG(ERROR) << "Header lists decoded from " << input_filename |
| 40 | << " to not match expected headers parsed from " |
| 41 | << expected_headers_filename; |
| 42 | return false; |
| 43 | } |
| 44 | |
| 45 | return true; |
| 46 | } |
| 47 | |
| 48 | void QpackOfflineDecoder::OnEncoderStreamError(QuicStringPiece error_message) { |
| 49 | QUIC_LOG(ERROR) << "Encoder stream error: " << error_message; |
| 50 | encoder_stream_error_detected_ = true; |
| 51 | } |
| 52 | |
| 53 | bool QpackOfflineDecoder::ParseInputFilename(QuicStringPiece input_filename) { |
| 54 | auto pieces = QuicTextUtils::Split(input_filename, '.'); |
| 55 | |
| 56 | if (pieces.size() < 3) { |
| 57 | QUIC_LOG(ERROR) << "Not enough fields in input filename " << input_filename; |
| 58 | return false; |
| 59 | } |
| 60 | |
| 61 | auto piece_it = pieces.rbegin(); |
| 62 | |
| 63 | // Acknowledgement mode: 1 for immediate, 0 for none. |
| 64 | bool immediate_acknowledgement = false; |
| 65 | if (*piece_it == "0") { |
| 66 | immediate_acknowledgement = false; |
| 67 | } else if (*piece_it == "1") { |
| 68 | immediate_acknowledgement = true; |
| 69 | } else { |
| 70 | QUIC_LOG(ERROR) |
| 71 | << "Header acknowledgement field must be 0 or 1 in input filename " |
| 72 | << input_filename; |
| 73 | return false; |
| 74 | } |
| 75 | |
| 76 | ++piece_it; |
| 77 | |
| 78 | // Maximum allowed number of blocked streams. |
bnc | 13750a8 | 2019-07-17 17:02:53 -0700 | [diff] [blame] | 79 | if (!QuicTextUtils::StringToUint64(*piece_it, &max_blocked_streams_)) { |
QUICHE team | a6ef0a6 | 2019-03-07 20:34:33 -0500 | [diff] [blame] | 80 | QUIC_LOG(ERROR) << "Error parsing part of input filename \"" << *piece_it |
| 81 | << "\" as an integer."; |
| 82 | return false; |
| 83 | } |
| 84 | |
QUICHE team | a6ef0a6 | 2019-03-07 20:34:33 -0500 | [diff] [blame] | 85 | ++piece_it; |
| 86 | |
bnc | 13750a8 | 2019-07-17 17:02:53 -0700 | [diff] [blame] | 87 | // Maximum Dynamic Table Capacity in bytes |
| 88 | uint64_t maximum_dynamic_table_capacity = 0; |
| 89 | if (!QuicTextUtils::StringToUint64(*piece_it, |
| 90 | &maximum_dynamic_table_capacity)) { |
QUICHE team | a6ef0a6 | 2019-03-07 20:34:33 -0500 | [diff] [blame] | 91 | QUIC_LOG(ERROR) << "Error parsing part of input filename \"" << *piece_it |
| 92 | << "\" as an integer."; |
| 93 | return false; |
| 94 | } |
bnc | 4c664c5 | 2019-08-04 18:14:12 -0700 | [diff] [blame] | 95 | qpack_decoder_ = QuicMakeUnique<QpackDecoder>( |
| 96 | maximum_dynamic_table_capacity, max_blocked_streams_, this, |
| 97 | &decoder_stream_sender_delegate_); |
QUICHE team | a6ef0a6 | 2019-03-07 20:34:33 -0500 | [diff] [blame] | 98 | |
| 99 | return true; |
| 100 | } |
| 101 | |
| 102 | bool QpackOfflineDecoder::DecodeHeaderBlocksFromFile( |
| 103 | QuicStringPiece input_filename) { |
| 104 | // Store data in |input_data_storage|; use a QuicStringPiece to efficiently |
| 105 | // keep track of remaining portion yet to be decoded. |
vasilvv | c48c871 | 2019-03-11 13:38:16 -0700 | [diff] [blame] | 106 | std::string input_data_storage; |
QUICHE team | a6ef0a6 | 2019-03-07 20:34:33 -0500 | [diff] [blame] | 107 | ReadFileContents(input_filename, &input_data_storage); |
| 108 | QuicStringPiece input_data(input_data_storage); |
| 109 | |
| 110 | while (!input_data.empty()) { |
bnc | 13750a8 | 2019-07-17 17:02:53 -0700 | [diff] [blame] | 111 | // Parse stream_id and length. |
QUICHE team | a6ef0a6 | 2019-03-07 20:34:33 -0500 | [diff] [blame] | 112 | if (input_data.size() < sizeof(uint64_t) + sizeof(uint32_t)) { |
| 113 | QUIC_LOG(ERROR) << "Unexpected end of input file."; |
| 114 | return false; |
| 115 | } |
| 116 | |
| 117 | uint64_t stream_id = QuicEndian::NetToHost64( |
| 118 | *reinterpret_cast<const uint64_t*>(input_data.data())); |
| 119 | input_data = input_data.substr(sizeof(uint64_t)); |
| 120 | |
| 121 | uint32_t length = QuicEndian::NetToHost32( |
| 122 | *reinterpret_cast<const uint32_t*>(input_data.data())); |
| 123 | input_data = input_data.substr(sizeof(uint32_t)); |
| 124 | |
| 125 | if (input_data.size() < length) { |
| 126 | QUIC_LOG(ERROR) << "Unexpected end of input file."; |
| 127 | return false; |
| 128 | } |
| 129 | |
bnc | 13750a8 | 2019-07-17 17:02:53 -0700 | [diff] [blame] | 130 | // Parse data. |
QUICHE team | a6ef0a6 | 2019-03-07 20:34:33 -0500 | [diff] [blame] | 131 | QuicStringPiece data = input_data.substr(0, length); |
| 132 | input_data = input_data.substr(length); |
| 133 | |
bnc | 13750a8 | 2019-07-17 17:02:53 -0700 | [diff] [blame] | 134 | // Process data. |
QUICHE team | a6ef0a6 | 2019-03-07 20:34:33 -0500 | [diff] [blame] | 135 | if (stream_id == 0) { |
bnc | 4c664c5 | 2019-08-04 18:14:12 -0700 | [diff] [blame] | 136 | qpack_decoder_->DecodeEncoderStreamData(data); |
QUICHE team | a6ef0a6 | 2019-03-07 20:34:33 -0500 | [diff] [blame] | 137 | |
| 138 | if (encoder_stream_error_detected_) { |
| 139 | QUIC_LOG(ERROR) << "Error detected on encoder stream."; |
| 140 | return false; |
| 141 | } |
bnc | 13750a8 | 2019-07-17 17:02:53 -0700 | [diff] [blame] | 142 | } else { |
| 143 | auto headers_handler = QuicMakeUnique<test::TestHeadersHandler>(); |
bnc | 4c664c5 | 2019-08-04 18:14:12 -0700 | [diff] [blame] | 144 | auto progressive_decoder = qpack_decoder_->CreateProgressiveDecoder( |
bnc | 13750a8 | 2019-07-17 17:02:53 -0700 | [diff] [blame] | 145 | stream_id, headers_handler.get()); |
QUICHE team | a6ef0a6 | 2019-03-07 20:34:33 -0500 | [diff] [blame] | 146 | |
bnc | 13750a8 | 2019-07-17 17:02:53 -0700 | [diff] [blame] | 147 | progressive_decoder->Decode(data); |
| 148 | progressive_decoder->EndHeaderBlock(); |
| 149 | |
| 150 | if (headers_handler->decoding_error_detected()) { |
| 151 | QUIC_LOG(ERROR) << "Sync decoding error on stream " << stream_id << ": " |
| 152 | << headers_handler->error_message(); |
| 153 | return false; |
| 154 | } |
| 155 | |
| 156 | decoders_.push_back({std::move(headers_handler), |
| 157 | std::move(progressive_decoder), stream_id}); |
QUICHE team | a6ef0a6 | 2019-03-07 20:34:33 -0500 | [diff] [blame] | 158 | } |
| 159 | |
bnc | 13750a8 | 2019-07-17 17:02:53 -0700 | [diff] [blame] | 160 | // Move decoded header lists from TestHeadersHandlers and append them to |
| 161 | // |decoded_header_lists_| while preserving the order in |decoders_|. |
| 162 | while (!decoders_.empty() && |
| 163 | decoders_.front().headers_handler->decoding_completed()) { |
| 164 | Decoder* decoder = &decoders_.front(); |
QUICHE team | a6ef0a6 | 2019-03-07 20:34:33 -0500 | [diff] [blame] | 165 | |
bnc | 13750a8 | 2019-07-17 17:02:53 -0700 | [diff] [blame] | 166 | if (decoder->headers_handler->decoding_error_detected()) { |
| 167 | QUIC_LOG(ERROR) << "Async decoding error on stream " |
| 168 | << decoder->stream_id << ": " |
| 169 | << decoder->headers_handler->error_message(); |
| 170 | return false; |
| 171 | } |
QUICHE team | a6ef0a6 | 2019-03-07 20:34:33 -0500 | [diff] [blame] | 172 | |
bnc | 13750a8 | 2019-07-17 17:02:53 -0700 | [diff] [blame] | 173 | if (!decoder->headers_handler->decoding_completed()) { |
| 174 | QUIC_LOG(ERROR) << "Decoding incomplete after reading entire" |
| 175 | " file, on stream " |
| 176 | << decoder->stream_id; |
| 177 | return false; |
| 178 | } |
| 179 | |
| 180 | decoded_header_lists_.push_back( |
| 181 | decoder->headers_handler->ReleaseHeaderList()); |
| 182 | decoders_.pop_front(); |
| 183 | } |
| 184 | |
| 185 | // Enforce limit on blocked streams. |
bnc | 4c664c5 | 2019-08-04 18:14:12 -0700 | [diff] [blame] | 186 | // TODO(b/112770235): Move this logic to QpackDecoder. |
bnc | 13750a8 | 2019-07-17 17:02:53 -0700 | [diff] [blame] | 187 | uint64_t blocked_streams_count = 0; |
| 188 | for (const auto& decoder : decoders_) { |
| 189 | if (!decoder.headers_handler->decoding_completed()) { |
| 190 | ++blocked_streams_count; |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | if (blocked_streams_count > max_blocked_streams_) { |
| 195 | QUIC_LOG(ERROR) << "Too many blocked streams: limit is " |
| 196 | << max_blocked_streams_ << ", actual count is " |
| 197 | << blocked_streams_count; |
QUICHE team | a6ef0a6 | 2019-03-07 20:34:33 -0500 | [diff] [blame] | 198 | return false; |
| 199 | } |
bnc | 13750a8 | 2019-07-17 17:02:53 -0700 | [diff] [blame] | 200 | } |
QUICHE team | a6ef0a6 | 2019-03-07 20:34:33 -0500 | [diff] [blame] | 201 | |
bnc | 13750a8 | 2019-07-17 17:02:53 -0700 | [diff] [blame] | 202 | if (!decoders_.empty()) { |
| 203 | DCHECK(!decoders_.front().headers_handler->decoding_completed()); |
| 204 | |
| 205 | QUIC_LOG(ERROR) << "Blocked decoding uncomplete after reading entire" |
| 206 | " file, on stream " |
| 207 | << decoders_.front().stream_id; |
| 208 | return false; |
QUICHE team | a6ef0a6 | 2019-03-07 20:34:33 -0500 | [diff] [blame] | 209 | } |
| 210 | |
| 211 | return true; |
| 212 | } |
| 213 | |
| 214 | bool QpackOfflineDecoder::VerifyDecodedHeaderLists( |
| 215 | QuicStringPiece expected_headers_filename) { |
| 216 | // Store data in |expected_headers_data_storage|; use a QuicStringPiece to |
| 217 | // efficiently keep track of remaining portion yet to be decoded. |
vasilvv | c48c871 | 2019-03-11 13:38:16 -0700 | [diff] [blame] | 218 | std::string expected_headers_data_storage; |
QUICHE team | a6ef0a6 | 2019-03-07 20:34:33 -0500 | [diff] [blame] | 219 | ReadFileContents(expected_headers_filename, &expected_headers_data_storage); |
| 220 | QuicStringPiece expected_headers_data(expected_headers_data_storage); |
| 221 | |
| 222 | while (!decoded_header_lists_.empty()) { |
| 223 | spdy::SpdyHeaderBlock decoded_header_list = |
| 224 | std::move(decoded_header_lists_.front()); |
| 225 | decoded_header_lists_.pop_front(); |
| 226 | |
| 227 | spdy::SpdyHeaderBlock expected_header_list; |
| 228 | if (!ReadNextExpectedHeaderList(&expected_headers_data, |
| 229 | &expected_header_list)) { |
| 230 | QUIC_LOG(ERROR) |
| 231 | << "Error parsing expected header list to match next decoded " |
| 232 | "header list."; |
| 233 | return false; |
| 234 | } |
| 235 | |
| 236 | if (!CompareHeaderBlocks(std::move(decoded_header_list), |
| 237 | std::move(expected_header_list))) { |
| 238 | QUIC_LOG(ERROR) << "Decoded header does not match expected header."; |
| 239 | return false; |
| 240 | } |
| 241 | } |
| 242 | |
| 243 | if (!expected_headers_data.empty()) { |
| 244 | QUIC_LOG(ERROR) |
| 245 | << "Not enough encoded header lists to match expected ones."; |
| 246 | return false; |
| 247 | } |
| 248 | |
| 249 | return true; |
| 250 | } |
| 251 | |
| 252 | bool QpackOfflineDecoder::ReadNextExpectedHeaderList( |
| 253 | QuicStringPiece* expected_headers_data, |
| 254 | spdy::SpdyHeaderBlock* expected_header_list) { |
| 255 | while (true) { |
| 256 | QuicStringPiece::size_type endline = expected_headers_data->find('\n'); |
| 257 | |
| 258 | // Even last header list must be followed by an empty line. |
| 259 | if (endline == QuicStringPiece::npos) { |
| 260 | QUIC_LOG(ERROR) << "Unexpected end of expected header list file."; |
| 261 | return false; |
| 262 | } |
| 263 | |
| 264 | if (endline == 0) { |
| 265 | // Empty line indicates end of header list. |
| 266 | *expected_headers_data = expected_headers_data->substr(1); |
| 267 | return true; |
| 268 | } |
| 269 | |
| 270 | QuicStringPiece header_field = expected_headers_data->substr(0, endline); |
| 271 | auto pieces = QuicTextUtils::Split(header_field, '\t'); |
| 272 | |
| 273 | if (pieces.size() != 2) { |
| 274 | QUIC_LOG(ERROR) << "Header key and value must be separated by TAB."; |
| 275 | return false; |
| 276 | } |
| 277 | |
| 278 | expected_header_list->AppendValueOrAddHeader(pieces[0], pieces[1]); |
| 279 | |
| 280 | *expected_headers_data = expected_headers_data->substr(endline + 1); |
| 281 | } |
| 282 | } |
| 283 | |
| 284 | bool QpackOfflineDecoder::CompareHeaderBlocks( |
| 285 | spdy::SpdyHeaderBlock decoded_header_list, |
| 286 | spdy::SpdyHeaderBlock expected_header_list) { |
| 287 | if (decoded_header_list == expected_header_list) { |
| 288 | return true; |
| 289 | } |
| 290 | |
| 291 | // The h2o decoder reshuffles the "content-length" header and pseudo-headers, |
| 292 | // see |
| 293 | // https://github.com/qpackers/qifs/blob/master/encoded/qpack-03/h2o/README.md. |
| 294 | // Remove such headers one by one if they match. |
| 295 | const char* kContentLength = "content-length"; |
| 296 | const char* kPseudoHeaderPrefix = ":"; |
| 297 | for (spdy::SpdyHeaderBlock::iterator decoded_it = decoded_header_list.begin(); |
| 298 | decoded_it != decoded_header_list.end();) { |
| 299 | const QuicStringPiece key = decoded_it->first; |
| 300 | if (key != kContentLength && |
| 301 | !QuicTextUtils::StartsWith(key, kPseudoHeaderPrefix)) { |
| 302 | ++decoded_it; |
| 303 | continue; |
| 304 | } |
| 305 | spdy::SpdyHeaderBlock::iterator expected_it = |
| 306 | expected_header_list.find(key); |
| 307 | if (expected_it == expected_header_list.end() || |
| 308 | decoded_it->second != expected_it->second) { |
| 309 | ++decoded_it; |
| 310 | continue; |
| 311 | } |
| 312 | // SpdyHeaderBlock does not support erasing by iterator, only by key. |
| 313 | ++decoded_it; |
| 314 | expected_header_list.erase(key); |
| 315 | // This will invalidate |key|. |
| 316 | decoded_header_list.erase(key); |
| 317 | } |
| 318 | |
| 319 | return decoded_header_list == expected_header_list; |
| 320 | } |
| 321 | |
| 322 | } // namespace quic |