blob: ebbb4f61b12216ace895bccf230c5c2eacd447b9 [file] [log] [blame]
// Copyright 2020 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/feed/core/v2/feed_network_impl.h"
#include <memory>
#include <utility>
#include "base/base64url.h"
#include "base/bind.h"
#include "base/command_line.h"
#include "base/containers/flat_set.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "components/feed/core/common/pref_names.h"
#include "components/feed/core/proto/v2/wire/feed_query.pb.h"
#include "components/feed/core/proto/v2/wire/request.pb.h"
#include "components/feed/core/proto/v2/wire/response.pb.h"
#include "components/feed/core/proto/v2/wire/upload_actions_request.pb.h"
#include "components/feed/core/proto/v2/wire/upload_actions_response.pb.h"
#include "components/feed/core/v2/metrics_reporter.h"
#include "components/prefs/pref_service.h"
#include "components/signin/public/identity_manager/access_token_info.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/signin/public/identity_manager/primary_account_access_token_fetcher.h"
#include "components/signin/public/identity_manager/scope_set.h"
#include "components/variations/net/variations_http_headers.h"
#include "net/base/isolation_info.h"
#include "net/base/load_flags.h"
#include "net/base/url_util.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/resource_request.h"
#include "services/network/public/cpp/resource_request_body.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "third_party/protobuf/src/google/protobuf/io/coded_stream.h"
#include "third_party/zlib/google/compression_utils.h"
// Token override for Feedv2NewTabPageCardInstrumentationTest.java:
// #define TOKEN_OVERRIDE_FOR_TESTING "put-test-token-here"
namespace feed {
namespace {
constexpr char kApplicationXProtobuf[] = "application/x-protobuf";
constexpr base::TimeDelta kNetworkTimeout = base::TimeDelta::FromSeconds(30);
constexpr char kDiscoverHost[] = "https://discover-pa.googleapis.com/";
signin::ScopeSet GetAuthScopes() {
return {"https://www.googleapis.com/auth/googlenow"};
}
GURL GetFeedQueryURL(feedwire::FeedQuery::RequestReason reason) {
// Add URLs for Bling when it is supported.
switch (reason) {
case feedwire::FeedQuery::SCHEDULED_REFRESH:
case feedwire::FeedQuery::PREFETCHED_WEB_FEED:
return GURL(
"https://www.google.com/httpservice/noretry/TrellisClankService/"
"FeedQuery");
case feedwire::FeedQuery::NEXT_PAGE_SCROLL:
return GURL(
"https://www.google.com/httpservice/retry/TrellisClankService/"
"NextPageQuery");
case feedwire::FeedQuery::MANUAL_REFRESH:
case feedwire::FeedQuery::INTERACTIVE_WEB_FEED:
return GURL(
"https://www.google.com/httpservice/retry/TrellisClankService/"
"FeedQuery");
default:
return GURL();
}
}
GURL GetUrlWithoutQuery(const GURL& url) {
GURL::Replacements replacements;
replacements.ClearQuery();
return url.ReplaceComponents(replacements);
}
using RawResponse = FeedNetwork::RawResponse;
} // namespace
namespace {
void ParseAndForwardQueryResponse(
NetworkRequestType request_type,
base::OnceCallback<void(FeedNetwork::QueryRequestResult)> result_callback,
RawResponse raw_response) {
MetricsReporter::NetworkRequestComplete(
request_type, raw_response.response_info.status_code,
raw_response.response_info.fetch_duration);
FeedNetwork::QueryRequestResult result;
result.response_info.fetch_time_ticks = base::TimeTicks::Now();
result.response_info = raw_response.response_info;
if (result.response_info.status_code == 200) {
::google::protobuf::io::CodedInputStream input_stream(
reinterpret_cast<const uint8_t*>(raw_response.response_bytes.data()),
raw_response.response_bytes.size());
// The first few bytes of the body are a varint containing the size of the
// message. We need to skip over them.
int message_size;
input_stream.ReadVarintSizeAsInt(&message_size);
auto response_message = std::make_unique<feedwire::Response>();
if (response_message->ParseFromCodedStream(&input_stream)) {
result.response_body = std::move(response_message);
}
}
std::move(result_callback).Run(std::move(result));
}
void AddMothershipPayloadQueryParams(const std::string& payload,
const std::string& language_tag,
GURL& url) {
url = net::AppendQueryParameter(url, "reqpld", payload);
url = net::AppendQueryParameter(url, "fmt", "bin");
if (!language_tag.empty())
url = net::AppendQueryParameter(url, "hl", language_tag);
}
// Compresses and attaches |request_body| for upload if it's not empty.
// Returns the compressed size of the request.
int PopulateRequestBody(const std::string& request_body,
network::SimpleURLLoader* loader) {
if (request_body.empty())
return 0;
std::string compressed_request_body;
compression::GzipCompress(request_body, &compressed_request_body);
loader->AttachStringForUpload(compressed_request_body, kApplicationXProtobuf);
return compressed_request_body.size();
}
GURL OverrideUrlSchemeHostPort(const GURL& url,
const GURL& override_scheme_host_port) {
GURL::Replacements replacements;
replacements.SetSchemeStr(override_scheme_host_port.scheme_piece());
replacements.SetHostStr(override_scheme_host_port.host_piece());
replacements.SetPortStr(override_scheme_host_port.port_piece());
return url.ReplaceComponents(replacements);
}
} // namespace
// Each NetworkFetch instance represents a single "logical" fetch that ends by
// calling the associated callback. Network fetches will actually attempt two
// fetches if there is a signed in user; the first to retrieve an access token,
// and the second to the specified url.
class FeedNetworkImpl::NetworkFetch {
public:
NetworkFetch(const GURL& url,
base::StringPiece request_method,
std::string request_body,
FeedNetworkImpl::Delegate* delegate,
signin::IdentityManager* identity_manager,
network::SharedURLLoaderFactory* loader_factory,
const std::string& api_key,
const std::string& gaia,
bool allow_bless_auth)
: url_(url),
request_method_(request_method),
request_body_(std::move(request_body)),
delegate_(delegate),
identity_manager_(identity_manager),
loader_factory_(loader_factory),
api_key_(api_key),
entire_send_start_ticks_(base::TimeTicks::Now()),
gaia_(gaia),
allow_bless_auth_(allow_bless_auth) {}
~NetworkFetch() = default;
NetworkFetch(const NetworkFetch&) = delete;
NetworkFetch& operator=(const NetworkFetch&) = delete;
void Start(base::OnceCallback<void(RawResponse)> done_callback) {
done_callback_ = std::move(done_callback);
if (gaia_.empty()) {
StartLoader();
return;
}
StartAccessTokenFetch();
}
private:
void StartAccessTokenFetch() {
// It's safe to pass base::Unretained(this) since deleting the token fetcher
// will prevent the callback from being completed.
token_fetcher_ = std::make_unique<signin::PrimaryAccountAccessTokenFetcher>(
"feed", identity_manager_, GetAuthScopes(),
base::BindOnce(&NetworkFetch::AccessTokenFetchFinished,
base::Unretained(this), base::TimeTicks::Now()),
signin::PrimaryAccountAccessTokenFetcher::Mode::kWaitUntilAvailable);
}
void AccessTokenFetchFinished(base::TimeTicks token_start_ticks,
GoogleServiceAuthError error,
signin::AccessTokenInfo access_token_info) {
DCHECK(!gaia_.empty());
UMA_HISTOGRAM_ENUMERATION(
"ContentSuggestions.Feed.Network.TokenFetchStatus", error.state(),
GoogleServiceAuthError::NUM_STATES);
base::TimeDelta token_duration = base::TimeTicks::Now() - token_start_ticks;
UMA_HISTOGRAM_MEDIUM_TIMES("ContentSuggestions.Feed.Network.TokenDuration",
token_duration);
access_token_ = access_token_info.token;
// Abort if the signed-in user doesn't match.
if (delegate_->GetSyncSignedInGaia() != gaia_) {
NetworkResponseInfo response_info;
RawResponse raw_response;
response_info.status_code = net::ERR_INVALID_ARGUMENT;
raw_response.response_info = std::move(response_info);
std::move(done_callback_).Run(std::move(raw_response));
return;
}
StartLoader();
}
void StartLoader() {
loader_only_start_ticks_ = base::TimeTicks::Now();
simple_loader_ = MakeLoader();
simple_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
loader_factory_, base::BindOnce(&NetworkFetch::OnSimpleLoaderComplete,
base::Unretained(this)));
}
std::unique_ptr<network::SimpleURLLoader> MakeLoader() {
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("interest_feedv2_send", R"(
semantics {
sender: "Feed Library"
description: "Chrome can show content suggestions (e.g. articles) "
"in the form of a feed. For signed-in users, these may be "
"personalized based on interest signals in the user's account."
trigger: "Triggered periodically in the background, or upon "
"explicit user request."
data: "The locale of the device and data describing the suggested "
"content that the user interacted with. For signed-in users "
"the request is authenticated. "
destination: GOOGLE_OWNED_SERVICE
}
policy {
cookies_allowed: YES
cookies_store: "user"
setting: "This can be disabled from the New Tab Page by collapsing "
"the articles section."
chrome_policy {
NTPContentSuggestionsEnabled {
policy_options {mode: MANDATORY}
NTPContentSuggestionsEnabled: false
}
}
})");
GURL url(url_);
if (access_token_.empty() && !api_key_.empty())
url = net::AppendQueryParameter(url_, "key", api_key_);
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = url;
resource_request->load_flags = net::LOAD_BYPASS_CACHE;
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
resource_request->method = request_method_;
if (allow_bless_auth_) {
// Include credentials ONLY if the user has overridden the feed host
// through the internals page. This allows for some authentication
// workflows we need for testing.
resource_request->credentials_mode =
network::mojom::CredentialsMode::kInclude;
resource_request->site_for_cookies = net::SiteForCookies::FromUrl(url);
} else {
// Otherwise, isolate feed traffic from other requests the browser might
// be making. This prevents the browser from reusing network connections
// which may not match the signed-in/out status of the feed.
resource_request->trusted_params =
network::ResourceRequest::TrustedParams();
resource_request->trusted_params->isolation_info =
net::IsolationInfo::CreateTransient();
}
SetRequestHeaders(!request_body_.empty(), *resource_request);
DVLOG(1) << "Feed Request url=" << url;
DVLOG(1) << "Feed Request headers=" << resource_request->headers.ToString();
auto simple_loader = network::SimpleURLLoader::Create(
std::move(resource_request), traffic_annotation);
simple_loader->SetAllowHttpErrorResults(true);
simple_loader->SetTimeoutDuration(kNetworkTimeout);
const int compressed_size =
PopulateRequestBody(request_body_, simple_loader.get());
UMA_HISTOGRAM_COUNTS_1M(
"ContentSuggestions.Feed.Network.RequestSizeKB.Compressed",
compressed_size / 1024);
return simple_loader;
}
void SetRequestHeaders(bool has_request_body,
network::ResourceRequest& request) const {
// content-type in the request header affects server response, so include it
// even if there's no body.
request.headers.SetHeader(net::HttpRequestHeaders::kContentType,
kApplicationXProtobuf);
if (has_request_body) {
request.headers.SetHeader("Content-Encoding", "gzip");
}
variations::SignedIn signed_in_status = variations::SignedIn::kNo;
if (!access_token_.empty()) {
base::StringPiece token = access_token_;
#ifdef TOKEN_OVERRIDE_FOR_TESTING
token = TOKEN_OVERRIDE_FOR_TESTING;
#endif
request.headers.SetHeader(net::HttpRequestHeaders::kAuthorization,
base::StrCat({"Bearer ", token}));
signed_in_status = variations::SignedIn::kYes;
}
// Add X-Client-Data header with experiment IDs from field trials.
variations::AppendVariationsHeader(url_, variations::InIncognito::kNo,
signed_in_status, &request);
}
void OnSimpleLoaderComplete(std::unique_ptr<std::string> response) {
NetworkResponseInfo response_info;
response_info.status_code = simple_loader_->NetError();
response_info.fetch_duration =
base::TimeTicks::Now() - entire_send_start_ticks_;
response_info.fetch_time = base::Time::Now();
response_info.base_request_url = GetUrlWithoutQuery(url_);
response_info.was_signed_in = !access_token_.empty();
response_info.loader_start_time_ticks = loader_only_start_ticks_;
// If overriding the feed host, try to grab the Bless nonce. This is
// strictly informational, and only displayed in snippets-internals.
if (allow_bless_auth_ && simple_loader_->ResponseInfo()) {
size_t iter = 0;
std::string value;
while (simple_loader_->ResponseInfo()->headers->EnumerateHeader(
&iter, "www-authenticate", &value)) {
size_t pos = value.find("nonce=\"");
if (pos != std::string::npos) {
std::string nonce = value.substr(pos + 7, 16);
if (nonce.size() == 16) {
response_info.bless_nonce = nonce;
break;
}
}
}
}
std::string response_body;
if (response) {
response_info.status_code =
simple_loader_->ResponseInfo()->headers->response_code();
response_info.response_body_bytes = response->size();
response_body = std::move(*response);
if (response_info.status_code == net::HTTP_UNAUTHORIZED) {
CoreAccountId account_id =
identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSync);
if (!account_id.empty()) {
identity_manager_->RemoveAccessTokenFromCache(
account_id, GetAuthScopes(), access_token_);
}
}
}
UMA_HISTOGRAM_MEDIUM_TIMES("ContentSuggestions.Feed.Network.Duration",
response_info.fetch_duration);
base::TimeDelta loader_only_duration =
base::TimeTicks::Now() - loader_only_start_ticks_;
// This histogram purposefully matches name and bucket size used in
// RemoteSuggestionsFetcherImpl.
UMA_HISTOGRAM_TIMES("NewTabPage.Snippets.FetchTime", loader_only_duration);
// The below is true even if there is a protocol error, so this will
// record response size as long as the request completed.
if (response_info.status_code >= 200) {
UMA_HISTOGRAM_COUNTS_1M("ContentSuggestions.Feed.Network.ResponseSizeKB",
static_cast<int>(response_body.size() / 1024));
}
RawResponse raw_response;
raw_response.response_info = std::move(response_info);
raw_response.response_bytes = std::move(response_body);
std::move(done_callback_).Run(std::move(raw_response));
}
GURL url_;
const std::string request_method_;
std::string access_token_;
const std::string request_body_;
FeedNetworkImpl::Delegate* delegate_;
signin::IdentityManager* const identity_manager_;
std::unique_ptr<signin::PrimaryAccountAccessTokenFetcher> token_fetcher_;
std::unique_ptr<network::SimpleURLLoader> simple_loader_;
base::OnceCallback<void(RawResponse)> done_callback_;
network::SharedURLLoaderFactory* loader_factory_;
const std::string api_key_;
// Set when the NetworkFetch is constructed, before token and article fetch.
const base::TimeTicks entire_send_start_ticks_;
const std::string gaia_;
// Should be set right before the article fetch, and after the token fetch if
// there is one.
base::TimeTicks loader_only_start_ticks_;
bool allow_bless_auth_ = false;
};
FeedNetworkImpl::FeedNetworkImpl(
Delegate* delegate,
signin::IdentityManager* identity_manager,
const std::string& api_key,
scoped_refptr<network::SharedURLLoaderFactory> loader_factory,
PrefService* pref_service)
: delegate_(delegate),
identity_manager_(identity_manager),
api_key_(api_key),
loader_factory_(loader_factory),
pref_service_(pref_service) {}
FeedNetworkImpl::~FeedNetworkImpl() = default;
void FeedNetworkImpl::SendQueryRequest(
NetworkRequestType request_type,
const feedwire::Request& request,
const std::string& gaia,
base::OnceCallback<void(QueryRequestResult)> callback) {
std::string binary_proto;
request.SerializeToString(&binary_proto);
std::string base64proto;
base::Base64UrlEncode(
binary_proto, base::Base64UrlEncodePolicy::INCLUDE_PADDING, &base64proto);
GURL url = GetFeedQueryURL(request.feed_request().feed_query().reason());
if (url.is_empty())
return std::move(callback).Run({});
// Override url if requested from internals page.
bool host_overridden = false;
std::string host_override =
pref_service_->GetString(feed::prefs::kHostOverrideHost);
if (host_override.empty()) {
host_override = base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
"feedv2-host-override");
}
if (!host_override.empty()) {
GURL override_host_url(host_override);
if (override_host_url.is_valid()) {
GURL::Replacements replacements;
replacements.SetSchemeStr(override_host_url.scheme_piece());
replacements.SetHostStr(override_host_url.host_piece());
replacements.SetPortStr(override_host_url.port_piece());
// Allow the host override to also add a prefix for the path. Ignore
// trailing slashes if they are provided, as the path part of |url| will
// always include "/".
base::StringPiece trimmed_path_prefix = base::TrimString(
override_host_url.path_piece(), "/", base::TRIM_TRAILING);
std::string replacement_path =
base::StrCat({trimmed_path_prefix, url.path_piece()});
replacements.SetPathStr(replacement_path);
url = url.ReplaceComponents(replacements);
host_overridden = true;
}
}
AddMothershipPayloadQueryParams(base64proto, delegate_->GetLanguageTag(),
url);
Send(url, "GET", /*request_body=*/{},
/*allow_bless_auth=*/host_overridden, gaia,
base::BindOnce(&ParseAndForwardQueryResponse, request_type,
std::move(callback)));
}
void FeedNetworkImpl::CancelRequests() {
pending_requests_.clear();
}
void FeedNetworkImpl::Send(const GURL& url,
base::StringPiece request_method,
std::string request_body,
bool allow_bless_auth,
const std::string& gaia,
base::OnceCallback<void(RawResponse)> callback) {
auto fetch = std::make_unique<NetworkFetch>(
url, request_method, std::move(request_body), delegate_,
identity_manager_, loader_factory_.get(), api_key_, gaia,
allow_bless_auth);
NetworkFetch* fetch_unowned = fetch.get();
pending_requests_.emplace(std::move(fetch));
// It's safe to pass base::Unretained(this) since deleting the network fetch
// will prevent the callback from being completed.
fetch_unowned->Start(base::BindOnce(&FeedNetworkImpl::SendComplete,
base::Unretained(this), fetch_unowned,
std::move(callback)));
}
void FeedNetworkImpl::SendDiscoverApiRequest(
NetworkRequestType request_type,
base::StringPiece request_path,
base::StringPiece method,
std::string request_body,
const std::string& gaia,
base::OnceCallback<void(RawResponse)> callback) {
GURL url(base::StrCat({kDiscoverHost, request_path}));
// Override url if requested.
std::string host_override =
pref_service_->GetString(feed::prefs::kDiscoverAPIEndpointOverride);
if (!host_override.empty()) {
GURL override_url(host_override);
if (override_url.is_valid()) {
url = OverrideUrlSchemeHostPort(url, override_url);
}
}
Send(url, method, std::move(request_body),
/*allow_bless_auth=*/false, gaia, std::move(callback));
}
void FeedNetworkImpl::SendComplete(
NetworkFetch* fetch,
base::OnceCallback<void(RawResponse)> callback,
RawResponse raw_response) {
DCHECK_EQ(1UL, pending_requests_.count(fetch));
pending_requests_.erase(fetch);
std::move(callback).Run(std::move(raw_response));
}
} // namespace feed