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