1 // Copyright 2014 The Chromium Authors
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/metrics/net/net_metrics_log_uploader.h"
6
7 #include <sstream>
8
9 #include "base/base64.h"
10 #include "base/feature_list.h"
11 #include "base/functional/bind.h"
12 #include "base/metrics/histogram_macros.h"
13 #include "base/metrics/statistics_recorder.h"
14 #include "base/strings/strcat.h"
15 #include "base/strings/string_number_conversions.h"
16 #include "components/encrypted_messages/encrypted_message.pb.h"
17 #include "components/encrypted_messages/message_encrypter.h"
18 #include "components/metrics/metrics_log.h"
19 #include "components/metrics/metrics_log_uploader.h"
20 #include "net/base/load_flags.h"
21 #include "net/base/url_util.h"
22 #include "net/traffic_annotation/network_traffic_annotation.h"
23 #include "services/network/public/cpp/resource_request.h"
24 #include "services/network/public/cpp/shared_url_loader_factory.h"
25 #include "services/network/public/cpp/simple_url_loader.h"
26 #include "services/network/public/mojom/url_response_head.mojom.h"
27 #include "third_party/metrics_proto/chrome_user_metrics_extension.pb.h"
28 #include "third_party/metrics_proto/reporting_info.pb.h"
29 #include "third_party/zlib/google/compression_utils.h"
30 #include "url/gurl.h"
31
32 namespace {
33
34 // Constants used for encrypting logs that are sent over HTTP. The
35 // corresponding private key is used by the metrics server to decrypt logs.
36 const char kEncryptedMessageLabel[] = "metrics log";
37
38 const uint8_t kServerPublicKey[] = {
39 0x51, 0xcc, 0x52, 0x67, 0x42, 0x47, 0x3b, 0x10, 0xe8, 0x63, 0x18,
40 0x3c, 0x61, 0xa7, 0x96, 0x76, 0x86, 0x91, 0x40, 0x71, 0x39, 0x5f,
41 0x31, 0x1a, 0x39, 0x5b, 0x76, 0xb1, 0x6b, 0x3d, 0x6a, 0x2b};
42
43 const uint32_t kServerPublicKeyVersion = 1;
44
45 constexpr char kNoUploadUrlsReasonMsg[] =
46 "No server upload URLs specified. Will not attempt to retransmit.";
47
GetNetworkTrafficAnnotation(const metrics::MetricsLogUploader::MetricServiceType & service_type,const metrics::LogMetadata & log_metadata)48 net::NetworkTrafficAnnotationTag GetNetworkTrafficAnnotation(
49 const metrics::MetricsLogUploader::MetricServiceType& service_type,
50 const metrics::LogMetadata& log_metadata) {
51 // The code in this function should remain so that we won't need a default
52 // case that does not have meaningful annotation.
53 // Structured Metrics is an UMA consented metric service.
54 if (service_type == metrics::MetricsLogUploader::UMA ||
55 service_type == metrics::MetricsLogUploader::STRUCTURED_METRICS) {
56 return net::DefineNetworkTrafficAnnotation("metrics_report_uma", R"(
57 semantics {
58 sender: "Metrics UMA Log Uploader"
59 description:
60 "Report of usage statistics and crash-related data about Chrome. "
61 "Usage statistics contain information such as preferences, button "
62 "clicks, and memory usage and do not include web page URLs or "
63 "personal information. See more at "
64 "https://www.google.com/chrome/browser/privacy/ under 'Usage "
65 "statistics and crash reports'. Usage statistics are tied to a "
66 "pseudonymous machine identifier and not to your email address."
67 trigger:
68 "Reports are automatically generated on startup and at intervals "
69 "while Chrome is running."
70 data:
71 "A protocol buffer with usage statistics and crash related data."
72 destination: GOOGLE_OWNED_SERVICE
73 last_reviewed: "2024-02-15"
74 user_data {
75 type: BIRTH_DATE
76 type: GENDER
77 type: HW_OS_INFO
78 type: OTHER
79 }
80 internal {
81 contacts {
82 owners: "//components/metrics/OWNERS"
83 }
84 }
85 }
86 policy {
87 cookies_allowed: NO
88 setting:
89 "Users can enable or disable this feature via "
90 "\"Help improve Chrome's features and performance\" in Chrome "
91 "settings under Sync and Google services > Other Google services. "
92 "The feature is enabled by default."
93 chrome_policy {
94 MetricsReportingEnabled {
95 policy_options {mode: MANDATORY}
96 MetricsReportingEnabled: false
97 }
98 }
99 })");
100 }
101 DCHECK_EQ(service_type, metrics::MetricsLogUploader::UKM);
102
103 if (log_metadata.log_source_type.has_value() &&
104 log_metadata.log_source_type.value() ==
105 metrics::UkmLogSourceType::APPKM_ONLY) {
106 return net::DefineNetworkTrafficAnnotation("metrics_report_appkm", R"(
107 semantics {
108 sender: "Metrics AppKM Log Uploader"
109 description:
110 "Report of usage statistics that are keyed by App Identifiers to "
111 "Google. These reports only contain App-Keyed Metrics (AppKMs) "
112 "records, which are the metrics related to the user interaction with "
113 "various Apps on ChromeOS devices only. The apps platform includes, "
114 "but is not limited to, progressive web apps (PWA), Chrome apps, and "
115 "apps from the various VMs / GuestOS's: Android (ARC++), Linux "
116 "(Crostini), Windows (Parallels), and Steam (Borealis). Usage "
117 "statistics are tied to a pseudonymous machine identifier and not to "
118 "your email address."
119 trigger:
120 "Reports are automatically generated on startup and at intervals "
121 "while Chrome is running with usage statistics and App Sync settings "
122 "enabled."
123 data:
124 "A protocol buffer with usage statistics and associated App Identifiers."
125 destination: GOOGLE_OWNED_SERVICE
126 last_reviewed: "2024-02-15"
127 user_data {
128 type: BIRTH_DATE
129 type: GENDER
130 type: HW_OS_INFO
131 type: SENSITIVE_URL
132 type: OTHER
133 }
134 internal {
135 contacts {
136 owners: "//components/metrics/OWNERS"
137 }
138 }
139 }
140 policy {
141 cookies_allowed: NO
142 setting:
143 "Users can enable or disable this feature using App Sync or usage "
144 "statistics checkbox from the settings. Both are on by default, but "
145 "can be turned-off by the user."
146 chrome_policy {
147 SyncDisabled {
148 policy_options {mode: MANDATORY}
149 SyncDisabled: true
150 }
151 MetricsReportingEnabled{
152 policy_options {mode: MANDATORY}
153 MetricsReportingEnabled: true
154 }
155 SyncTypesListDisabled {
156 SyncTypesListDisabled: {
157 entries: "apps"
158 }
159 }
160 }
161 })");
162 } else if (log_metadata.log_source_type.has_value() &&
163 log_metadata.log_source_type.value() ==
164 metrics::UkmLogSourceType::BOTH_UKM_AND_APPKM) {
165 return net::DefineNetworkTrafficAnnotation("metrics_report_ukm_and_appkm",
166 R"(
167 semantics {
168 sender: "Metrics UKM and AppKM Log Uploader"
169 description:
170 "Report of usage statistics that are keyed by URLs to Google. These "
171 "reports contains both AppKM and UKM data. This includes information "
172 "about the web pages you visit and your usage of them, such as page "
173 "load speed. This will also include URLs and statistics related to "
174 "downloaded files. These statistics may also include information "
175 "about the extensions that have been installed from Chrome Web "
176 "Store. Google only stores usage statistics associated with published "
177 "extensions, and URLs that are known by Google’s search index. Usage "
178 "statistics are tied to a pseudonymous machine identifier and not to "
179 "your email address. Note: Reports containing only AppKM data will be "
180 "reported under 'Metrics AppKM Log Uploader' and only UKM data will "
181 "be reported under 'Metrics UKM Log Uploader' instead."
182 trigger:
183 "Reports are automatically generated on startup and at intervals "
184 "while Chrome is running with usage statistics, 'Make searches and "
185 "browsing better' and App Sync settings enabled."
186 data:
187 "A protocol buffer with usage statistics and associated URLs."
188 destination: GOOGLE_OWNED_SERVICE
189 last_reviewed: "2024-02-15"
190 user_data {
191 type: BIRTH_DATE
192 type: GENDER
193 type: HW_OS_INFO
194 type: SENSITIVE_URL
195 type: OTHER
196 }
197 internal {
198 contacts {
199 owners: "//components/metrics/OWNERS"
200 }
201 }
202 }
203 policy {
204 cookies_allowed: NO
205 setting:
206 "Users can disble this feature by disabling 'Make searches and "
207 "browsing better' in Chrome's settings under Advanced Settings or "
208 "disabling App Sync. This is only enabled if the user has 'Help "
209 "improve Chrome's features and performance' enabled in the same "
210 "settings menu. Information about the installed extensions is sent "
211 "only if Extension Sync is enabled."
212 chrome_policy {
213 SyncDisabled {
214 policy_options {mode: MANDATORY}
215 SyncDisabled: true
216 }
217 MetricsReportingEnabled{
218 policy_options {mode: MANDATORY}
219 MetricsReportingEnabled: true
220 }
221 SyncTypesListDisabled {
222 SyncTypesListDisabled: {
223 entries: "apps"
224 }
225 }
226 UrlKeyedAnonymizedDataCollectionEnabled {
227 policy_options {mode: MANDATORY}
228 UrlKeyedAnonymizedDataCollectionEnabled: false
229 }
230 }
231 })");
232 } else {
233 return net::DefineNetworkTrafficAnnotation("metrics_report_ukm", R"(
234 semantics {
235 sender: "Metrics UKM Log Uploader"
236 description:
237 "Report of usage statistics that are keyed by URLs to Google. These "
238 "reports contains only UKM data. This includes information about the "
239 "web pages you visit and your usage of them, such as page load speed. "
240 "This will also include URLs and statistics related to downloaded "
241 "files. These statistics may also include information about the "
242 "extensions that have been installed from Chrome Web Store. Google "
243 "only stores usage statistics associated with published extensions, "
244 "and URLs that are known by Google’s search index. Usage statistics "
245 "are tied to a pseudonymous machine identifier and not to your email "
246 "address."
247 trigger:
248 "Reports are automatically generated on startup and at intervals "
249 "while Chrome is running with usage statistics and 'Make searches "
250 "and browsing better' settings enabled."
251 data:
252 "A protocol buffer with usage statistics and associated URLs."
253 destination: GOOGLE_OWNED_SERVICE
254 last_reviewed: "2024-02-15"
255 user_data {
256 type: BIRTH_DATE
257 type: GENDER
258 type: HW_OS_INFO
259 type: SENSITIVE_URL
260 type: OTHER
261 }
262 internal {
263 contacts {
264 owners: "//components/metrics/OWNERS"
265 }
266 }
267 }
268 policy {
269 cookies_allowed: NO
270 setting:
271 "Users can enable or disable this feature by disabling 'Make "
272 "searches and browsing better' in Chrome's settings under Advanced "
273 "Settings, Privacy. This has to be enabled for all active profiles. "
274 "This is only enabled if the user has 'Help improve Chrome's "
275 "features and performance' enabled in the same settings menu. "
276 "Information about the installed extensions is sent only if "
277 "Extension Sync is enabled."
278 chrome_policy {
279 MetricsReportingEnabled {
280 policy_options {mode: MANDATORY}
281 MetricsReportingEnabled: false
282 }
283 UrlKeyedAnonymizedDataCollectionEnabled {
284 policy_options {mode: MANDATORY}
285 UrlKeyedAnonymizedDataCollectionEnabled: false
286 }
287 }
288 })");
289 }
290 }
291
292 std::string SerializeReportingInfo(
293 const metrics::ReportingInfo& reporting_info) {
294 std::string bytes;
295 bool success = reporting_info.SerializeToString(&bytes);
296 DCHECK(success);
297 return base::Base64Encode(bytes);
298 }
299
300 // Encrypts a |plaintext| string, using the encrypted_messages component,
301 // returns |encrypted| which is a serialized EncryptedMessage object. Returns
302 // false if there was a problem encrypting.
303 bool EncryptString(const std::string& plaintext, std::string* encrypted) {
304 encrypted_messages::EncryptedMessage encrypted_message;
305 if (!encrypted_messages::EncryptSerializedMessage(
306 kServerPublicKey, kServerPublicKeyVersion, kEncryptedMessageLabel,
307 plaintext, &encrypted_message)) {
308 NOTREACHED() << "Error encrypting string.";
309 return false;
310 }
311 if (!encrypted_message.SerializeToString(encrypted)) {
312 NOTREACHED() << "Error serializing encrypted string.";
313 return false;
314 }
315 return true;
316 }
317
318 // Encrypts a |plaintext| string and returns |encoded|, which is a base64
319 // encoded serialized EncryptedMessage object. Returns false if there was a
320 // problem encrypting or serializing.
321 bool EncryptAndBase64EncodeString(const std::string& plaintext,
322 std::string* encoded) {
323 std::string encrypted_text;
324 if (!EncryptString(plaintext, &encrypted_text)) {
325 return false;
326 }
327
328 *encoded = base::Base64Encode(encrypted_text);
329 return true;
330 }
331
332 #ifndef NDEBUG
333 void LogUploadingHistograms(const std::string& compressed_log_data) {
334 if (!VLOG_IS_ON(2)) {
335 return;
336 }
337
338 std::string uncompressed;
339 if (!compression::GzipUncompress(compressed_log_data, &uncompressed)) {
340 DVLOG(2) << "failed to uncompress log";
341 return;
342 }
343 metrics::ChromeUserMetricsExtension proto;
344 if (!proto.ParseFromString(uncompressed)) {
345 DVLOG(2) << "failed to parse uncompressed log";
346 return;
347 };
348 DVLOG(2) << "Uploading histograms...";
349
350 const base::StatisticsRecorder::Histograms histograms =
351 base::StatisticsRecorder::GetHistograms();
352 auto get_histogram_name = [&](uint64_t name_hash) -> std::string {
353 for (base::HistogramBase* histogram : histograms) {
354 if (histogram->name_hash() == name_hash) {
355 return histogram->histogram_name();
356 }
357 }
358 return base::StrCat({"unnamed ", base::NumberToString(name_hash)});
359 };
360
361 for (int i = 0; i < proto.histogram_event_size(); i++) {
362 const metrics::HistogramEventProto& event = proto.histogram_event(i);
363
364 std::stringstream summary;
365 summary << " sum=" << event.sum();
366 for (int j = 0; j < event.bucket_size(); j++) {
367 const metrics::HistogramEventProto::Bucket& b = event.bucket(j);
368 // Empty fields have a specific meaning, see
369 // third_party/metrics_proto/histogram_event.proto.
370 summary << " bucket["
371 << (b.has_min() ? base::NumberToString(b.min()) : "..") << '-'
372 << (b.has_max() ? base::NumberToString(b.max()) : "..") << ")="
373 << (b.has_count() ? base::NumberToString(b.count()) : "(1)");
374 }
375 DVLOG(2) << get_histogram_name(event.name_hash()) << summary.str();
376 }
377 }
378 #endif
379
380 } // namespace
381
382 namespace metrics {
383
384 NetMetricsLogUploader::NetMetricsLogUploader(
385 scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
386 const GURL& server_url,
387 base::StringPiece mime_type,
388 MetricsLogUploader::MetricServiceType service_type,
389 const MetricsLogUploader::UploadCallback& on_upload_complete)
390 : NetMetricsLogUploader(url_loader_factory,
391 server_url,
392 /*insecure_server_url=*/GURL(),
393 mime_type,
394 service_type,
395 on_upload_complete) {}
396
397 NetMetricsLogUploader::NetMetricsLogUploader(
398 scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
399 const GURL& server_url,
400 const GURL& insecure_server_url,
401 base::StringPiece mime_type,
402 MetricsLogUploader::MetricServiceType service_type,
403 const MetricsLogUploader::UploadCallback& on_upload_complete)
404 : url_loader_factory_(std::move(url_loader_factory)),
405 server_url_(server_url),
406 insecure_server_url_(insecure_server_url),
407 mime_type_(mime_type.data(), mime_type.size()),
408 service_type_(service_type),
409 on_upload_complete_(on_upload_complete) {}
410
411 NetMetricsLogUploader::~NetMetricsLogUploader() = default;
412
413 void NetMetricsLogUploader::UploadLog(const std::string& compressed_log_data,
414 const LogMetadata& log_metadata,
415 const std::string& log_hash,
416 const std::string& log_signature,
417 const ReportingInfo& reporting_info) {
418 // If this attempt is a retry, there was a network error, the last attempt was
419 // over HTTPS, and there is an insecure URL set, then attempt this upload over
420 // HTTP.
421 if (reporting_info.attempt_count() > 1 &&
422 reporting_info.last_error_code() != 0 &&
423 reporting_info.last_attempt_was_https() &&
424 !insecure_server_url_.is_empty()) {
425 UploadLogToURL(compressed_log_data, log_metadata, log_hash, log_signature,
426 reporting_info, insecure_server_url_);
427 return;
428 }
429 UploadLogToURL(compressed_log_data, log_metadata, log_hash, log_signature,
430 reporting_info, server_url_);
431 }
432
433 void NetMetricsLogUploader::UploadLogToURL(
434 const std::string& compressed_log_data,
435 const LogMetadata& log_metadata,
436 const std::string& log_hash,
437 const std::string& log_signature,
438 const ReportingInfo& reporting_info,
439 const GURL& url) {
440 DCHECK(!log_hash.empty());
441
442 #ifndef NDEBUG
443 // For debug builds, you can use -vmodule=net_metrics_log_uploader=2
444 // to enable logging of uploaded histograms. You probably also want to use
445 // --force-enable-metrics-reporting, or metrics reporting may not be enabled.
446 LogUploadingHistograms(compressed_log_data);
447 #endif
448
449 auto resource_request = std::make_unique<network::ResourceRequest>();
450 resource_request->url = url;
451 // Drop cookies and auth data.
452 resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
453 resource_request->method = "POST";
454
455 std::string reporting_info_string = SerializeReportingInfo(reporting_info);
456 // If we are not using HTTPS for this upload, encrypt it. We do not encrypt
457 // requests to localhost to allow testing with a local collector that doesn't
458 // have decryption enabled.
459 bool should_encrypt =
460 !url.SchemeIs(url::kHttpsScheme) && !net::IsLocalhost(url);
461 if (should_encrypt) {
462 std::string base64_encoded_hash;
463 if (!EncryptAndBase64EncodeString(log_hash, &base64_encoded_hash)) {
464 HTTPFallbackAborted();
465 return;
466 }
467 resource_request->headers.SetHeader("X-Chrome-UMA-Log-SHA1",
468 base64_encoded_hash);
469
470 std::string base64_encoded_signature;
471 if (!EncryptAndBase64EncodeString(log_signature,
472 &base64_encoded_signature)) {
473 HTTPFallbackAborted();
474 return;
475 }
476 resource_request->headers.SetHeader("X-Chrome-UMA-Log-HMAC-SHA256",
477 base64_encoded_signature);
478
479 std::string base64_reporting_info;
480 if (!EncryptAndBase64EncodeString(reporting_info_string,
481 &base64_reporting_info)) {
482 HTTPFallbackAborted();
483 return;
484 }
485 resource_request->headers.SetHeader("X-Chrome-UMA-ReportingInfo",
486 base64_reporting_info);
487 } else {
488 resource_request->headers.SetHeader("X-Chrome-UMA-Log-SHA1", log_hash);
489 resource_request->headers.SetHeader("X-Chrome-UMA-Log-HMAC-SHA256",
490 log_signature);
491 resource_request->headers.SetHeader("X-Chrome-UMA-ReportingInfo",
492 reporting_info_string);
493 // Tell the server that we're uploading gzipped protobufs only if we are not
494 // encrypting, since encrypted messages have to be decrypted server side
495 // after decryption, not before.
496 resource_request->headers.SetHeader("content-encoding", "gzip");
497 }
498
499 net::NetworkTrafficAnnotationTag traffic_annotation =
500 GetNetworkTrafficAnnotation(service_type_, log_metadata);
501 url_loader_ = network::SimpleURLLoader::Create(std::move(resource_request),
502 traffic_annotation);
503
504 if (should_encrypt) {
505 std::string encrypted_message;
506 if (!EncryptString(compressed_log_data, &encrypted_message)) {
507 url_loader_.reset();
508 HTTPFallbackAborted();
509 return;
510 }
511 url_loader_->AttachStringForUpload(encrypted_message, mime_type_);
512 } else {
513 url_loader_->AttachStringForUpload(compressed_log_data, mime_type_);
514 }
515
516 // It's safe to use |base::Unretained(this)| here, because |this| owns
517 // the |url_loader_|, and the callback will be cancelled if the |url_loader_|
518 // is destroyed.
519 url_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
520 url_loader_factory_.get(),
521 base::BindOnce(&NetMetricsLogUploader::OnURLLoadComplete,
522 base::Unretained(this)));
523 }
524
525 void NetMetricsLogUploader::HTTPFallbackAborted() {
526 // The callback is called with: a response code of 0 to indicate no upload was
527 // attempted, a generic net error, and false to indicate it wasn't a secure
528 // connection. If no server URLs were specified, discard the log and do not
529 // attempt retransmission.
530 bool force_discard =
531 server_url_.is_empty() && insecure_server_url_.is_empty();
532 base::StringPiece force_discard_reason =
533 force_discard ? kNoUploadUrlsReasonMsg : "";
534 on_upload_complete_.Run(/*response_code=*/0, net::ERR_FAILED,
535 /*was_https=*/false, force_discard,
536 force_discard_reason);
537 }
538
539 // The callback is only invoked if |url_loader_| it was bound against is alive.
540 void NetMetricsLogUploader::OnURLLoadComplete(
541 std::unique_ptr<std::string> response_body) {
542 int response_code = -1;
543 if (url_loader_->ResponseInfo() && url_loader_->ResponseInfo()->headers) {
544 response_code = url_loader_->ResponseInfo()->headers->response_code();
545 }
546
547 int error_code = url_loader_->NetError();
548
549 bool was_https = url_loader_->GetFinalURL().SchemeIs(url::kHttpsScheme);
550 url_loader_.reset();
551
552 // If no server URLs were specified, discard the log and do not attempt
553 // retransmission.
554 bool force_discard =
555 server_url_.is_empty() && insecure_server_url_.is_empty();
556 base::StringPiece force_discard_reason =
557 force_discard ? kNoUploadUrlsReasonMsg : "";
558 on_upload_complete_.Run(response_code, error_code, was_https, force_discard,
559 force_discard_reason);
560 }
561
562 } // namespace metrics
563