blob: 1bb44aabb919f25ae6f104809f74fd173a0a135e [file] [log] [blame]
// Copyright 2012 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/sync/engine/net/http_bridge.h"
#include <stddef.h>
#include <utility>
#include <vector>
#include "base/bit_cast.h"
#include "base/location.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/task/post_task.h"
#include "components/sync/base/cancelation_signal.h"
#include "net/base/load_flags.h"
#include "net/base/net_errors.h"
#include "net/http/http_cache.h"
#include "net/http/http_network_layer.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "net/url_request/static_http_user_agent_settings.h"
#include "net/url_request/url_request_status.h"
#include "services/network/public/cpp/resource_response.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "third_party/zlib/google/compression_utils.h"
namespace syncer {
namespace {
// It's possible for an http request to be silently stalled. We set a time
// limit for all http requests, beyond which the request is cancelled and
// treated as a transient failure.
const int kMaxHttpRequestTimeSeconds = 60 * 5; // 5 minutes.
// Helper method for logging timeouts via UMA.
void LogTimeout(bool timed_out) {
UMA_HISTOGRAM_BOOLEAN("Sync.URLFetchTimedOut", timed_out);
}
void RecordSyncRequestContentLengthHistograms(int64_t compressed_content_length,
int64_t original_content_length) {
UMA_HISTOGRAM_COUNTS_1M("Sync.RequestContentLength.Compressed",
compressed_content_length);
UMA_HISTOGRAM_COUNTS_1M("Sync.RequestContentLength.Original",
original_content_length);
}
void RecordSyncResponseContentLengthHistograms(
int64_t compressed_content_length,
int64_t original_content_length) {
UMA_HISTOGRAM_COUNTS_1M("Sync.ResponseContentLength.Compressed",
compressed_content_length);
UMA_HISTOGRAM_COUNTS_1M("Sync.ResponseContentLength.Original",
original_content_length);
}
base::LazyInstance<scoped_refptr<base::SequencedTaskRunner>>::Leaky
g_io_capable_task_runner_for_tests = LAZY_INSTANCE_INITIALIZER;
} // namespace
HttpBridgeFactory::HttpBridgeFactory(
std::unique_ptr<network::SharedURLLoaderFactoryInfo>
url_loader_factory_info,
const NetworkTimeUpdateCallback& network_time_update_callback,
CancelationSignal* cancelation_signal)
: network_time_update_callback_(network_time_update_callback),
cancelation_signal_(cancelation_signal) {
// Some tests pass null'ed out url_loader_factory_info instances.
if (url_loader_factory_info) {
url_loader_factory_ = network::SharedURLLoaderFactory::Create(
std::move(url_loader_factory_info));
}
// This registration is happening on the Sync thread, while signalling occurs
// on the UI thread. We must handle the possibility signalling has already
// occurred.
if (cancelation_signal_->TryRegisterHandler(this)) {
registered_for_cancelation_ = true;
} else {
OnSignalReceived();
}
}
HttpBridgeFactory::~HttpBridgeFactory() {
if (registered_for_cancelation_) {
cancelation_signal_->UnregisterHandler(this);
}
}
void HttpBridgeFactory::Init(
const std::string& user_agent,
const BindToTrackerCallback& bind_to_tracker_callback) {
user_agent_ = user_agent;
bind_to_tracker_callback_ = bind_to_tracker_callback;
}
HttpPostProviderInterface* HttpBridgeFactory::Create() {
DCHECK(url_loader_factory_);
scoped_refptr<HttpBridge> http =
new HttpBridge(user_agent_, url_loader_factory_->Clone(),
network_time_update_callback_, bind_to_tracker_callback_);
http->AddRef();
return http.get();
}
void HttpBridgeFactory::Destroy(HttpPostProviderInterface* http) {
static_cast<HttpBridge*>(http)->Release();
}
void HttpBridgeFactory::OnSignalReceived() {
// TODO(tonikitoo): Remove this method.
//
// Prior to the URLLoader conversion the sync code was holding on to a
// scoped_refptr of a URLRequestContextGetter it was changing the lifetime
// of net objects. With URLLoader, it's only holding on to mojo pipes
// that issue doesn't exist.
NOTIMPLEMENTED();
}
HttpBridge::URLFetchState::URLFetchState()
: aborted(false),
request_completed(false),
request_succeeded(false),
http_status_code(-1),
net_error_code(-1) {}
HttpBridge::URLFetchState::~URLFetchState() {}
HttpBridge::HttpBridge(
const std::string& user_agent,
std::unique_ptr<network::SharedURLLoaderFactoryInfo>
url_loader_factory_info,
const NetworkTimeUpdateCallback& network_time_update_callback,
const BindToTrackerCallback& bind_to_tracker_callback)
: user_agent_(user_agent),
http_post_completed_(base::WaitableEvent::ResetPolicy::AUTOMATIC,
base::WaitableEvent::InitialState::NOT_SIGNALED),
url_loader_factory_info_(std::move(url_loader_factory_info)),
network_task_runner_(
g_io_capable_task_runner_for_tests.Get()
? g_io_capable_task_runner_for_tests.Get()
: base::CreateSequencedTaskRunnerWithTraits({base::MayBlock()})),
network_time_update_callback_(network_time_update_callback),
bind_to_tracker_callback_(bind_to_tracker_callback) {}
HttpBridge::~HttpBridge() {}
void HttpBridge::SetExtraRequestHeaders(const char* headers) {
DCHECK(extra_headers_.empty())
<< "HttpBridge::SetExtraRequestHeaders called twice.";
extra_headers_.assign(headers);
}
void HttpBridge::SetURL(const char* url, int port) {
#if DCHECK_IS_ON()
DCHECK(thread_checker_.CalledOnValidThread());
{
base::AutoLock lock(fetch_state_lock_);
DCHECK(!fetch_state_.request_completed);
}
DCHECK(url_for_request_.is_empty())
<< "HttpBridge::SetURL called more than once?!";
#endif
GURL temp(url);
GURL::Replacements replacements;
std::string port_str = base::IntToString(port);
replacements.SetPort(port_str.c_str(), url::Component(0, port_str.length()));
url_for_request_ = temp.ReplaceComponents(replacements);
}
void HttpBridge::SetPostPayload(const char* content_type,
int content_length,
const char* content) {
#if DCHECK_IS_ON()
DCHECK(thread_checker_.CalledOnValidThread());
{
base::AutoLock lock(fetch_state_lock_);
DCHECK(!fetch_state_.request_completed);
}
DCHECK(content_type_.empty()) << "Bridge payload already set.";
DCHECK_GE(content_length, 0) << "Content length < 0";
#endif
content_type_ = content_type;
if (!content || (content_length == 0)) {
DCHECK_EQ(content_length, 0);
request_content_ = " "; // TODO(timsteele): URLFetcher requires non-empty
// content for POSTs whereas CURL does not, for now
// we hack this to support the sync backend.
} else {
request_content_.assign(content, content_length);
}
}
bool HttpBridge::MakeSynchronousPost(int* net_error_code,
int* http_status_code) {
#if DCHECK_IS_ON()
DCHECK(thread_checker_.CalledOnValidThread());
{
base::AutoLock lock(fetch_state_lock_);
DCHECK(!fetch_state_.request_completed);
}
DCHECK(url_for_request_.is_valid()) << "Invalid URL for request";
DCHECK(!content_type_.empty()) << "Payload not set";
#endif
if (!network_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&HttpBridge::CallMakeAsynchronousPost, this))) {
// This usually happens when we're in a unit test.
LOG(WARNING) << "Could not post CallMakeAsynchronousPost task";
return false;
}
// Block until network request completes or is aborted. See
// OnURLFetchComplete and Abort.
http_post_completed_.Wait();
base::AutoLock lock(fetch_state_lock_);
DCHECK(fetch_state_.request_completed || fetch_state_.aborted);
*net_error_code = fetch_state_.net_error_code;
*http_status_code = fetch_state_.http_status_code;
return fetch_state_.request_succeeded;
}
void HttpBridge::MakeAsynchronousPost() {
DCHECK(network_task_runner_->RunsTasksInCurrentSequence());
base::AutoLock lock(fetch_state_lock_);
DCHECK(!fetch_state_.request_completed);
if (fetch_state_.aborted)
return;
// Start the timer on the network thread (the same thread progress is made
// on, and on which the url fetcher lives).
DCHECK(!fetch_state_.http_request_timeout_timer);
fetch_state_.http_request_timeout_timer =
std::make_unique<base::OneShotTimer>();
fetch_state_.http_request_timeout_timer->Start(
FROM_HERE, base::TimeDelta::FromSeconds(kMaxHttpRequestTimeSeconds),
base::BindOnce(&HttpBridge::OnURLLoadTimedOut, this));
// Some tests inject |url_loader_factory_| created to operated on the
// IO-capable thread currently running.
DCHECK((!url_loader_factory_ && url_loader_factory_info_) ||
(url_loader_factory_ && !url_loader_factory_info_));
if (!url_loader_factory_) {
DCHECK(url_loader_factory_info_);
url_loader_factory_ = network::SharedURLLoaderFactory::Create(
std::move(url_loader_factory_info_));
}
fetch_state_.start_time = base::Time::Now();
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("sync_http_bridge", R"(
semantics {
sender: "Chrome Sync"
description:
"Chrome Sync synchronizes profile data between Chromium clients "
"and Google for a given user account."
trigger:
"User makes a change to syncable profile data after enabling sync "
"on the device."
data:
"The device and user identifiers, along with any profile data that "
"is changing."
destination: GOOGLE_OWNED_SERVICE
}
policy {
cookies_allowed: NO
setting:
"Users can disable Chrome Sync by going into the profile settings "
"and choosing to Sign Out."
chrome_policy {
SyncDisabled {
policy_options {mode: MANDATORY}
SyncDisabled: true
}
}
})");
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = url_for_request_;
resource_request->method = "POST";
resource_request->load_flags =
net::LOAD_BYPASS_CACHE | net::LOAD_DISABLE_CACHE |
net::LOAD_DO_NOT_SAVE_COOKIES | net::LOAD_DO_NOT_SEND_COOKIES;
if (!extra_headers_.empty())
resource_request->headers.AddHeadersFromString(extra_headers_);
resource_request->headers.SetHeader("Content-Encoding", "gzip");
resource_request->headers.SetHeader(net::HttpRequestHeaders::kUserAgent,
user_agent_);
fetch_state_.url_loader = network::SimpleURLLoader::Create(
std::move(resource_request), traffic_annotation);
network::SimpleURLLoader* url_loader = fetch_state_.url_loader.get();
// TODO(https://crbug.com/808498): Re-add data use measurement once
// SimpleURLLoader supports it.
//
// This calls |BindFetcherToDataTracker| in
// components/sync/driver/glue/sync_backend_host_core.cc which used to
// data_use_measurement::DataUseUserData::AttachToFetcher.
// if (!bind_to_tracker_callback_.is_null())
// bind_to_tracker_callback_.Run(fetch_state_.url_poster);
std::string request_to_send;
compression::GzipCompress(request_content_, &request_to_send);
url_loader->AttachStringForUpload(request_to_send, content_type_);
RecordSyncRequestContentLengthHistograms(request_to_send.size(),
request_content_.size());
// Sync relies on HTTP errors being detectable (and distinct from
// net/connection errors).
url_loader->SetAllowHttpErrorResults(true);
url_loader->SetOnUploadProgressCallback(base::BindRepeating(
&HttpBridge::OnURLLoadUploadProgress, base::Unretained(this)));
url_loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
url_loader_factory_.get(),
base::BindOnce(&HttpBridge::OnURLLoadComplete, base::Unretained(this)));
}
int HttpBridge::GetResponseContentLength() const {
DCHECK(thread_checker_.CalledOnValidThread());
base::AutoLock lock(fetch_state_lock_);
DCHECK(fetch_state_.request_completed);
return fetch_state_.response_content.size();
}
const char* HttpBridge::GetResponseContent() const {
DCHECK(thread_checker_.CalledOnValidThread());
base::AutoLock lock(fetch_state_lock_);
DCHECK(fetch_state_.request_completed);
return fetch_state_.response_content.data();
}
const std::string HttpBridge::GetResponseHeaderValue(
const std::string& name) const {
DCHECK(thread_checker_.CalledOnValidThread());
base::AutoLock lock(fetch_state_lock_);
DCHECK(fetch_state_.request_completed);
std::string value;
fetch_state_.response_headers->EnumerateHeader(nullptr, name, &value);
return value;
}
void HttpBridge::Abort() {
base::AutoLock lock(fetch_state_lock_);
// Release |url_loader_factory_info_| as soon as possible so that
// no URLLoaderFactory instances proceed on the network task runner.
url_loader_factory_info_.reset();
DCHECK(!fetch_state_.aborted);
if (fetch_state_.aborted || fetch_state_.request_completed)
return;
fetch_state_.aborted = true;
if (!network_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&HttpBridge::DestroyURLLoaderOnIOThread, this,
std::move(fetch_state_.url_loader),
std::move(fetch_state_.http_request_timeout_timer)))) {
// Madness ensues.
NOTREACHED() << "Could not post task to delete URLLoader";
}
fetch_state_.net_error_code = net::ERR_ABORTED;
http_post_completed_.Signal();
}
void HttpBridge::DestroyURLLoaderOnIOThread(
std::unique_ptr<network::SimpleURLLoader> loader,
std::unique_ptr<base::OneShotTimer> loader_timer) {
DCHECK(network_task_runner_->RunsTasksInCurrentSequence());
// Both |loader_timer| and |loader| go out of scope.
url_loader_factory_ = nullptr;
}
void HttpBridge::OnURLLoadComplete(std::unique_ptr<std::string> response_body) {
DCHECK(network_task_runner_->RunsTasksInCurrentSequence());
base::AutoLock lock(fetch_state_lock_);
network::SimpleURLLoader* url_loader = fetch_state_.url_loader.get();
// If the fetch completes in the window between Abort() and
// DestroyURLLoaderOnIOThread() this will still run. Abort() has already
// reported the result, so no extra work is needed.
if (fetch_state_.aborted)
return;
int http_status_code = -1;
if (url_loader->ResponseInfo() && url_loader->ResponseInfo()->headers) {
http_status_code = url_loader->ResponseInfo()->headers->response_code();
fetch_state_.response_headers = url_loader->ResponseInfo()->headers;
}
OnURLLoadCompleteInternal(
http_status_code, url_loader->NetError(), url_loader->GetContentSize(),
url_loader->GetFinalURL(), std::move(response_body));
}
void HttpBridge::OnURLLoadCompleteInternal(
int http_status_code,
int net_error_code,
int64_t compressed_content_length,
const GURL& final_url,
std::unique_ptr<std::string> response_body) {
DCHECK(network_task_runner_->RunsTasksInCurrentSequence());
// Stop the request timer now that the request completed.
if (fetch_state_.http_request_timeout_timer)
fetch_state_.http_request_timeout_timer.reset();
// TODO(crbug.com/844968): Relax this if-check to become a DCHECK?
if (fetch_state_.aborted)
return;
fetch_state_.end_time = base::Time::Now();
fetch_state_.request_completed = true;
fetch_state_.request_succeeded =
net_error_code == net::OK && http_status_code != -1;
fetch_state_.http_status_code = http_status_code;
fetch_state_.net_error_code = net_error_code;
if (fetch_state_.request_succeeded)
LogTimeout(false);
base::UmaHistogramSparse(
"Sync.URLFetchResponse",
fetch_state_.request_succeeded
? fetch_state_.http_status_code
: net::URLRequestStatus::FromError(fetch_state_.net_error_code)
.ToNetError());
UMA_HISTOGRAM_LONG_TIMES("Sync.URLFetchTime",
fetch_state_.end_time - fetch_state_.start_time);
// Use a real (non-debug) log to facilitate troubleshooting in the wild.
VLOG(2) << "HttpBridge::OnURLFetchComplete for: " << final_url.spec();
VLOG(1) << "HttpBridge received response code: "
<< fetch_state_.http_status_code;
if (response_body)
fetch_state_.response_content = std::move(*response_body);
UpdateNetworkTime();
int64_t original_content_length = fetch_state_.response_content.size();
RecordSyncResponseContentLengthHistograms(compressed_content_length,
original_content_length);
fetch_state_.url_loader.reset();
url_loader_factory_ = nullptr;
// Wake the blocked syncer thread in MakeSynchronousPost.
// WARNING: DONT DO ANYTHING AFTER THIS CALL! |this| may be deleted!
http_post_completed_.Signal();
}
void HttpBridge::OnURLLoadUploadProgress(uint64_t position, uint64_t total) {
DCHECK(network_task_runner_->RunsTasksInCurrentSequence());
// Reset the delay when forward progress is made.
base::AutoLock lock(fetch_state_lock_);
if (fetch_state_.http_request_timeout_timer)
fetch_state_.http_request_timeout_timer->Reset();
}
void HttpBridge::OnURLLoadTimedOut() {
DCHECK(network_task_runner_->RunsTasksInCurrentSequence());
base::AutoLock lock(fetch_state_lock_);
if (!fetch_state_.url_loader)
return;
LogTimeout(true);
DVLOG(1) << "Sync url fetch timed out. Canceling.";
fetch_state_.end_time = base::Time::Now();
fetch_state_.request_completed = true;
fetch_state_.request_succeeded = false;
fetch_state_.http_status_code = -1;
fetch_state_.net_error_code = net::ERR_TIMED_OUT;
// This method is called by the timer, not the url fetcher implementation,
// so it's safe to delete the fetcher here.
fetch_state_.url_loader.reset();
url_loader_factory_ = nullptr;
// Timer is smart enough to handle being deleted as part of the invoked task.
fetch_state_.http_request_timeout_timer.reset();
// Wake the blocked syncer thread in MakeSynchronousPost.
// WARNING: DONT DO ANYTHING AFTER THIS CALL! |this| may be deleted!
http_post_completed_.Signal();
}
void HttpBridge::SetIOCapableTaskRunnerForTest(
scoped_refptr<base::SequencedTaskRunner> task_runner) {
g_io_capable_task_runner_for_tests.Get() = task_runner;
}
void HttpBridge::UpdateNetworkTime() {
std::string sane_time_str;
if (!fetch_state_.request_succeeded || fetch_state_.start_time.is_null() ||
fetch_state_.end_time < fetch_state_.start_time ||
!fetch_state_.response_headers ||
!fetch_state_.response_headers->EnumerateHeader(
nullptr, "Sane-Time-Millis", &sane_time_str)) {
return;
}
int64_t sane_time_ms = 0;
if (base::StringToInt64(sane_time_str, &sane_time_ms)) {
network_time_update_callback_.Run(
base::Time::FromJsTime(sane_time_ms),
base::TimeDelta::FromMilliseconds(1),
fetch_state_.end_time - fetch_state_.start_time);
}
}
} // namespace syncer