| OLD | NEW |
| (Empty) |
| 1 // Copyright 2014 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/cert/crl_set_storage.h" | |
| 6 | |
| 7 #include "base/base64.h" | |
| 8 #include "base/format_macros.h" | |
| 9 #include "base/json/json_reader.h" | |
| 10 #include "base/numerics/safe_conversions.h" | |
| 11 #include "base/strings/stringprintf.h" | |
| 12 #include "base/trace_event/trace_event.h" | |
| 13 #include "base/values.h" | |
| 14 #include "crypto/sha2.h" | |
| 15 #include "third_party/zlib/zlib.h" | |
| 16 | |
| 17 namespace net { | |
| 18 | |
| 19 // Decompress zlib decompressed |in| into |out|. |out_len| is the number of | |
| 20 // bytes at |out| and must be exactly equal to the size of the decompressed | |
| 21 // data. | |
| 22 static bool DecompressZlib(uint8* out, int out_len, base::StringPiece in) { | |
| 23 z_stream z; | |
| 24 memset(&z, 0, sizeof(z)); | |
| 25 | |
| 26 z.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(in.data())); | |
| 27 z.avail_in = in.size(); | |
| 28 z.next_out = reinterpret_cast<Bytef*>(out); | |
| 29 z.avail_out = out_len; | |
| 30 | |
| 31 if (inflateInit(&z) != Z_OK) | |
| 32 return false; | |
| 33 bool ret = false; | |
| 34 int r = inflate(&z, Z_FINISH); | |
| 35 if (r != Z_STREAM_END) | |
| 36 goto err; | |
| 37 if (z.avail_in || z.avail_out) | |
| 38 goto err; | |
| 39 ret = true; | |
| 40 | |
| 41 err: | |
| 42 inflateEnd(&z); | |
| 43 return ret; | |
| 44 } | |
| 45 | |
| 46 // CRLSet format: | |
| 47 // | |
| 48 // uint16le header_len | |
| 49 // byte[header_len] header_bytes | |
| 50 // repeated { | |
| 51 // byte[32] parent_spki_sha256 | |
| 52 // uint32le num_serials | |
| 53 // [num_serials] { | |
| 54 // uint8 serial_length; | |
| 55 // byte[serial_length] serial; | |
| 56 // } | |
| 57 // | |
| 58 // header_bytes consists of a JSON dictionary with the following keys: | |
| 59 // Version (int): currently 0 | |
| 60 // ContentType (string): "CRLSet" or "CRLSetDelta" (magic value) | |
| 61 // DeltaFrom (int32): if this is a delta update (see below), then this | |
| 62 // contains the sequence number of the base CRLSet. | |
| 63 // Sequence (int32): the monotonic sequence number of this CRL set. | |
| 64 // | |
| 65 // A delta CRLSet is similar to a CRLSet: | |
| 66 // | |
| 67 // struct CompressedChanges { | |
| 68 // uint32le uncompressed_size | |
| 69 // uint32le compressed_size | |
| 70 // byte[compressed_size] zlib_data | |
| 71 // } | |
| 72 // | |
| 73 // uint16le header_len | |
| 74 // byte[header_len] header_bytes | |
| 75 // CompressedChanges crl_changes | |
| 76 // [crl_changes.uncompressed_size] { | |
| 77 // switch (crl_changes[i]) { | |
| 78 // case 0: | |
| 79 // // CRL is the same | |
| 80 // case 1: | |
| 81 // // New CRL inserted | |
| 82 // // See CRL structure from the non-delta format | |
| 83 // case 2: | |
| 84 // // CRL deleted | |
| 85 // case 3: | |
| 86 // // CRL changed | |
| 87 // CompressedChanges serials_changes | |
| 88 // [serials_changes.uncompressed_size] { | |
| 89 // switch (serials_changes[i]) { | |
| 90 // case 0: | |
| 91 // // the serial is the same | |
| 92 // case 1: | |
| 93 // // serial inserted | |
| 94 // uint8 serial_length | |
| 95 // byte[serial_length] serial | |
| 96 // case 2: | |
| 97 // // serial deleted | |
| 98 // } | |
| 99 // } | |
| 100 // } | |
| 101 // } | |
| 102 // | |
| 103 // A delta CRLSet applies to a specific CRL set as given in the | |
| 104 // header's "DeltaFrom" value. The delta describes the changes to each CRL | |
| 105 // in turn with a zlib compressed array of options: either the CRL is the same, | |
| 106 // a new CRL is inserted, the CRL is deleted or the CRL is updated. In the case | |
| 107 // of an update, the serials in the CRL are considered in the same fashion | |
| 108 // except there is no delta update of a serial number: they are either | |
| 109 // inserted, deleted or left the same. | |
| 110 | |
| 111 // ReadHeader reads the header (including length prefix) from |data| and | |
| 112 // updates |data| to remove the header on return. Caller takes ownership of the | |
| 113 // returned pointer. | |
| 114 static base::DictionaryValue* ReadHeader(base::StringPiece* data) { | |
| 115 if (data->size() < 2) | |
| 116 return NULL; | |
| 117 uint16 header_len; | |
| 118 memcpy(&header_len, data->data(), 2); // assumes little-endian. | |
| 119 data->remove_prefix(2); | |
| 120 | |
| 121 if (data->size() < header_len) | |
| 122 return NULL; | |
| 123 | |
| 124 const base::StringPiece header_bytes(data->data(), header_len); | |
| 125 data->remove_prefix(header_len); | |
| 126 | |
| 127 scoped_ptr<base::Value> header(base::JSONReader::Read( | |
| 128 header_bytes, base::JSON_ALLOW_TRAILING_COMMAS)); | |
| 129 if (header.get() == NULL) | |
| 130 return NULL; | |
| 131 | |
| 132 if (!header->IsType(base::Value::TYPE_DICTIONARY)) | |
| 133 return NULL; | |
| 134 return reinterpret_cast<base::DictionaryValue*>(header.release()); | |
| 135 } | |
| 136 | |
| 137 // kCurrentFileVersion is the version of the CRLSet file format that we | |
| 138 // currently implement. | |
| 139 static const int kCurrentFileVersion = 0; | |
| 140 | |
| 141 static bool ReadCRL(base::StringPiece* data, std::string* out_parent_spki_hash, | |
| 142 std::vector<std::string>* out_serials) { | |
| 143 if (data->size() < crypto::kSHA256Length) | |
| 144 return false; | |
| 145 out_parent_spki_hash->assign(data->data(), crypto::kSHA256Length); | |
| 146 data->remove_prefix(crypto::kSHA256Length); | |
| 147 | |
| 148 if (data->size() < sizeof(uint32)) | |
| 149 return false; | |
| 150 uint32 num_serials; | |
| 151 memcpy(&num_serials, data->data(), sizeof(uint32)); // assumes little endian | |
| 152 if (num_serials > 32 * 1024 * 1024) // Sanity check. | |
| 153 return false; | |
| 154 | |
| 155 out_serials->reserve(num_serials); | |
| 156 data->remove_prefix(sizeof(uint32)); | |
| 157 | |
| 158 for (uint32 i = 0; i < num_serials; ++i) { | |
| 159 if (data->size() < sizeof(uint8)) | |
| 160 return false; | |
| 161 | |
| 162 uint8 serial_length = data->data()[0]; | |
| 163 data->remove_prefix(sizeof(uint8)); | |
| 164 | |
| 165 if (data->size() < serial_length) | |
| 166 return false; | |
| 167 | |
| 168 out_serials->push_back(std::string()); | |
| 169 out_serials->back().assign(data->data(), serial_length); | |
| 170 data->remove_prefix(serial_length); | |
| 171 } | |
| 172 | |
| 173 return true; | |
| 174 } | |
| 175 | |
| 176 // static | |
| 177 bool CRLSetStorage::CopyBlockedSPKIsFromHeader( | |
| 178 CRLSet* crl_set, | |
| 179 base::DictionaryValue* header_dict) { | |
| 180 base::ListValue* blocked_spkis_list = NULL; | |
| 181 if (!header_dict->GetList("BlockedSPKIs", &blocked_spkis_list)) { | |
| 182 // BlockedSPKIs is optional, so it's fine if we don't find it. | |
| 183 return true; | |
| 184 } | |
| 185 | |
| 186 crl_set->blocked_spkis_.clear(); | |
| 187 crl_set->blocked_spkis_.reserve(blocked_spkis_list->GetSize()); | |
| 188 | |
| 189 std::string spki_sha256_base64; | |
| 190 | |
| 191 for (size_t i = 0; i < blocked_spkis_list->GetSize(); ++i) { | |
| 192 spki_sha256_base64.clear(); | |
| 193 | |
| 194 if (!blocked_spkis_list->GetString(i, &spki_sha256_base64)) | |
| 195 return false; | |
| 196 | |
| 197 crl_set->blocked_spkis_.push_back(std::string()); | |
| 198 if (!base::Base64Decode(spki_sha256_base64, | |
| 199 &crl_set->blocked_spkis_.back())) { | |
| 200 crl_set->blocked_spkis_.pop_back(); | |
| 201 return false; | |
| 202 } | |
| 203 } | |
| 204 | |
| 205 return true; | |
| 206 } | |
| 207 | |
| 208 // kMaxUncompressedChangesLength is the largest changes array that we'll | |
| 209 // accept. This bounds the number of CRLs in the CRLSet as well as the number | |
| 210 // of serial numbers in a given CRL. | |
| 211 static const unsigned kMaxUncompressedChangesLength = 1024 * 1024; | |
| 212 | |
| 213 static bool ReadChanges(base::StringPiece* data, | |
| 214 std::vector<uint8>* out_changes) { | |
| 215 uint32 uncompressed_size, compressed_size; | |
| 216 if (data->size() < 2 * sizeof(uint32)) | |
| 217 return false; | |
| 218 // assumes little endian. | |
| 219 memcpy(&uncompressed_size, data->data(), sizeof(uint32)); | |
| 220 data->remove_prefix(4); | |
| 221 memcpy(&compressed_size, data->data(), sizeof(uint32)); | |
| 222 data->remove_prefix(4); | |
| 223 | |
| 224 if (uncompressed_size > kMaxUncompressedChangesLength) | |
| 225 return false; | |
| 226 if (data->size() < compressed_size) | |
| 227 return false; | |
| 228 | |
| 229 out_changes->clear(); | |
| 230 if (uncompressed_size == 0) | |
| 231 return true; | |
| 232 | |
| 233 out_changes->resize(uncompressed_size); | |
| 234 base::StringPiece compressed(data->data(), compressed_size); | |
| 235 data->remove_prefix(compressed_size); | |
| 236 return DecompressZlib(&(*out_changes)[0], uncompressed_size, compressed); | |
| 237 } | |
| 238 | |
| 239 // These are the range coder symbols used in delta updates. | |
| 240 enum { | |
| 241 SYMBOL_SAME = 0, | |
| 242 SYMBOL_INSERT = 1, | |
| 243 SYMBOL_DELETE = 2, | |
| 244 SYMBOL_CHANGED = 3, | |
| 245 }; | |
| 246 | |
| 247 static bool ReadDeltaCRL(base::StringPiece* data, | |
| 248 const std::vector<std::string>& old_serials, | |
| 249 std::vector<std::string>* out_serials) { | |
| 250 std::vector<uint8> changes; | |
| 251 if (!ReadChanges(data, &changes)) | |
| 252 return false; | |
| 253 | |
| 254 size_t i = 0; | |
| 255 for (std::vector<uint8>::const_iterator k = changes.begin(); | |
| 256 k != changes.end(); ++k) { | |
| 257 if (*k == SYMBOL_SAME) { | |
| 258 if (i >= old_serials.size()) | |
| 259 return false; | |
| 260 out_serials->push_back(old_serials[i]); | |
| 261 i++; | |
| 262 } else if (*k == SYMBOL_INSERT) { | |
| 263 uint8 serial_length; | |
| 264 if (data->size() < sizeof(uint8)) | |
| 265 return false; | |
| 266 memcpy(&serial_length, data->data(), sizeof(uint8)); | |
| 267 data->remove_prefix(sizeof(uint8)); | |
| 268 | |
| 269 if (data->size() < serial_length) | |
| 270 return false; | |
| 271 const std::string serial(data->data(), serial_length); | |
| 272 data->remove_prefix(serial_length); | |
| 273 | |
| 274 out_serials->push_back(serial); | |
| 275 } else if (*k == SYMBOL_DELETE) { | |
| 276 if (i >= old_serials.size()) | |
| 277 return false; | |
| 278 i++; | |
| 279 } else { | |
| 280 NOTREACHED(); | |
| 281 return false; | |
| 282 } | |
| 283 } | |
| 284 | |
| 285 if (i != old_serials.size()) | |
| 286 return false; | |
| 287 return true; | |
| 288 } | |
| 289 | |
| 290 // static | |
| 291 bool CRLSetStorage::Parse(base::StringPiece data, | |
| 292 scoped_refptr<CRLSet>* out_crl_set) { | |
| 293 TRACE_EVENT0("CRLSet", "Parse"); | |
| 294 // Other parts of Chrome assume that we're little endian, so we don't lose | |
| 295 // anything by doing this. | |
| 296 #if defined(__BYTE_ORDER) | |
| 297 // Linux check | |
| 298 static_assert(__BYTE_ORDER == __LITTLE_ENDIAN, "assumes little endian"); | |
| 299 #elif defined(__BIG_ENDIAN__) | |
| 300 // Mac check | |
| 301 #error assumes little endian | |
| 302 #endif | |
| 303 | |
| 304 scoped_ptr<base::DictionaryValue> header_dict(ReadHeader(&data)); | |
| 305 if (!header_dict.get()) | |
| 306 return false; | |
| 307 | |
| 308 std::string contents; | |
| 309 if (!header_dict->GetString("ContentType", &contents)) | |
| 310 return false; | |
| 311 if (contents != "CRLSet") | |
| 312 return false; | |
| 313 | |
| 314 int version; | |
| 315 if (!header_dict->GetInteger("Version", &version) || | |
| 316 version != kCurrentFileVersion) { | |
| 317 return false; | |
| 318 } | |
| 319 | |
| 320 int sequence; | |
| 321 if (!header_dict->GetInteger("Sequence", &sequence)) | |
| 322 return false; | |
| 323 | |
| 324 double not_after; | |
| 325 if (!header_dict->GetDouble("NotAfter", ¬_after)) { | |
| 326 // NotAfter is optional for now. | |
| 327 not_after = 0; | |
| 328 } | |
| 329 if (not_after < 0) | |
| 330 return false; | |
| 331 | |
| 332 scoped_refptr<CRLSet> crl_set(new CRLSet()); | |
| 333 crl_set->sequence_ = static_cast<uint32>(sequence); | |
| 334 crl_set->not_after_ = static_cast<uint64>(not_after); | |
| 335 crl_set->crls_.reserve(64); // Value observed experimentally. | |
| 336 | |
| 337 for (size_t crl_index = 0; !data.empty(); crl_index++) { | |
| 338 // Speculatively push back a pair and pass it to ReadCRL() to avoid | |
| 339 // unnecessary copies. | |
| 340 crl_set->crls_.push_back( | |
| 341 std::make_pair(std::string(), std::vector<std::string>())); | |
| 342 std::pair<std::string, std::vector<std::string> >* const back_pair = | |
| 343 &crl_set->crls_.back(); | |
| 344 | |
| 345 if (!ReadCRL(&data, &back_pair->first, &back_pair->second)) { | |
| 346 // Undo the speculative push_back() performed above. | |
| 347 crl_set->crls_.pop_back(); | |
| 348 return false; | |
| 349 } | |
| 350 | |
| 351 crl_set->crls_index_by_issuer_[back_pair->first] = crl_index; | |
| 352 } | |
| 353 | |
| 354 if (!CopyBlockedSPKIsFromHeader(crl_set.get(), header_dict.get())) | |
| 355 return false; | |
| 356 | |
| 357 *out_crl_set = crl_set; | |
| 358 return true; | |
| 359 } | |
| 360 | |
| 361 // static | |
| 362 bool CRLSetStorage::ApplyDelta(const CRLSet* in_crl_set, | |
| 363 const base::StringPiece& delta_bytes, | |
| 364 scoped_refptr<CRLSet>* out_crl_set) { | |
| 365 base::StringPiece data(delta_bytes); | |
| 366 scoped_ptr<base::DictionaryValue> header_dict(ReadHeader(&data)); | |
| 367 if (!header_dict.get()) | |
| 368 return false; | |
| 369 | |
| 370 std::string contents; | |
| 371 if (!header_dict->GetString("ContentType", &contents)) | |
| 372 return false; | |
| 373 if (contents != "CRLSetDelta") | |
| 374 return false; | |
| 375 | |
| 376 int version; | |
| 377 if (!header_dict->GetInteger("Version", &version) || | |
| 378 version != kCurrentFileVersion) { | |
| 379 return false; | |
| 380 } | |
| 381 | |
| 382 int sequence, delta_from; | |
| 383 if (!header_dict->GetInteger("Sequence", &sequence) || | |
| 384 !header_dict->GetInteger("DeltaFrom", &delta_from) || | |
| 385 delta_from < 0 || | |
| 386 static_cast<uint32>(delta_from) != in_crl_set->sequence_) { | |
| 387 return false; | |
| 388 } | |
| 389 | |
| 390 double not_after; | |
| 391 if (!header_dict->GetDouble("NotAfter", ¬_after)) { | |
| 392 // NotAfter is optional for now. | |
| 393 not_after = 0; | |
| 394 } | |
| 395 if (not_after < 0) | |
| 396 return false; | |
| 397 | |
| 398 scoped_refptr<CRLSet> crl_set(new CRLSet); | |
| 399 crl_set->sequence_ = static_cast<uint32>(sequence); | |
| 400 crl_set->not_after_ = static_cast<uint64>(not_after); | |
| 401 | |
| 402 if (!CopyBlockedSPKIsFromHeader(crl_set.get(), header_dict.get())) | |
| 403 return false; | |
| 404 | |
| 405 std::vector<uint8> crl_changes; | |
| 406 | |
| 407 if (!ReadChanges(&data, &crl_changes)) | |
| 408 return false; | |
| 409 | |
| 410 size_t i = 0, j = 0; | |
| 411 for (std::vector<uint8>::const_iterator k = crl_changes.begin(); | |
| 412 k != crl_changes.end(); ++k) { | |
| 413 if (*k == SYMBOL_SAME) { | |
| 414 if (i >= in_crl_set->crls_.size()) | |
| 415 return false; | |
| 416 crl_set->crls_.push_back(in_crl_set->crls_[i]); | |
| 417 crl_set->crls_index_by_issuer_[in_crl_set->crls_[i].first] = j; | |
| 418 i++; | |
| 419 j++; | |
| 420 } else if (*k == SYMBOL_INSERT) { | |
| 421 std::string parent_spki_hash; | |
| 422 std::vector<std::string> serials; | |
| 423 if (!ReadCRL(&data, &parent_spki_hash, &serials)) | |
| 424 return false; | |
| 425 crl_set->crls_.push_back(std::make_pair(parent_spki_hash, serials)); | |
| 426 crl_set->crls_index_by_issuer_[parent_spki_hash] = j; | |
| 427 j++; | |
| 428 } else if (*k == SYMBOL_DELETE) { | |
| 429 if (i >= in_crl_set->crls_.size()) | |
| 430 return false; | |
| 431 i++; | |
| 432 } else if (*k == SYMBOL_CHANGED) { | |
| 433 if (i >= in_crl_set->crls_.size()) | |
| 434 return false; | |
| 435 std::vector<std::string> serials; | |
| 436 if (!ReadDeltaCRL(&data, in_crl_set->crls_[i].second, &serials)) | |
| 437 return false; | |
| 438 crl_set->crls_.push_back( | |
| 439 std::make_pair(in_crl_set->crls_[i].first, serials)); | |
| 440 crl_set->crls_index_by_issuer_[in_crl_set->crls_[i].first] = j; | |
| 441 i++; | |
| 442 j++; | |
| 443 } else { | |
| 444 NOTREACHED(); | |
| 445 return false; | |
| 446 } | |
| 447 } | |
| 448 | |
| 449 if (!data.empty()) | |
| 450 return false; | |
| 451 if (i != in_crl_set->crls_.size()) | |
| 452 return false; | |
| 453 | |
| 454 *out_crl_set = crl_set; | |
| 455 return true; | |
| 456 } | |
| 457 | |
| 458 // static | |
| 459 bool CRLSetStorage::GetIsDeltaUpdate(const base::StringPiece& bytes, | |
| 460 bool* is_delta) { | |
| 461 base::StringPiece data(bytes); | |
| 462 scoped_ptr<base::DictionaryValue> header_dict(ReadHeader(&data)); | |
| 463 if (!header_dict.get()) | |
| 464 return false; | |
| 465 | |
| 466 std::string contents; | |
| 467 if (!header_dict->GetString("ContentType", &contents)) | |
| 468 return false; | |
| 469 | |
| 470 if (contents == "CRLSet") { | |
| 471 *is_delta = false; | |
| 472 } else if (contents == "CRLSetDelta") { | |
| 473 *is_delta = true; | |
| 474 } else { | |
| 475 return false; | |
| 476 } | |
| 477 | |
| 478 return true; | |
| 479 } | |
| 480 | |
| 481 // static | |
| 482 std::string CRLSetStorage::Serialize(const CRLSet* crl_set) { | |
| 483 std::string header = base::StringPrintf( | |
| 484 "{" | |
| 485 "\"Version\":0," | |
| 486 "\"ContentType\":\"CRLSet\"," | |
| 487 "\"Sequence\":%u," | |
| 488 "\"DeltaFrom\":0," | |
| 489 "\"NumParents\":%u," | |
| 490 "\"BlockedSPKIs\":[", | |
| 491 static_cast<unsigned>(crl_set->sequence_), | |
| 492 static_cast<unsigned>(crl_set->crls_.size())); | |
| 493 | |
| 494 for (std::vector<std::string>::const_iterator i = | |
| 495 crl_set->blocked_spkis_.begin(); | |
| 496 i != crl_set->blocked_spkis_.end(); ++i) { | |
| 497 std::string spki_hash_base64; | |
| 498 base::Base64Encode(*i, &spki_hash_base64); | |
| 499 | |
| 500 if (i != crl_set->blocked_spkis_.begin()) | |
| 501 header += ","; | |
| 502 header += "\"" + spki_hash_base64 + "\""; | |
| 503 } | |
| 504 header += "]"; | |
| 505 if (crl_set->not_after_ != 0) | |
| 506 header += base::StringPrintf(",\"NotAfter\":%" PRIu64, crl_set->not_after_); | |
| 507 header += "}"; | |
| 508 | |
| 509 size_t len = 2 /* header len */ + header.size(); | |
| 510 | |
| 511 for (CRLSet::CRLList::const_iterator i = crl_set->crls_.begin(); | |
| 512 i != crl_set->crls_.end(); ++i) { | |
| 513 len += i->first.size() + 4 /* num serials */; | |
| 514 for (std::vector<std::string>::const_iterator j = i->second.begin(); | |
| 515 j != i->second.end(); ++j) { | |
| 516 len += 1 /* serial length */ + j->size(); | |
| 517 } | |
| 518 } | |
| 519 | |
| 520 std::string ret; | |
| 521 uint8_t* out = reinterpret_cast<uint8_t*>( | |
| 522 WriteInto(&ret, len + 1 /* to include final NUL */)); | |
| 523 size_t off = 0; | |
| 524 CHECK(base::IsValueInRangeForNumericType<uint16>(header.size())); | |
| 525 out[off++] = static_cast<uint8_t>(header.size()); | |
| 526 out[off++] = static_cast<uint8_t>(header.size() >> 8); | |
| 527 memcpy(out + off, header.data(), header.size()); | |
| 528 off += header.size(); | |
| 529 | |
| 530 for (CRLSet::CRLList::const_iterator i = crl_set->crls_.begin(); | |
| 531 i != crl_set->crls_.end(); ++i) { | |
| 532 memcpy(out + off, i->first.data(), i->first.size()); | |
| 533 off += i->first.size(); | |
| 534 const uint32 num_serials = i->second.size(); | |
| 535 memcpy(out + off, &num_serials, sizeof(num_serials)); | |
| 536 off += sizeof(num_serials); | |
| 537 | |
| 538 for (std::vector<std::string>::const_iterator j = i->second.begin(); | |
| 539 j != i->second.end(); ++j) { | |
| 540 CHECK(base::IsValueInRangeForNumericType<uint8_t>(j->size())); | |
| 541 out[off++] = static_cast<uint8_t>(j->size()); | |
| 542 memcpy(out + off, j->data(), j->size()); | |
| 543 off += j->size(); | |
| 544 } | |
| 545 } | |
| 546 | |
| 547 CHECK_EQ(off, len); | |
| 548 return ret; | |
| 549 } | |
| 550 | |
| 551 } // namespace net | |
| OLD | NEW |