Chromium Code Reviews| 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 "components/data_reduction_proxy/browser/data_reduction_proxy_tamper_de tect.h" | |
| 6 | |
| 7 #include <algorithm> | |
| 8 #include <cstring> | |
| 9 | |
| 10 #include "base/base64.h" | |
| 11 #include "base/md5.h" | |
| 12 #include "base/metrics/histogram.h" | |
| 13 #include "base/metrics/sparse_histogram.h" | |
| 14 #include "base/strings/string_number_conversions.h" | |
| 15 #include "components/data_reduction_proxy/common/data_reduction_proxy_headers.h" | |
| 16 #include "net/android/network_library.h" | |
| 17 #include "net/http/http_response_headers.h" | |
| 18 #include "net/http/http_util.h" | |
| 19 | |
| 20 // Macro for UMA reporting. First reporting to either |https_histogram| or | |
| 21 // |http_histogram| depending on |scheme_is_https|, with |carrier_id| as bucket | |
| 22 // which counts for each carrier, how many responses have been tampered with. | |
| 23 // Then reporting to |http(s)_histogram|_Total, which counts the total number of | |
| 24 // detected tampering (sum of tampering detected for all carriers). | |
| 25 #define UMA_REPORT(scheme_is_https, http_histogram, https_histogram, carrier_id) \ | |
| 26 do { \ | |
| 27 if (scheme_is_https) { \ | |
| 28 UMA_HISTOGRAM_SPARSE_SLOWLY(https_histogram, carrier_id); \ | |
| 29 UMA_HISTOGRAM_COUNTS(https_histogram "_Total", 1); \ | |
| 30 } else { \ | |
| 31 UMA_HISTOGRAM_SPARSE_SLOWLY(http_histogram, carrier_id); \ | |
| 32 UMA_HISTOGRAM_COUNTS(http_histogram "_Total", 1); \ | |
| 33 }\ | |
| 34 } while (0) | |
| 35 | |
| 36 namespace { | |
| 37 // Four fingerprints will be added to Chrome-Proxy header. One starts with | |
| 38 // |kTamperDetectFingerprintChromeProxy|, which is the fingerprint for the | |
| 39 // Chrome-Proxy header. The other three have been put together into a string | |
| 40 // starts with key tag |kTamperDetectFingerprint|. | |
| 41 const char kTamperDetectFingerprints[] = "fp"; | |
|
bengr
2014/07/16 21:24:48
To avoid name conflicts, all chrome-proxy actions
xingx
2014/07/18 17:25:01
Done.
| |
| 42 const char kTamperDetectFingerprintChromeProxy[] = "cp"; | |
| 43 | |
| 44 // Fingerprint |kTamperDetectFingerprint| contains three fingerprints, each | |
| 45 // starts with a key tag followed by "=" and its fingerprint value. Three | |
| 46 // fingerprints and their respective tags are defined below. | |
| 47 const char kTamperDetectFingerprintVia[] = "via"; | |
| 48 const char kTamperDetectFingerprintOther[] = "oh"; | |
| 49 const char kTamperDetectFingerprintContengLength[] = "cl"; | |
| 50 } // namespace | |
|
bengr
2014/07/16 21:24:48
Add a space before //
xingx
2014/07/18 17:25:01
Done.
| |
| 51 | |
| 52 namespace data_reduction_proxy { | |
| 53 | |
| 54 // static | |
| 55 void DataReductionProxyTamperDetection::CheckResponseFingerprint( | |
| 56 const net::HttpResponseHeaders* headers, | |
|
bengr
2014/07/16 21:24:47
check that headers is non-NULL.
xingx
2014/07/18 17:25:01
Done.
| |
| 57 const bool is_secure_scheme) | |
|
bengr
2014/07/16 21:24:48
can this function be const?
xingx
2014/07/18 17:25:01
Right now the function is static, so can't be cons
| |
| 58 { | |
|
bengr
2014/07/16 21:24:48
Move the curly up a line.
xingx
2014/07/18 17:25:01
Done.
| |
| 59 // If Chrome-Proxy header fingerprint absents, quits tamper detection ASAP. | |
|
bengr
2014/07/16 21:24:48
If the Chrome-Proxy header fingerprint is absent,
xingx
2014/07/18 17:25:01
Done.
| |
| 60 if (!GetDataReductionProxyActionValue( | |
| 61 headers, kTamperDetectFingerprintChromeProxy, NULL)) | |
| 62 return; | |
| 63 | |
| 64 // Found tamper detection request field. Saves Chrome-Proxy header values to | |
| 65 // get fingerprints. | |
| 66 std::vector<std::string> values = GetHeaderValues(headers, "Chrome-Proxy"); | |
|
bengr
2014/07/16 21:24:47
I still don't understand why you need this.
bengr
2014/07/16 21:24:47
values -> chrome_proxy_header_values
xingx
2014/07/18 17:25:01
Acknowledged.
xingx
2014/07/18 17:25:02
Acknowledged.
| |
| 67 | |
| 68 // |chrome_proxy_fingerprint| holds the value of fingerprint of Chrome-Proxy | |
| 69 // header. |other_fingerprints| holds the value of other fingerprints. | |
| 70 // |values| saves all the values of Chrome-Proxy header but with fingerprint | |
| 71 // for Chrome-Proxy header removed. | |
| 72 std::string chrome_proxy_fingerprint, other_fingerprints; | |
| 73 if (!GetTamperDetectionFingerprints(&values, | |
| 74 &chrome_proxy_fingerprint, | |
| 75 &other_fingerprints)) | |
| 76 return; | |
|
bengr
2014/07/16 21:24:48
I would add curly braces around this.
xingx
2014/07/18 17:25:01
Done.
| |
| 77 | |
| 78 // Get carrier ID. | |
| 79 unsigned carrier_id = 0; | |
| 80 #if defined(OS_ANDROID) | |
|
bengr
2014/07/16 21:24:47
Is this busted on other platforms?
xingx
2014/07/18 17:25:01
Discussed with Bolian, right now for other platfor
| |
| 81 base::StringToUint(net::android::GetTelephonyNetworkOperator(), &carrier_id); | |
| 82 #endif | |
| 83 | |
| 84 DataReductionProxyTamperDetection tamper_detection(headers, | |
| 85 is_secure_scheme, | |
| 86 carrier_id, | |
| 87 &values); | |
| 88 | |
| 89 // Check if Chrome-Proxy header has been tampered with. | |
| 90 if (tamper_detection.IsChromeProxyHeaderTampered(chrome_proxy_fingerprint)) { | |
| 91 tamper_detection.ReportChromeProxyHeaderTamperedUMA(); | |
| 92 return; | |
| 93 } else { | |
| 94 UMA_REPORT(is_secure_scheme, | |
| 95 "DataReductionProxy.HTTPSHeaderTamperDetection", | |
| 96 "DataReductionProxy.HTTPHeaderTamperDetection", | |
| 97 carrier_id); | |
| 98 } | |
| 99 | |
| 100 // Separate fingerprints from |other_fingerprints|, with delimiter "|". | |
| 101 net::HttpUtil::ValuesIterator it( | |
| 102 other_fingerprints.begin(), other_fingerprints.end(), '|'); | |
|
bengr
2014/07/16 21:24:48
Are fingerprints guaranteed not to contain '|'. Sa
xingx
2014/07/18 17:25:02
Modified the design.
| |
| 103 | |
| 104 // For each fingerprint, get its name |key| and the fingerprint |value| | |
|
bengr
2014/07/16 21:24:48
|value|.
xingx
2014/07/18 17:25:02
Done.
| |
| 105 size_t delimiter_pos = std::string::npos; | |
| 106 while (it.GetNext()) { | |
| 107 delimiter_pos = it.value().find("="); | |
| 108 if (delimiter_pos == std::string::npos) | |
| 109 continue; | |
| 110 std::string key = it.value().substr(0, delimiter_pos); | |
| 111 std::string value = it.value().substr(delimiter_pos + 1); | |
|
bengr
2014/07/16 21:24:48
Make each fingerprint a Chrome-Proxy action starti
xingx
2014/07/18 17:25:01
Done.
| |
| 112 | |
| 113 FingerprintCode fingerprint_code = tamper_detection.GetFingerprintCode(key); | |
| 114 switch (fingerprint_code) { | |
| 115 case VIA: | |
| 116 if (tamper_detection.IsViaHeaderTampered(value)) | |
| 117 tamper_detection.ReportViaHeaderTamperedUMA(); | |
| 118 break; | |
| 119 case OTHERHEADERS: | |
| 120 if (tamper_detection.AreOtherHeadersTampered(value)) | |
| 121 tamper_detection.ReportOtherHeadersTamperedUMA(); | |
| 122 break; | |
| 123 case CONTENTLENGTH: | |
| 124 if (tamper_detection.IsContentLengthHeaderTampered(value)) | |
| 125 tamper_detection.ReportContentLengthHeaderTamperedUMA(); | |
| 126 break; | |
| 127 default: | |
| 128 NOTREACHED(); | |
| 129 break; | |
| 130 } | |
| 131 } | |
| 132 return; | |
| 133 } | |
| 134 | |
| 135 // Constructor initializes fingerprint code map. | |
| 136 DataReductionProxyTamperDetection::DataReductionProxyTamperDetection( | |
| 137 const net::HttpResponseHeaders* headers, | |
|
bengr
2014/07/16 21:24:48
DCHECK that headers is non-NULL
xingx
2014/07/18 17:25:01
Done.
| |
| 138 const bool is_secure, | |
| 139 const unsigned carrier_id, | |
| 140 std::vector<std::string>* values) | |
| 141 : response_headers_(headers), | |
| 142 is_secure_scheme_(is_secure), | |
| 143 carrier_id_(carrier_id), | |
| 144 clean_chrome_proxy_header_values_(values) { | |
| 145 fingperprint_tag_code_map_ = std::map<std::string, FingerprintCode>(); | |
| 146 fingperprint_tag_code_map_[kTamperDetectFingerprintVia] = VIA; | |
| 147 fingperprint_tag_code_map_[kTamperDetectFingerprintOther] = OTHERHEADERS; | |
| 148 fingperprint_tag_code_map_[kTamperDetectFingerprintContengLength] = | |
| 149 CONTENTLENGTH; | |
| 150 }; | |
| 151 | |
| 152 DataReductionProxyTamperDetection::~DataReductionProxyTamperDetection() {}; | |
| 153 | |
| 154 // Checks whether the Chrome-Proxy header has been tampered with. |fingerprint| | |
| 155 // is the fingerprint received from data reduction proxy, which is Base64 | |
| 156 // encoded. Decodes it first. Calculates the hash value of Chrome-Proxy header. | |
| 157 // Note that |clean_chrome_proxy_header_values_| holds the values of | |
| 158 // Chrome-Proxy header with its own fingerprint removed, so it's the correct | |
| 159 // values to be used to calculate fingerprint. Compares calculated fingerprint | |
| 160 // to the fingerprint from data reduction proxy (the removed value) and see | |
| 161 // there is tamper detected or not. | |
| 162 bool DataReductionProxyTamperDetection::IsChromeProxyHeaderTampered( | |
| 163 const std::string& fingerprint) const { | |
| 164 std::string received_fingerprint; | |
| 165 if (!base::Base64Decode(fingerprint, &received_fingerprint)) | |
| 166 return true; | |
| 167 // Calculate the MD5 hash value of Chrome-Proxy. | |
| 168 std::string actual_fingerprint = GetMD5( | |
| 169 ValuesToSortedString(clean_chrome_proxy_header_values_)); | |
| 170 | |
| 171 return received_fingerprint != actual_fingerprint; | |
| 172 } | |
| 173 | |
| 174 // Reports Chrome-Proxy header tamper detected. | |
| 175 void DataReductionProxyTamperDetection::ReportChromeProxyHeaderTamperedUMA() | |
| 176 const { | |
| 177 UMA_REPORT(is_secure_scheme_, | |
|
bengr
2014/07/16 21:24:47
I'd rename the macro to be less general sounding.
xingx
2014/07/18 17:25:01
Done.
| |
| 178 "DataReductionProxy.HTTPSHeaderTampered_ChromeProxy", | |
| 179 "DataReductionProxy.HTTPHeaderTampered_ChromeProxy", | |
| 180 carrier_id_); | |
| 181 } | |
| 182 | |
| 183 // Checks whether there are other proxies/middleboxes' name after the data | |
| 184 // reduction proxy's name in Via header. | |
| 185 bool DataReductionProxyTamperDetection::IsViaHeaderTampered( | |
| 186 const std::string& fingerprint) const { | |
| 187 bool has, is_the_last; | |
| 188 has = HasDataReductionProxyViaHeader(response_headers_, &is_the_last); | |
| 189 | |
| 190 // Via header of the data reductoin proxy is missing. | |
|
bengr
2014/07/16 21:24:47
spelling
xingx
2014/07/18 17:25:01
Done.
| |
| 191 if (!has) { | |
| 192 UMA_REPORT(is_secure_scheme_, | |
| 193 "DataReductionProxy.HTTPSHeaderTampered_Via_Missing", | |
| 194 "DataReductionProxy.HTTPHeaderTampered_Via_Missing", | |
| 195 carrier_id_); | |
| 196 return false; | |
| 197 } | |
| 198 | |
| 199 return !is_the_last; | |
| 200 } | |
| 201 | |
| 202 // Reports Via header tamper detected. | |
| 203 void DataReductionProxyTamperDetection::ReportViaHeaderTamperedUMA() const { | |
| 204 UMA_REPORT(is_secure_scheme_, | |
| 205 "DataReductionProxy.HTTPSHeaderTampered_Via", | |
| 206 "DataReductionProxy.HTTPHeaderTampered_Via", | |
| 207 carrier_id_); | |
| 208 } | |
| 209 | |
| 210 // Checks whether values of a predefined list of headers have been tampered. | |
| 211 // The format for |fingerprint| is: | |
| 212 // [base64encoded_fingerprint]:header_name1:header_namer2:... | |
| 213 // Firstly extract the header names in the |fingerprint|. For each header, get | |
| 214 // all the values of such header, sort the values and concatenate them to a | |
| 215 // string. Concatenate the string for all the headers (with delimiter ";") and | |
| 216 // calculate the MD5 hash value on it. Compare such hash value to the | |
| 217 // fingerprint received from data reduction proxy. | |
| 218 bool DataReductionProxyTamperDetection::AreOtherHeadersTampered( | |
| 219 const std::string& fingerprint) const { | |
| 220 std::string received_fingerprint; | |
| 221 | |
| 222 // Fingerprint should never be empty. | |
| 223 DCHECK(fingerprint.size()); | |
| 224 | |
| 225 net::HttpUtil::ValuesIterator it(fingerprint.begin(), | |
| 226 fingerprint.end(), ':'); | |
| 227 | |
| 228 // The first value from fingerprint is the actual fingerprint; the following | |
| 229 // values are the header names will be included for fingerprint calculation. | |
| 230 // Make sure there is [base64fingerprint] and it can be decoded. | |
| 231 if (!(it.GetNext() && | |
| 232 base::Base64Decode(it.value(), &received_fingerprint))) { | |
| 233 NOTREACHED(); | |
| 234 return true; | |
| 235 } | |
| 236 | |
| 237 std::string header_values; | |
| 238 // Enumerate the list of headers. | |
| 239 while (it.GetNext()) { | |
| 240 // Get values of one header. | |
| 241 std::vector<std::string> values = | |
|
bengr
2014/07/16 21:24:48
rename as response_header_values
xingx
2014/07/18 17:25:01
Done.
| |
| 242 GetHeaderValues(response_headers_, it.value()); | |
| 243 // Sort the values and concatenate them. | |
| 244 header_values += ValuesToSortedString(&values) + ";"; | |
|
bengr
2014/07/16 21:24:47
are the values guaranteed not to contain ';'? Add
xingx
2014/07/18 17:25:02
Done.
| |
| 245 } | |
| 246 | |
| 247 // Calculate MD5 hash value on the concatenated string. | |
| 248 std::string actual_fingerprint = GetMD5(header_values); | |
| 249 | |
| 250 return received_fingerprint != actual_fingerprint; | |
| 251 } | |
| 252 | |
| 253 // Reports other headers tamper detected. | |
| 254 void DataReductionProxyTamperDetection::ReportOtherHeadersTamperedUMA() const { | |
| 255 UMA_REPORT(is_secure_scheme_, | |
| 256 "DataReductionProxy.HTTPSHeaderTampered_OtherHeaders", | |
| 257 "DataReductionProxy.HTTPHeaderTampered_OtherHeaders", | |
| 258 carrier_id_); | |
| 259 } | |
| 260 | |
| 261 // Checks whether the Content-Length value is different from what data reduction | |
| 262 // proxy sees. Reports it as modified only if Content-Length can be decoded as | |
| 263 // an interger at both end and such two numbers are not equal. | |
| 264 bool DataReductionProxyTamperDetection::IsContentLengthHeaderTampered( | |
| 265 const std::string& fingerprint) const { | |
| 266 int received_content_length, actual_content_length; | |
| 267 // If Content-Length value from data reduction proxy is not available or | |
| 268 // it cannot be converted to an integer, pass. | |
| 269 if (base::StringToInt(fingerprint, &received_content_length)) { | |
| 270 std::string actual_content_length_; | |
| 271 // If there is no Content-Length header received, pass. | |
| 272 if (response_headers_->GetNormalizedHeader("Content-Length", | |
| 273 &actual_content_length_)) { | |
|
bengr
2014/07/16 21:24:47
indentation.
xingx
2014/07/18 17:25:02
Done.
| |
| 274 // If the Content-Length value cannot be converted to integer, | |
| 275 // i.e., not valid, pass. | |
| 276 if (!base::StringToInt(actual_content_length_, &actual_content_length)) | |
| 277 return false; | |
| 278 return received_content_length != actual_content_length; | |
| 279 } | |
| 280 } | |
| 281 return false; | |
| 282 } | |
| 283 | |
| 284 // Reports Content-Length tamper detected. | |
| 285 // Gets MIME type of the response and report to different UMA histogram. | |
| 286 // Right now MIME types contain JavaScript, CSS, Images, and others. | |
| 287 void DataReductionProxyTamperDetection::ReportContentLengthHeaderTamperedUMA() | |
| 288 const { | |
| 289 UMA_REPORT(is_secure_scheme_, | |
| 290 "DataReductionProxy.HTTPSHeaderTampered_ContentLength", | |
| 291 "DataReductionProxy.HTTPHeaderTampered_ContentLength", | |
| 292 carrier_id_); | |
| 293 | |
| 294 // Get MIME type. | |
| 295 std::string mime_type; | |
| 296 response_headers_->GetMimeType(&mime_type); | |
| 297 | |
| 298 // Reports tampered JavaScript. | |
| 299 if (mime_type.compare("text/javascript") == 0 || | |
| 300 mime_type.compare("application/x-javascript") == 0 || | |
| 301 mime_type.compare("application/javascript") == 0) | |
| 302 UMA_REPORT(is_secure_scheme_, | |
| 303 "DataReductionProxy.HTTPSHeaderTampered_ContentLength_JS", | |
| 304 "DataReductionProxy.HTTPHeaderTampered_ContentLength_JS", | |
| 305 carrier_id_); | |
| 306 // Reports tampered CSSs. | |
| 307 else if (mime_type.compare("text/css") == 0) | |
| 308 UMA_REPORT(is_secure_scheme_, | |
| 309 "DataReductionProxy.HTTPSHeaderTampered_ContentLength_CSS", | |
| 310 "DataReductionProxy.HTTPHeaderTampered_ContentLength_CSS", | |
| 311 carrier_id_); | |
| 312 // Reports tampered images. | |
| 313 else if (mime_type.find("image") == 0) | |
| 314 UMA_REPORT(is_secure_scheme_, | |
| 315 "DataReductionProxy.HTTPSHeaderTampered_ContentLength_Image", | |
| 316 "DataReductionProxy.HTTPHeaderTampered_ContentLength_Image", | |
| 317 carrier_id_); | |
| 318 // Reports tampered other MIME types. | |
| 319 else | |
| 320 UMA_REPORT(is_secure_scheme_, | |
| 321 "DataReductionProxy.HTTPSHeaderTampered_ContentLength_Other", | |
| 322 "DataReductionProxy.HTTPHeaderTampered_ContentLength_Other", | |
| 323 carrier_id_); | |
| 324 } | |
| 325 | |
| 326 DataReductionProxyTamperDetection::FingerprintCode | |
| 327 DataReductionProxyTamperDetection::GetFingerprintCode( | |
| 328 const std::string& tag) { | |
| 329 std::map<std::string, DataReductionProxyTamperDetection::FingerprintCode> | |
| 330 ::iterator it = fingperprint_tag_code_map_.find(tag); | |
| 331 | |
| 332 return it == fingperprint_tag_code_map_.end() ? NONEXIST : it->second; | |
| 333 } | |
| 334 | |
| 335 // Enumerates the values of Chrome-Proxy header and checks if there are | |
|
bengr
2014/07/16 21:24:48
Move method comments to the .h.
xingx
2014/07/18 17:25:01
Function removed.
| |
| 336 // fingerprints for Chrome-Proxy header (kTamperDetectFingerprintChromeProxy) | |
| 337 // and other fingerprints (kTamperDetectFingerprint). If there are, saves them | |
| 338 // accordingly, removes fingerprint for Chrome-Proxy header from |values| and | |
| 339 // return true, otherwise return false. | |
| 340 bool DataReductionProxyTamperDetection::GetTamperDetectionFingerprints( | |
| 341 std::vector<std::string>* values, | |
|
bengr
2014/07/16 21:24:47
DCHECK that values is non-NULL.
xingx
2014/07/18 17:25:02
Function removed.
| |
| 342 std::string* chrome_proxy_fingerprint, | |
| 343 std::string* other_fingerprints) { | |
| 344 int chrome_proxy_fingerprint_index = -1; | |
| 345 | |
| 346 size_t size = values->size(); | |
| 347 for (size_t i = 0; i < size; ++i) { | |
| 348 if ((*values)[i].find(kTamperDetectFingerprintChromeProxy) == 0 && | |
| 349 (*values)[i][strlen(kTamperDetectFingerprintChromeProxy)] == '=') { | |
| 350 chrome_proxy_fingerprint_index = i; | |
| 351 // Saves Chrome-Proxy fingerprint. | |
| 352 *chrome_proxy_fingerprint = (*values)[i].substr(strlen( | |
| 353 kTamperDetectFingerprintChromeProxy)); | |
| 354 } | |
| 355 else if ((*values)[i].find(kTamperDetectFingerprints) == 0 && | |
| 356 (*values)[i][strlen(kTamperDetectFingerprints)] == '=') { | |
| 357 // Saves other fingerprints. | |
| 358 *other_fingerprints = (*values)[i].substr( | |
| 359 strlen(kTamperDetectFingerprints)); | |
| 360 } | |
| 361 } | |
| 362 | |
| 363 if (chrome_proxy_fingerprint_index == -1) | |
| 364 return false; | |
| 365 | |
| 366 // Erases Chrome-Proxy's fingerprint from Chrome-Proxy header for | |
| 367 // later fingerprint calculation. | |
| 368 values->erase(values->begin() + chrome_proxy_fingerprint_index); | |
| 369 return true; | |
| 370 } | |
| 371 | |
| 372 // Sorts the strings in |values| and concatenates them into a string. | |
| 373 std::string DataReductionProxyTamperDetection::ValuesToSortedString( | |
| 374 std::vector<std::string>* values) { | |
| 375 std::string aggregated_values; | |
| 376 | |
| 377 std::sort(values->begin(), values->end()); | |
| 378 for (size_t i = 0; i < values->size(); ++i) | |
| 379 aggregated_values += (*values)[i] + ","; | |
| 380 return aggregated_values; | |
| 381 } | |
| 382 | |
| 383 // For a given string, calculates and returns the MD5 hash value of the string. | |
| 384 std::string DataReductionProxyTamperDetection::GetMD5( | |
| 385 const std::string &input) { | |
| 386 base::MD5Digest digest; | |
| 387 base::MD5Sum(input.c_str(), input.size(), &digest); | |
| 388 return std::string((char*)digest.a, ARRAYSIZE_UNSAFE(digest.a)); | |
| 389 } | |
| 390 | |
| 391 // For a given |header_name|, gets all its values and returns them as a vector. | |
| 392 std::vector<std::string> DataReductionProxyTamperDetection::GetHeaderValues( | |
| 393 const net::HttpResponseHeaders* headers, const std::string& header_name) { | |
| 394 std::vector<std::string> values; | |
| 395 std::string value; | |
| 396 void* iter = NULL; | |
| 397 while (headers->EnumerateHeader(&iter, header_name, &value)) { | |
| 398 values.push_back(value); | |
| 399 } | |
| 400 return values; | |
| 401 } | |
| 402 } // namespace data_reduction_proxy | |
| OLD | NEW |