blob: 47a8cc6df142ff58fa0d727946ce7a4871498902 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/data_reduction_proxy/core/browser/data_reduction_proxy_config_service_client.h"
#include <string>
#include <utility>
#include <vector>
#include "base/base64.h"
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/json/json_writer.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_number_conversions.h"
#include "base/system/sys_info.h"
#include "base/values.h"
#include "build/build_config.h"
#include "components/data_reduction_proxy/core/browser/data_reduction_proxy_config.h"
#include "components/data_reduction_proxy/core/browser/data_reduction_proxy_io_data.h"
#include "components/data_reduction_proxy/core/browser/data_reduction_proxy_mutable_config_values.h"
#include "components/data_reduction_proxy/core/browser/data_reduction_proxy_request_options.h"
#include "components/data_reduction_proxy/core/browser/data_reduction_proxy_util.h"
#include "components/data_reduction_proxy/core/common/data_reduction_proxy_features.h"
#include "components/data_reduction_proxy/core/common/data_reduction_proxy_params.h"
#include "components/data_reduction_proxy/core/common/data_reduction_proxy_server.h"
#include "components/data_reduction_proxy/core/common/data_reduction_proxy_switches.h"
#include "components/data_reduction_proxy/proto/client_config.pb.h"
#include "components/data_use_measurement/core/data_use_user_data.h"
#include "components/variations/net/variations_http_headers.h"
#include "net/base/host_port_pair.h"
#include "net/base/load_flags.h"
#include "net/base/load_timing_info.h"
#include "net/base/net_errors.h"
#include "net/base/proxy_server.h"
#include "net/http/http_network_session.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#if defined(OS_ANDROID)
#include "net/android/network_library.h"
#endif
namespace data_reduction_proxy {
namespace {
// Key of the UMA DataReductionProxy.ConfigService.FetchResponseCode histogram.
const char kUMAConfigServiceFetchResponseCode[] =
"DataReductionProxy.ConfigService.FetchResponseCode";
// Key of the UMA
// DataReductionProxy.ConfigService.FetchFailedAttemptsBeforeSuccess histogram.
const char kUMAConfigServiceFetchFailedAttemptsBeforeSuccess[] =
"DataReductionProxy.ConfigService.FetchFailedAttemptsBeforeSuccess";
// Key of the UMA DataReductionProxy.ConfigService.FetchLatency histogram.
const char kUMAConfigServiceFetchLatency[] =
"DataReductionProxy.ConfigService.FetchLatency";
// Key of the UMA DataReductionProxy.ConfigService.AuthExpired histogram.
const char kUMAConfigServiceAuthExpired[] =
"DataReductionProxy.ConfigService.AuthExpired";
#if defined(OS_ANDROID)
// Maximum duration to wait before fetching the config, while the application
// is in background.
const uint32_t kMaxBackgroundFetchIntervalSeconds = 6 * 60 * 60; // 6 hours.
#endif
// This is the default backoff policy used to communicate with the Data
// Reduction Proxy configuration service. The policy gets overwritten based on
// kDataReductionProxyAggressiveConfigFetch.
const net::BackoffEntry::Policy kDefaultBackoffPolicy = {
0, // num_errors_to_ignore
10 * 1000, // initial_delay_ms
3, // multiply_factor
0.25, // jitter_factor,
128 * 60 * 1000, // maximum_backoff_ms
-1, // entry_lifetime_ms
true, // always_use_initial_delay
};
// Extracts the list of Data Reduction Proxy servers to use for HTTP requests.
std::vector<DataReductionProxyServer> GetProxiesForHTTP(
const data_reduction_proxy::ProxyConfig& proxy_config) {
std::vector<DataReductionProxyServer> proxies;
for (const auto& server : proxy_config.http_proxy_servers()) {
if (server.scheme() != ProxyServer_ProxyScheme_UNSPECIFIED) {
proxies.push_back(DataReductionProxyServer(net::ProxyServer(
protobuf_parser::SchemeFromProxyScheme(server.scheme()),
net::HostPortPair(server.host(), server.port()),
/* HTTPS proxies are marked as trusted. */
server.scheme() == ProxyServer_ProxyScheme_HTTPS)));
}
}
return proxies;
}
void RecordAuthExpiredHistogram(bool auth_expired) {
UMA_HISTOGRAM_BOOLEAN(kUMAConfigServiceAuthExpired, auth_expired);
}
// Records whether the session key used in the request matches the current
// sesssion key.
void RecordAuthExpiredSessionKey(bool matches) {
// This enum must remain synchronized with the
// DataReductionProxyConfigServiceAuthExpiredSessionKey enum in
// metrics/histograms/histograms.xml.
enum AuthExpiredSessionKey {
AUTH_EXPIRED_SESSION_KEY_MISMATCH = 0,
AUTH_EXPIRED_SESSION_KEY_MATCH = 1,
AUTH_EXPIRED_SESSION_KEY_BOUNDARY = 2
};
AuthExpiredSessionKey state = matches ? AUTH_EXPIRED_SESSION_KEY_MATCH
: AUTH_EXPIRED_SESSION_KEY_MISMATCH;
UMA_HISTOGRAM_ENUMERATION(
"DataReductionProxy.ConfigService.AuthExpiredSessionKey", state,
AUTH_EXPIRED_SESSION_KEY_BOUNDARY);
}
} // namespace
net::BackoffEntry::Policy GetBackoffPolicy() {
net::BackoffEntry::Policy policy = kDefaultBackoffPolicy;
if (base::FeatureList::IsEnabled(
features::kDataReductionProxyAggressiveConfigFetch)) {
// Disabling always_use_initial_delay allows no backoffs until
// num_errors_to_ignore failures have occurred.
policy.num_errors_to_ignore = 2;
policy.always_use_initial_delay = false;
}
return policy;
}
DataReductionProxyConfigServiceClient::DataReductionProxyConfigServiceClient(
const net::BackoffEntry::Policy& backoff_policy,
DataReductionProxyRequestOptions* request_options,
DataReductionProxyMutableConfigValues* config_values,
DataReductionProxyConfig* config,
DataReductionProxyIOData* io_data,
network::NetworkConnectionTracker* network_connection_tracker,
ConfigStorer config_storer)
: request_options_(request_options),
config_values_(config_values),
config_(config),
io_data_(io_data),
network_connection_tracker_(network_connection_tracker),
config_storer_(config_storer),
backoff_policy_(backoff_policy),
backoff_entry_(&backoff_policy_),
config_service_url_(util::AddApiKeyToUrl(params::GetConfigServiceURL())),
enabled_(false),
remote_config_applied_(false),
#if defined(OS_ANDROID)
foreground_fetch_pending_(false),
#endif
previous_request_failed_authentication_(false),
failed_attempts_before_success_(0),
fetch_in_progress_(false),
client_config_override_used_(false) {
DCHECK(request_options);
DCHECK(config_values);
DCHECK(config);
DCHECK(io_data);
DCHECK(config_service_url_.is_valid());
DCHECK(!params::IsIncludedInHoldbackFieldTrial());
const base::CommandLine& command_line =
*base::CommandLine::ForCurrentProcess();
client_config_override_ = command_line.GetSwitchValueASCII(
switches::kDataReductionProxyServerClientConfig);
// Constructed on the UI thread, but should be checked on the IO thread.
thread_checker_.DetachFromThread();
}
DataReductionProxyConfigServiceClient::
~DataReductionProxyConfigServiceClient() {
network_connection_tracker_->RemoveNetworkConnectionObserver(this);
}
base::TimeDelta
DataReductionProxyConfigServiceClient::CalculateNextConfigRefreshTime(
bool fetch_succeeded,
const base::TimeDelta& config_expiration_delta,
const base::TimeDelta& backoff_delay) {
DCHECK(backoff_delay >= base::TimeDelta());
#if defined(OS_ANDROID)
foreground_fetch_pending_ = false;
if (!fetch_succeeded &&
!base::FeatureList::IsEnabled(
features::kDataReductionProxyAggressiveConfigFetch) &&
IsApplicationStateBackground()) {
// If Chromium is in background, then fetch the config when Chromium comes
// to foreground or after max of |kMaxBackgroundFetchIntervalSeconds| and
// |backoff_delay|.
foreground_fetch_pending_ = true;
return std::max(backoff_delay, base::TimeDelta::FromSeconds(
kMaxBackgroundFetchIntervalSeconds));
}
#endif
if (fetch_succeeded) {
return std::max(backoff_delay, config_expiration_delta);
}
return backoff_delay;
}
void DataReductionProxyConfigServiceClient::InitializeOnIOThread(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) {
DCHECK(url_loader_factory);
#if defined(OS_ANDROID)
// It is okay to use Unretained here because |app_status_listener| would be
// destroyed before |this|.
app_status_listener_ =
base::android::ApplicationStatusListener::New(base::BindRepeating(
&DataReductionProxyConfigServiceClient::OnApplicationStateChange,
base::Unretained(this)));
#endif
url_loader_factory_ = std::move(url_loader_factory);
network_connection_tracker_->AddNetworkConnectionObserver(this);
}
void DataReductionProxyConfigServiceClient::SetEnabled(bool enabled) {
DCHECK(thread_checker_.CalledOnValidThread());
enabled_ = enabled;
}
void DataReductionProxyConfigServiceClient::RetrieveConfig() {
DCHECK(thread_checker_.CalledOnValidThread());
if (!enabled_)
return;
if (!client_config_override_.empty()) {
// Return fast if the override has already been attempted.
if (client_config_override_used_) {
return;
}
// Set this flag so that we only attempt to apply the given config once. If
// there are parse errors, the DCHECKs will catch them in a debug build.
client_config_override_used_ = true;
std::string override_config;
bool b64_decode_ok =
base::Base64Decode(client_config_override_, &override_config);
LOG_IF(DFATAL, !b64_decode_ok)
<< "The given ClientConfig is not valid base64";
ClientConfig config;
bool was_valid_config = config.ParseFromString(override_config);
LOG_IF(DFATAL, !was_valid_config) << "The given ClientConfig was invalid.";
if (was_valid_config)
ParseAndApplyProxyConfig(config);
return;
}
config_fetch_start_time_ = base::TimeTicks::Now();
RetrieveRemoteConfig();
}
bool DataReductionProxyConfigServiceClient::RemoteConfigApplied() const {
DCHECK(thread_checker_.CalledOnValidThread());
return remote_config_applied_;
}
void DataReductionProxyConfigServiceClient::ApplySerializedConfig(
const std::string& config_value) {
DCHECK(thread_checker_.CalledOnValidThread());
if (RemoteConfigApplied())
return;
if (!client_config_override_.empty())
return;
std::string decoded_config;
if (base::Base64Decode(config_value, &decoded_config)) {
ClientConfig config;
if (config.ParseFromString(decoded_config))
ParseAndApplyProxyConfig(config);
}
}
bool DataReductionProxyConfigServiceClient::ShouldRetryDueToAuthFailure(
const net::HttpRequestHeaders& request_headers,
const net::HttpResponseHeaders* response_headers,
const net::ProxyServer& proxy_server,
const net::LoadTimingInfo& load_timing_info) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(response_headers);
if (!config_->FindConfiguredDataReductionProxy(proxy_server))
return false;
if (response_headers->response_code() !=
net::HTTP_PROXY_AUTHENTICATION_REQUIRED) {
previous_request_failed_authentication_ = false;
return false;
}
// If the session key used in the request is different from the current
// session key, then the current session key does not need to be
// invalidated.
base::Optional<std::string> session_key =
request_options_->GetSessionKeyFromRequestHeaders(request_headers);
if ((session_key.has_value() ? session_key.value() : std::string()) !=
request_options_->GetSecureSession()) {
RecordAuthExpiredSessionKey(false);
return true;
}
RecordAuthExpiredSessionKey(true);
// The default backoff logic is to increment the failure count (and
// increase the backoff time) with each response failure to the remote
// config service, and to decrement the failure count (and decrease the
// backoff time) with each response success. In the case where the
// config service returns a success response (decrementing the failure
// count) but the session key is continually invalid (as a response from
// the Data Reduction Proxy and not the config service), the previous
// response should be considered a failure in order to ensure the backoff
// time continues to increase.
if (previous_request_failed_authentication_)
GetBackoffEntry()->InformOfRequest(false);
// Record that a request resulted in an authentication failure.
RecordAuthExpiredHistogram(true);
previous_request_failed_authentication_ = true;
InvalidateConfig();
DCHECK(config_->GetProxiesForHttp().empty());
if (fetch_in_progress_) {
// If a client config fetch is already in progress, then do not start
// another fetch since starting a new fetch will cause extra data
// usage, and also cancel the ongoing fetch.
return true;
}
RetrieveConfig();
if (!load_timing_info.send_start.is_null() &&
!load_timing_info.request_start.is_null() &&
!network_connection_tracker_->IsOffline() &&
last_ip_address_change_ < load_timing_info.request_start) {
// Record only if there was no change in the IP address since the
// request started.
UMA_HISTOGRAM_TIMES(
"DataReductionProxy.ConfigService.AuthFailure.LatencyPenalty",
base::TimeTicks::Now() - load_timing_info.request_start);
}
return true;
}
net::BackoffEntry* DataReductionProxyConfigServiceClient::GetBackoffEntry() {
DCHECK(thread_checker_.CalledOnValidThread());
return &backoff_entry_;
}
void DataReductionProxyConfigServiceClient::SetConfigRefreshTimer(
const base::TimeDelta& delay) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(delay >= base::TimeDelta());
config_refresh_timer_.Stop();
config_refresh_timer_.Start(
FROM_HERE, delay, this,
&DataReductionProxyConfigServiceClient::RetrieveConfig);
}
base::Time DataReductionProxyConfigServiceClient::Now() {
return base::Time::Now();
}
void DataReductionProxyConfigServiceClient::OnConnectionChanged(
network::mojom::ConnectionType type) {
DCHECK(thread_checker_.CalledOnValidThread());
if (type == network::mojom::ConnectionType::CONNECTION_NONE)
return;
GetBackoffEntry()->Reset();
last_ip_address_change_ = base::TimeTicks::Now();
failed_attempts_before_success_ = 0;
RetrieveConfig();
}
void DataReductionProxyConfigServiceClient::OnURLLoadComplete(
std::unique_ptr<std::string> response_body) {
DCHECK(thread_checker_.CalledOnValidThread());
fetch_in_progress_ = false;
int response_code = -1;
if (url_loader_->ResponseInfo() && url_loader_->ResponseInfo()->headers) {
response_code = url_loader_->ResponseInfo()->headers->response_code();
}
HandleResponse(response_body ? *response_body : "", url_loader_->NetError(),
response_code);
}
void DataReductionProxyConfigServiceClient::RetrieveRemoteConfig() {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(!params::IsIncludedInHoldbackFieldTrial());
CreateClientConfigRequest request;
std::string serialized_request;
#if defined(OS_ANDROID)
request.set_telephony_network_operator(
net::android::GetTelephonyNetworkOperator());
#endif
data_reduction_proxy::ConfigDeviceInfo* device_info =
request.mutable_device_info();
device_info->set_total_device_memory_kb(
base::SysInfo::AmountOfPhysicalMemory() / 1024);
const std::string& session_key = request_options_->GetSecureSession();
if (!session_key.empty())
request.set_session_key(request_options_->GetSecureSession());
request.set_dogfood_group(
base::FeatureList::IsEnabled(features::kDogfood)
? CreateClientConfigRequest_DogfoodGroup_DOGFOOD
: CreateClientConfigRequest_DogfoodGroup_NONDOGFOOD);
data_reduction_proxy::VersionInfo* version_info =
request.mutable_version_info();
uint32_t build;
uint32_t patch;
util::GetChromiumBuildAndPatchAsInts(util::ChromiumVersion(), &build, &patch);
version_info->set_client(util::GetStringForClient(io_data_->client()));
version_info->set_build(build);
version_info->set_patch(patch);
version_info->set_channel(io_data_->channel());
request.SerializeToString(&serialized_request);
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("data_reduction_proxy_config", R"(
semantics {
sender: "Data Reduction Proxy"
description:
"Requests a configuration that specifies how to connect to the "
"data reduction proxy."
trigger:
"Requested when Data Saver is enabled and the browser does not "
"have a configuration that is not older than a threshold set by "
"the server."
data: "None."
destination: GOOGLE_OWNED_SERVICE
}
policy {
cookies_allowed: NO
setting:
"Users can control Data Saver on Android via 'Data Saver' setting. "
"Data Saver is not available on iOS, and on desktop it is enabled "
"by insalling the Data Saver extension."
policy_exception_justification: "Not implemented."
})");
fetch_in_progress_ = true;
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = config_service_url_;
resource_request->method = "POST";
resource_request->load_flags = net::LOAD_BYPASS_PROXY;
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
// Attach variations headers.
url_loader_ = variations::CreateSimpleURLLoaderWithVariationsHeader(
std::move(resource_request), variations::InIncognito::kNo,
variations::SignedIn::kNo, traffic_annotation);
url_loader_->AttachStringForUpload(serialized_request,
"application/x-protobuf");
// |url_loader_| should not retry on 5xx errors since the server may already
// be overloaded. Spurious 5xx errors are still retried on exponential
// backoff. |url_loader_| should retry on network changes since the network
// stack may receive the connection change event later than |this|.
static const int kMaxRetries = 5;
url_loader_->SetRetryOptions(
kMaxRetries, network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE);
url_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
url_loader_factory_.get(),
base::BindOnce(&DataReductionProxyConfigServiceClient::OnURLLoadComplete,
base::Unretained(this)));
}
void DataReductionProxyConfigServiceClient::InvalidateConfig() {
DCHECK(thread_checker_.CalledOnValidThread());
GetBackoffEntry()->InformOfRequest(false);
config_storer_.Run(std::string());
request_options_->Invalidate();
config_values_->Invalidate();
io_data_->SetPingbackReportingFraction(0.0f);
config_->OnNewClientConfigFetched();
}
void DataReductionProxyConfigServiceClient::HandleResponse(
const std::string& config_data,
int status,
int response_code) {
DCHECK(thread_checker_.CalledOnValidThread());
ClientConfig config;
bool succeeded = false;
if (!network_connection_tracker_->IsOffline())
base::UmaHistogramSparse(kUMAConfigServiceFetchResponseCode, response_code);
if (status == net::OK && response_code == net::HTTP_OK &&
config.ParseFromString(config_data)) {
succeeded = ParseAndApplyProxyConfig(config);
}
// These are proxies listed in the config. The proxies that client eventually
// ends up using depend on the field trials.
std::vector<DataReductionProxyServer> proxies;
base::TimeDelta refresh_duration;
if (succeeded) {
proxies = GetProxiesForHTTP(config.proxy_config());
refresh_duration =
protobuf_parser::DurationToTimeDelta(config.refresh_duration());
DCHECK(!config_fetch_start_time_.is_null());
base::TimeDelta configuration_fetch_latency =
base::TimeTicks::Now() - config_fetch_start_time_;
RecordAuthExpiredHistogram(false);
UMA_HISTOGRAM_MEDIUM_TIMES(kUMAConfigServiceFetchLatency,
configuration_fetch_latency);
UMA_HISTOGRAM_COUNTS_100(kUMAConfigServiceFetchFailedAttemptsBeforeSuccess,
failed_attempts_before_success_);
failed_attempts_before_success_ = 0;
std::string encoded_config;
base::Base64Encode(config_data, &encoded_config);
config_storer_.Run(encoded_config);
// Record timing metrics on successful requests only.
const network::ResourceResponseHead* info = url_loader_->ResponseInfo();
base::TimeDelta http_request_rtt =
info->response_start - info->request_start;
UMA_HISTOGRAM_TIMES("DataReductionProxy.ConfigService.HttpRequestRTT",
http_request_rtt);
if (info->load_timing.connect_timing.connect_end > base::TimeTicks() &&
info->load_timing.connect_timing.connect_start > base::TimeTicks()) {
base::TimeDelta connection_setup =
info->load_timing.connect_timing.connect_end -
info->load_timing.connect_timing.connect_start;
UMA_HISTOGRAM_TIMES(
"DataReductionProxy.ConfigService.ConnectionSetupTime",
connection_setup);
}
} else {
++failed_attempts_before_success_;
}
GetBackoffEntry()->InformOfRequest(succeeded);
base::TimeDelta next_config_refresh_time = CalculateNextConfigRefreshTime(
succeeded, refresh_duration, GetBackoffEntry()->GetTimeUntilRelease());
SetConfigRefreshTimer(next_config_refresh_time);
}
bool DataReductionProxyConfigServiceClient::ParseAndApplyProxyConfig(
const ClientConfig& config) {
DCHECK(thread_checker_.CalledOnValidThread());
float reporting_fraction = 0.0f;
if (config.has_pageload_metrics_config() &&
config.pageload_metrics_config().has_reporting_fraction()) {
reporting_fraction = config.pageload_metrics_config().reporting_fraction();
}
DCHECK_LE(0.0f, reporting_fraction);
DCHECK_GE(1.0f, reporting_fraction);
io_data_->SetPingbackReportingFraction(reporting_fraction);
if (!config.has_proxy_config())
return false;
io_data_->SetIgnoreLongTermBlackListRules(
config.ignore_long_term_black_list_rules());
// An empty proxy config is OK, and allows the server to effectively turn off
// DataSaver if needed. See http://crbug.com/840978.
std::vector<DataReductionProxyServer> proxies =
GetProxiesForHTTP(config.proxy_config());
request_options_->SetSecureSession(config.session_key());
config_values_->UpdateValues(proxies);
config_->OnNewClientConfigFetched();
remote_config_applied_ = true;
return true;
}
#if defined(OS_ANDROID)
bool DataReductionProxyConfigServiceClient::IsApplicationStateBackground()
const {
return base::android::ApplicationStatusListener::GetState() !=
base::android::APPLICATION_STATE_HAS_RUNNING_ACTIVITIES;
}
void DataReductionProxyConfigServiceClient::OnApplicationStateChange(
base::android::ApplicationState new_state) {
DCHECK(thread_checker_.CalledOnValidThread());
if (new_state == base::android::APPLICATION_STATE_HAS_RUNNING_ACTIVITIES &&
foreground_fetch_pending_) {
foreground_fetch_pending_ = false;
RetrieveConfig();
}
}
#endif
} // namespace data_reduction_proxy