xref: /aosp_15_r20/external/cronet/components/metrics/net/net_metrics_log_uploader.cc (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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