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