396 lines
16 KiB
C++
396 lines
16 KiB
C++
|
|
// Copyright 2014 The Chromium Authors
|
|||
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|||
|
|
// found in the LICENSE file.
|
|||
|
|
|
|||
|
|
#include "components/metrics/net/net_metrics_log_uploader.h"
|
|||
|
|
|
|||
|
|
#include <sstream>
|
|||
|
|
|
|||
|
|
#include "base/base64.h"
|
|||
|
|
#include "base/feature_list.h"
|
|||
|
|
#include "base/functional/bind.h"
|
|||
|
|
#include "base/metrics/histogram_macros.h"
|
|||
|
|
#include "base/metrics/statistics_recorder.h"
|
|||
|
|
#include "base/strings/strcat.h"
|
|||
|
|
#include "base/strings/string_number_conversions.h"
|
|||
|
|
#include "components/encrypted_messages/encrypted_message.pb.h"
|
|||
|
|
#include "components/encrypted_messages/message_encrypter.h"
|
|||
|
|
#include "components/metrics/metrics_log_uploader.h"
|
|||
|
|
#include "net/base/load_flags.h"
|
|||
|
|
#include "net/base/url_util.h"
|
|||
|
|
#include "net/traffic_annotation/network_traffic_annotation.h"
|
|||
|
|
#include "services/network/public/cpp/resource_request.h"
|
|||
|
|
#include "services/network/public/cpp/shared_url_loader_factory.h"
|
|||
|
|
#include "services/network/public/cpp/simple_url_loader.h"
|
|||
|
|
#include "services/network/public/cpp/simple_url_loader_throttle.h"
|
|||
|
|
#include "services/network/public/mojom/url_response_head.mojom.h"
|
|||
|
|
#include "third_party/metrics_proto/chrome_user_metrics_extension.pb.h"
|
|||
|
|
#include "third_party/metrics_proto/reporting_info.pb.h"
|
|||
|
|
#include "third_party/zlib/google/compression_utils.h"
|
|||
|
|
#include "url/gurl.h"
|
|||
|
|
|
|||
|
|
namespace {
|
|||
|
|
|
|||
|
|
// Constants used for encrypting logs that are sent over HTTP. The
|
|||
|
|
// corresponding private key is used by the metrics server to decrypt logs.
|
|||
|
|
const char kEncryptedMessageLabel[] = "metrics log";
|
|||
|
|
|
|||
|
|
const uint8_t kServerPublicKey[] = {
|
|||
|
|
0x51, 0xcc, 0x52, 0x67, 0x42, 0x47, 0x3b, 0x10, 0xe8, 0x63, 0x18,
|
|||
|
|
0x3c, 0x61, 0xa7, 0x96, 0x76, 0x86, 0x91, 0x40, 0x71, 0x39, 0x5f,
|
|||
|
|
0x31, 0x1a, 0x39, 0x5b, 0x76, 0xb1, 0x6b, 0x3d, 0x6a, 0x2b};
|
|||
|
|
|
|||
|
|
const uint32_t kServerPublicKeyVersion = 1;
|
|||
|
|
|
|||
|
|
constexpr char kNoUploadUrlsReasonMsg[] =
|
|||
|
|
"No server upload URLs specified. Will not attempt to retransmit.";
|
|||
|
|
|
|||
|
|
net::NetworkTrafficAnnotationTag GetNetworkTrafficAnnotation(
|
|||
|
|
const metrics::MetricsLogUploader::MetricServiceType& service_type) {
|
|||
|
|
// The code in this function should remain so that we won't need a default
|
|||
|
|
// case that does not have meaningful annotation.
|
|||
|
|
if (service_type == metrics::MetricsLogUploader::UMA) {
|
|||
|
|
return net::DefineNetworkTrafficAnnotation("metrics_report_uma", R"(
|
|||
|
|
semantics {
|
|||
|
|
sender: "Metrics UMA Log Uploader"
|
|||
|
|
description:
|
|||
|
|
"Report of usage statistics and crash-related data about Chromium. "
|
|||
|
|
"Usage statistics contain information such as preferences, button "
|
|||
|
|
"clicks, and memory usage and do not include web page URLs or "
|
|||
|
|
"personal information. See more at "
|
|||
|
|
"https://www.google.com/chrome/browser/privacy/ under 'Usage "
|
|||
|
|
"statistics and crash reports'. Usage statistics are tied to a "
|
|||
|
|
"pseudonymous machine identifier and not to your email address."
|
|||
|
|
trigger:
|
|||
|
|
"Reports are automatically generated on startup and at intervals "
|
|||
|
|
"while Chromium is running."
|
|||
|
|
data:
|
|||
|
|
"A protocol buffer with usage statistics and crash related data."
|
|||
|
|
destination: GOOGLE_OWNED_SERVICE
|
|||
|
|
}
|
|||
|
|
policy {
|
|||
|
|
cookies_allowed: NO
|
|||
|
|
setting:
|
|||
|
|
"Users can enable or disable this feature by disabling "
|
|||
|
|
"'Automatically send usage statistics and crash reports to Google' "
|
|||
|
|
"in Chromium's settings under Advanced Settings, Privacy. The "
|
|||
|
|
"feature is enabled by default."
|
|||
|
|
chrome_policy {
|
|||
|
|
MetricsReportingEnabled {
|
|||
|
|
policy_options {mode: MANDATORY}
|
|||
|
|
MetricsReportingEnabled: false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})");
|
|||
|
|
}
|
|||
|
|
DCHECK_EQ(service_type, metrics::MetricsLogUploader::UKM);
|
|||
|
|
return net::DefineNetworkTrafficAnnotation("metrics_report_ukm", R"(
|
|||
|
|
semantics {
|
|||
|
|
sender: "Metrics UKM Log Uploader"
|
|||
|
|
description:
|
|||
|
|
"Report of usage statistics that are keyed by URLs to Chromium. This "
|
|||
|
|
"includes information about the web pages you visit and your usage "
|
|||
|
|
"of them, such as page load speed. This will also include URLs and "
|
|||
|
|
"statistics related to downloaded files. These statistics may also "
|
|||
|
|
"include information about the extensions that have been installed "
|
|||
|
|
"from Chrome Web Store. Google only stores usage statistics "
|
|||
|
|
"associated with published extensions, and URLs that are known by "
|
|||
|
|
"Google’s search index. Usage statistics are tied to a "
|
|||
|
|
"pseudonymous machine identifier and not to your email address."
|
|||
|
|
trigger:
|
|||
|
|
"Reports are automatically generated on startup and at intervals "
|
|||
|
|
"while Chromium is running with Sync enabled."
|
|||
|
|
data:
|
|||
|
|
"A protocol buffer with usage statistics and associated URLs."
|
|||
|
|
destination: GOOGLE_OWNED_SERVICE
|
|||
|
|
}
|
|||
|
|
policy {
|
|||
|
|
cookies_allowed: NO
|
|||
|
|
setting:
|
|||
|
|
"Users can enable or disable this feature by disabling 'Make "
|
|||
|
|
"searches and browsing better' in Chrome's settings under Advanced "
|
|||
|
|
"Settings, Privacy. This has to be enabled for all active profiles. "
|
|||
|
|
"This is only enabled if the user has 'Help improve Chrome's "
|
|||
|
|
"features and performance' enabled in the same settings menu. "
|
|||
|
|
"Information about the installed extensions is sent only if "
|
|||
|
|
"Extension Sync is enabled."
|
|||
|
|
chrome_policy {
|
|||
|
|
MetricsReportingEnabled {
|
|||
|
|
policy_options {mode: MANDATORY}
|
|||
|
|
MetricsReportingEnabled: false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
std::string SerializeReportingInfo(
|
|||
|
|
const metrics::ReportingInfo& reporting_info) {
|
|||
|
|
std::string result;
|
|||
|
|
std::string bytes;
|
|||
|
|
bool success = reporting_info.SerializeToString(&bytes);
|
|||
|
|
DCHECK(success);
|
|||
|
|
base::Base64Encode(bytes, &result);
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Encrypts a |plaintext| string, using the encrypted_messages component,
|
|||
|
|
// returns |encrypted| which is a serialized EncryptedMessage object. Returns
|
|||
|
|
// false if there was a problem encrypting.
|
|||
|
|
bool EncryptString(const std::string& plaintext, std::string* encrypted) {
|
|||
|
|
encrypted_messages::EncryptedMessage encrypted_message;
|
|||
|
|
if (!encrypted_messages::EncryptSerializedMessage(
|
|||
|
|
kServerPublicKey, kServerPublicKeyVersion, kEncryptedMessageLabel,
|
|||
|
|
plaintext, &encrypted_message)) {
|
|||
|
|
NOTREACHED() << "Error encrypting string.";
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
if (!encrypted_message.SerializeToString(encrypted)) {
|
|||
|
|
NOTREACHED() << "Error serializing encrypted string.";
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Encrypts a |plaintext| string and returns |encoded|, which is a base64
|
|||
|
|
// encoded serialized EncryptedMessage object. Returns false if there was a
|
|||
|
|
// problem encrypting or serializing.
|
|||
|
|
bool EncryptAndBase64EncodeString(const std::string& plaintext,
|
|||
|
|
std::string* encoded) {
|
|||
|
|
std::string encrypted_text;
|
|||
|
|
if (!EncryptString(plaintext, &encrypted_text))
|
|||
|
|
return false;
|
|||
|
|
|
|||
|
|
base::Base64Encode(encrypted_text, encoded);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#ifndef NDEBUG
|
|||
|
|
void LogUploadingHistograms(const std::string& compressed_log_data) {
|
|||
|
|
if (!VLOG_IS_ON(2))
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
std::string uncompressed;
|
|||
|
|
if (!compression::GzipUncompress(compressed_log_data, &uncompressed)) {
|
|||
|
|
DVLOG(2) << "failed to uncompress log";
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
metrics::ChromeUserMetricsExtension proto;
|
|||
|
|
if (!proto.ParseFromString(uncompressed)) {
|
|||
|
|
DVLOG(2) << "failed to parse uncompressed log";
|
|||
|
|
return;
|
|||
|
|
};
|
|||
|
|
DVLOG(2) << "Uploading histograms...";
|
|||
|
|
|
|||
|
|
const base::StatisticsRecorder::Histograms histograms =
|
|||
|
|
base::StatisticsRecorder::GetHistograms();
|
|||
|
|
auto get_histogram_name = [&](uint64_t name_hash) -> std::string {
|
|||
|
|
for (base::HistogramBase* histogram : histograms) {
|
|||
|
|
if (histogram->name_hash() == name_hash)
|
|||
|
|
return histogram->histogram_name();
|
|||
|
|
}
|
|||
|
|
return base::StrCat({"unnamed ", base::NumberToString(name_hash)});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
for (int i = 0; i < proto.histogram_event_size(); i++) {
|
|||
|
|
const metrics::HistogramEventProto& event = proto.histogram_event(i);
|
|||
|
|
|
|||
|
|
std::stringstream summary;
|
|||
|
|
summary << " sum=" << event.sum();
|
|||
|
|
for (int j = 0; j < event.bucket_size(); j++) {
|
|||
|
|
const metrics::HistogramEventProto::Bucket& b = event.bucket(j);
|
|||
|
|
// Empty fields have a specific meaning, see
|
|||
|
|
// third_party/metrics_proto/histogram_event.proto.
|
|||
|
|
summary << " bucket["
|
|||
|
|
<< (b.has_min() ? base::NumberToString(b.min()) : "..") << '-'
|
|||
|
|
<< (b.has_max() ? base::NumberToString(b.max()) : "..") << ")="
|
|||
|
|
<< (b.has_count() ? base::NumberToString(b.count()) : "(1)");
|
|||
|
|
}
|
|||
|
|
DVLOG(2) << get_histogram_name(event.name_hash()) << summary.str();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
#endif
|
|||
|
|
|
|||
|
|
} // namespace
|
|||
|
|
|
|||
|
|
namespace metrics {
|
|||
|
|
|
|||
|
|
NetMetricsLogUploader::NetMetricsLogUploader(
|
|||
|
|
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
|
|||
|
|
const GURL& server_url,
|
|||
|
|
base::StringPiece mime_type,
|
|||
|
|
MetricsLogUploader::MetricServiceType service_type,
|
|||
|
|
const MetricsLogUploader::UploadCallback& on_upload_complete)
|
|||
|
|
: NetMetricsLogUploader(url_loader_factory,
|
|||
|
|
server_url,
|
|||
|
|
/*insecure_server_url=*/GURL(),
|
|||
|
|
mime_type,
|
|||
|
|
service_type,
|
|||
|
|
on_upload_complete) {}
|
|||
|
|
|
|||
|
|
NetMetricsLogUploader::NetMetricsLogUploader(
|
|||
|
|
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
|
|||
|
|
const GURL& server_url,
|
|||
|
|
const GURL& insecure_server_url,
|
|||
|
|
base::StringPiece mime_type,
|
|||
|
|
MetricsLogUploader::MetricServiceType service_type,
|
|||
|
|
const MetricsLogUploader::UploadCallback& on_upload_complete)
|
|||
|
|
: url_loader_factory_(std::move(url_loader_factory)),
|
|||
|
|
server_url_(server_url),
|
|||
|
|
insecure_server_url_(insecure_server_url),
|
|||
|
|
mime_type_(mime_type.data(), mime_type.size()),
|
|||
|
|
service_type_(service_type),
|
|||
|
|
on_upload_complete_(on_upload_complete) {}
|
|||
|
|
|
|||
|
|
NetMetricsLogUploader::~NetMetricsLogUploader() = default;
|
|||
|
|
|
|||
|
|
void NetMetricsLogUploader::UploadLog(const std::string& compressed_log_data,
|
|||
|
|
const std::string& log_hash,
|
|||
|
|
const std::string& log_signature,
|
|||
|
|
const ReportingInfo& reporting_info) {
|
|||
|
|
// If this attempt is a retry, there was a network error, the last attempt was
|
|||
|
|
// over HTTPS, and there is an insecure URL set, then attempt this upload over
|
|||
|
|
// HTTP.
|
|||
|
|
if (reporting_info.attempt_count() > 1 &&
|
|||
|
|
reporting_info.last_error_code() != 0 &&
|
|||
|
|
reporting_info.last_attempt_was_https() &&
|
|||
|
|
!insecure_server_url_.is_empty()) {
|
|||
|
|
UploadLogToURL(compressed_log_data, log_hash, log_signature, reporting_info,
|
|||
|
|
insecure_server_url_);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
UploadLogToURL(compressed_log_data, log_hash, log_signature, reporting_info,
|
|||
|
|
server_url_);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void NetMetricsLogUploader::UploadLogToURL(
|
|||
|
|
const std::string& compressed_log_data,
|
|||
|
|
const std::string& log_hash,
|
|||
|
|
const std::string& log_signature,
|
|||
|
|
const ReportingInfo& reporting_info,
|
|||
|
|
const GURL& url) {
|
|||
|
|
DCHECK(!log_hash.empty());
|
|||
|
|
|
|||
|
|
#ifndef NDEBUG
|
|||
|
|
// For debug builds, you can use -vmodule=net_metrics_log_uploader=2
|
|||
|
|
// to enable logging of uploaded histograms. You probably also want to use
|
|||
|
|
// --force-enable-metrics-reporting, or metrics reporting may not be enabled.
|
|||
|
|
LogUploadingHistograms(compressed_log_data);
|
|||
|
|
#endif
|
|||
|
|
|
|||
|
|
auto resource_request = std::make_unique<network::ResourceRequest>();
|
|||
|
|
resource_request->url = url;
|
|||
|
|
// Drop cookies and auth data.
|
|||
|
|
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
|
|||
|
|
resource_request->method = "POST";
|
|||
|
|
|
|||
|
|
std::string reporting_info_string = SerializeReportingInfo(reporting_info);
|
|||
|
|
// If we are not using HTTPS for this upload, encrypt it. We do not encrypt
|
|||
|
|
// requests to localhost to allow testing with a local collector that doesn't
|
|||
|
|
// have decryption enabled.
|
|||
|
|
bool should_encrypt =
|
|||
|
|
!url.SchemeIs(url::kHttpsScheme) && !net::IsLocalhost(url);
|
|||
|
|
if (should_encrypt) {
|
|||
|
|
std::string base64_encoded_hash;
|
|||
|
|
if (!EncryptAndBase64EncodeString(log_hash, &base64_encoded_hash)) {
|
|||
|
|
HTTPFallbackAborted();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
resource_request->headers.SetHeader("X-Chrome-UMA-Log-SHA1",
|
|||
|
|
base64_encoded_hash);
|
|||
|
|
|
|||
|
|
std::string base64_encoded_signature;
|
|||
|
|
if (!EncryptAndBase64EncodeString(log_signature,
|
|||
|
|
&base64_encoded_signature)) {
|
|||
|
|
HTTPFallbackAborted();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
resource_request->headers.SetHeader("X-Chrome-UMA-Log-HMAC-SHA256",
|
|||
|
|
base64_encoded_signature);
|
|||
|
|
|
|||
|
|
std::string base64_reporting_info;
|
|||
|
|
if (!EncryptAndBase64EncodeString(reporting_info_string,
|
|||
|
|
&base64_reporting_info)) {
|
|||
|
|
HTTPFallbackAborted();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
resource_request->headers.SetHeader("X-Chrome-UMA-ReportingInfo",
|
|||
|
|
base64_reporting_info);
|
|||
|
|
} else {
|
|||
|
|
resource_request->headers.SetHeader("X-Chrome-UMA-Log-SHA1", log_hash);
|
|||
|
|
resource_request->headers.SetHeader("X-Chrome-UMA-Log-HMAC-SHA256",
|
|||
|
|
log_signature);
|
|||
|
|
resource_request->headers.SetHeader("X-Chrome-UMA-ReportingInfo",
|
|||
|
|
reporting_info_string);
|
|||
|
|
// Tell the server that we're uploading gzipped protobufs only if we are not
|
|||
|
|
// encrypting, since encrypted messages have to be decrypted server side
|
|||
|
|
// after decryption, not before.
|
|||
|
|
resource_request->headers.SetHeader("content-encoding", "gzip");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
net::NetworkTrafficAnnotationTag traffic_annotation =
|
|||
|
|
GetNetworkTrafficAnnotation(service_type_);
|
|||
|
|
url_loader_ = network::SimpleURLLoader::Create(std::move(resource_request),
|
|||
|
|
traffic_annotation);
|
|||
|
|
|
|||
|
|
if (network::SimpleURLLoaderThrottle::IsBatchingEnabled(traffic_annotation))
|
|||
|
|
url_loader_->SetAllowBatching();
|
|||
|
|
|
|||
|
|
if (should_encrypt) {
|
|||
|
|
std::string encrypted_message;
|
|||
|
|
if (!EncryptString(compressed_log_data, &encrypted_message)) {
|
|||
|
|
url_loader_.reset();
|
|||
|
|
HTTPFallbackAborted();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
url_loader_->AttachStringForUpload(encrypted_message, mime_type_);
|
|||
|
|
} else {
|
|||
|
|
url_loader_->AttachStringForUpload(compressed_log_data, mime_type_);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// It's safe to use |base::Unretained(this)| here, because |this| owns
|
|||
|
|
// the |url_loader_|, and the callback will be cancelled if the |url_loader_|
|
|||
|
|
// is destroyed.
|
|||
|
|
url_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
|
|||
|
|
url_loader_factory_.get(),
|
|||
|
|
base::BindOnce(&NetMetricsLogUploader::OnURLLoadComplete,
|
|||
|
|
base::Unretained(this)));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void NetMetricsLogUploader::HTTPFallbackAborted() {
|
|||
|
|
// The callback is called with: a response code of 0 to indicate no upload was
|
|||
|
|
// attempted, a generic net error, and false to indicate it wasn't a secure
|
|||
|
|
// connection. If no server URLs were specified, discard the log and do not
|
|||
|
|
// attempt retransmission.
|
|||
|
|
bool force_discard =
|
|||
|
|
server_url_.is_empty() && insecure_server_url_.is_empty();
|
|||
|
|
base::StringPiece force_discard_reason =
|
|||
|
|
force_discard ? kNoUploadUrlsReasonMsg : "";
|
|||
|
|
on_upload_complete_.Run(/*response_code=*/0, net::ERR_FAILED,
|
|||
|
|
/*was_https=*/false, force_discard,
|
|||
|
|
force_discard_reason);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// The callback is only invoked if |url_loader_| it was bound against is alive.
|
|||
|
|
void NetMetricsLogUploader::OnURLLoadComplete(
|
|||
|
|
std::unique_ptr<std::string> response_body) {
|
|||
|
|
int response_code = -1;
|
|||
|
|
if (url_loader_->ResponseInfo() && url_loader_->ResponseInfo()->headers)
|
|||
|
|
response_code = url_loader_->ResponseInfo()->headers->response_code();
|
|||
|
|
|
|||
|
|
int error_code = url_loader_->NetError();
|
|||
|
|
|
|||
|
|
bool was_https = url_loader_->GetFinalURL().SchemeIs(url::kHttpsScheme);
|
|||
|
|
url_loader_.reset();
|
|||
|
|
|
|||
|
|
// If no server URLs were specified, discard the log and do not attempt
|
|||
|
|
// retransmission.
|
|||
|
|
bool force_discard =
|
|||
|
|
server_url_.is_empty() && insecure_server_url_.is_empty();
|
|||
|
|
base::StringPiece force_discard_reason =
|
|||
|
|
force_discard ? kNoUploadUrlsReasonMsg : "";
|
|||
|
|
on_upload_complete_.Run(response_code, error_code, was_https, force_discard,
|
|||
|
|
force_discard_reason);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} // namespace metrics
|