123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394 |
- // Copyright 2018 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/invalidation/impl/per_user_topic_subscription_request.h"
- #include <memory>
- #include "base/bind.h"
- #include "base/json/json_writer.h"
- #include "base/memory/ptr_util.h"
- #include "base/metrics/histogram_functions.h"
- #include "base/strings/stringprintf.h"
- #include "base/values.h"
- #include "components/sync/base/model_type.h"
- #include "net/http/http_status_code.h"
- #include "services/network/public/cpp/resource_request.h"
- #include "services/network/public/mojom/fetch_api.mojom-shared.h"
- #include "services/network/public/mojom/url_response_head.mojom.h"
- using net::HttpRequestHeaders;
- namespace invalidation {
- namespace {
- const char kPublicTopicNameKey[] = "publicTopicName";
- const char kPrivateTopicNameKey[] = "privateTopicName";
- const std::string* GetTopicName(const base::Value& value) {
- if (!value.is_dict())
- return nullptr;
- if (value.FindBoolKey("isPublic").value_or(false))
- return value.FindStringKey(kPublicTopicNameKey);
- return value.FindStringKey(kPrivateTopicNameKey);
- }
- bool IsNetworkError(int net_error) {
- // Note: ERR_HTTP_RESPONSE_CODE_FAILURE isn't a real network error - it
- // indicates that the network request itself succeeded, but we received an
- // HTTP error. The alternative to special-casing this error would be to call
- // SetAllowHttpErrorResults(true) on the SimpleUrlLoader, but that also means
- // we'd download the response body in case of HTTP errors, which we don't
- // need.
- return net_error != net::OK &&
- net_error != net::ERR_HTTP_RESPONSE_CODE_FAILURE;
- }
- // Subscription status values for UMA_HISTOGRAM.
- enum class SubscriptionStatus {
- kSuccess = 0,
- kNetworkFailure = 1,
- kHttpFailure = 2,
- kParsingFailure = 3,
- kFailure = 4,
- kMaxValue = kFailure,
- };
- void RecordRequestStatus(SubscriptionStatus status,
- PerUserTopicSubscriptionRequest::RequestType type,
- const std::string& topic,
- int net_error = net::OK,
- int response_code = 200) {
- switch (type) {
- case PerUserTopicSubscriptionRequest::SUBSCRIBE: {
- base::UmaHistogramEnumeration(
- "FCMInvalidations.SubscriptionRequestStatus", status);
- break;
- }
- case PerUserTopicSubscriptionRequest::UNSUBSCRIBE: {
- base::UmaHistogramEnumeration(
- "FCMInvalidations.UnsubscriptionRequestStatus", status);
- break;
- }
- }
- if (type != PerUserTopicSubscriptionRequest::SUBSCRIBE) {
- return;
- }
- if (IsNetworkError(net_error)) {
- // Tracks the cases, when request fails due to network error.
- base::UmaHistogramSparse("FCMInvalidations.FailedSubscriptionsErrorCode",
- -net_error);
- DVLOG(1) << "Subscription request failed with error: " << net_error << ": "
- << net::ErrorToString(net_error);
- } else {
- // Log a histogram to track response success vs. failure rates.
- base::UmaHistogramSparse("FCMInvalidations.SubscriptionResponseCode",
- response_code);
- // If the topic corresponds to a Sync ModelType, use that as the histogram
- // suffix. Otherwise (e.g. Drive or Policy), just use "OTHER" for now.
- // TODO(crbug.com/1029698): Depending on sync is a layering violation.
- // Eventually the "whitelisted for metrics" bit should be part of a Topic.
- syncer::ModelType model_type; // Unused.
- std::string suffix =
- syncer::NotificationTypeToRealModelType(topic, &model_type) ? topic
- : "OTHER";
- base::UmaHistogramSparse(
- "FCMInvalidations.SubscriptionResponseCodeForTopic." + suffix,
- response_code);
- }
- }
- } // namespace
- PerUserTopicSubscriptionRequest::PerUserTopicSubscriptionRequest() = default;
- PerUserTopicSubscriptionRequest::~PerUserTopicSubscriptionRequest() = default;
- void PerUserTopicSubscriptionRequest::Start(
- CompletedCallback callback,
- network::mojom::URLLoaderFactory* loader_factory) {
- DCHECK(request_completed_callback_.is_null()) << "Request already running!";
- request_completed_callback_ = std::move(callback);
- simple_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
- loader_factory,
- base::BindOnce(&PerUserTopicSubscriptionRequest::OnURLFetchComplete,
- weak_ptr_factory_.GetWeakPtr()));
- }
- void PerUserTopicSubscriptionRequest::OnURLFetchComplete(
- std::unique_ptr<std::string> response_body) {
- int response_code = 0;
- if (simple_loader_->ResponseInfo() &&
- simple_loader_->ResponseInfo()->headers) {
- response_code = simple_loader_->ResponseInfo()->headers->response_code();
- }
- OnURLFetchCompleteInternal(simple_loader_->NetError(), response_code,
- std::move(response_body));
- // Potentially dead after the above invocation; nothing to do except return.
- }
- void PerUserTopicSubscriptionRequest::OnURLFetchCompleteInternal(
- int net_error,
- int response_code,
- std::unique_ptr<std::string> response_body) {
- if (IsNetworkError(net_error)) {
- RecordRequestStatus(SubscriptionStatus::kNetworkFailure, type_, topic_,
- net_error, response_code);
- RunCompletedCallbackAndMaybeDie(
- Status(StatusCode::FAILED, base::StringPrintf("Network Error")),
- std::string());
- // Potentially dead after the above invocation; nothing to do except return.
- return;
- }
- if (response_code != net::HTTP_OK) {
- StatusCode status = StatusCode::FAILED;
- if (response_code == net::HTTP_UNAUTHORIZED) {
- status = StatusCode::AUTH_FAILURE;
- } else if (response_code >= 400 && response_code <= 499) {
- status = StatusCode::FAILED_NON_RETRIABLE;
- }
- RecordRequestStatus(SubscriptionStatus::kHttpFailure, type_, topic_,
- net_error, response_code);
- RunCompletedCallbackAndMaybeDie(
- Status(status, base::StringPrintf("HTTP Error: %d", response_code)),
- std::string());
- // Potentially dead after the above invocation; nothing to do except return.
- return;
- }
- if (type_ == UNSUBSCRIBE) {
- // No response body expected for DELETE requests.
- RecordRequestStatus(SubscriptionStatus::kSuccess, type_, topic_, net_error,
- response_code);
- RunCompletedCallbackAndMaybeDie(Status(StatusCode::SUCCESS, std::string()),
- std::string());
- // Potentially dead after the above invocation; nothing to do except return.
- return;
- }
- if (!response_body || response_body->empty()) {
- RecordRequestStatus(SubscriptionStatus::kParsingFailure, type_, topic_,
- net_error, response_code);
- RunCompletedCallbackAndMaybeDie(
- Status(StatusCode::FAILED, base::StringPrintf("Body missing")),
- std::string());
- // Potentially dead after the above invocation; nothing to do except return.
- return;
- }
- data_decoder::DataDecoder::ParseJsonIsolated(
- *response_body,
- base::BindOnce(&PerUserTopicSubscriptionRequest::OnJsonParse,
- weak_ptr_factory_.GetWeakPtr()));
- }
- void PerUserTopicSubscriptionRequest::OnJsonParse(
- data_decoder::DataDecoder::ValueOrError result) {
- if (!result.has_value()) {
- RecordRequestStatus(SubscriptionStatus::kParsingFailure, type_, topic_);
- RunCompletedCallbackAndMaybeDie(
- Status(StatusCode::FAILED, base::StringPrintf("Body parse error")),
- std::string());
- // Potentially dead after the above invocation; nothing to do except return.
- return;
- }
- const std::string* topic_name = GetTopicName(*result);
- if (topic_name) {
- RecordRequestStatus(SubscriptionStatus::kSuccess, type_, topic_);
- RunCompletedCallbackAndMaybeDie(Status(StatusCode::SUCCESS, std::string()),
- *topic_name);
- // Potentially dead after the above invocation; nothing to do except return.
- } else {
- RecordRequestStatus(SubscriptionStatus::kParsingFailure, type_, topic_);
- RunCompletedCallbackAndMaybeDie(
- Status(StatusCode::FAILED, base::StringPrintf("Missing topic name")),
- std::string());
- // Potentially dead after the above invocation; nothing to do except return.
- }
- }
- void PerUserTopicSubscriptionRequest::RunCompletedCallbackAndMaybeDie(
- Status status,
- std::string topic_name) {
- std::move(request_completed_callback_)
- .Run(std::move(status), std::move(topic_name));
- }
- PerUserTopicSubscriptionRequest::Builder::Builder() = default;
- PerUserTopicSubscriptionRequest::Builder::~Builder() = default;
- std::unique_ptr<PerUserTopicSubscriptionRequest>
- PerUserTopicSubscriptionRequest::Builder::Build() const {
- DCHECK(!scope_.empty());
- auto request = base::WrapUnique(new PerUserTopicSubscriptionRequest());
- std::string url;
- switch (type_) {
- case SUBSCRIBE:
- url = base::StringPrintf(
- "%s/v1/perusertopics/%s/rel/topics/?subscriber_token=%s",
- scope_.c_str(), project_id_.c_str(), instance_id_token_.c_str());
- break;
- case UNSUBSCRIBE:
- std::string public_param;
- if (topic_is_public_) {
- public_param = "subscription.is_public=true&";
- }
- url = base::StringPrintf(
- "%s/v1/perusertopics/%s/rel/topics/%s?%ssubscriber_token=%s",
- scope_.c_str(), project_id_.c_str(), topic_.c_str(),
- public_param.c_str(), instance_id_token_.c_str());
- break;
- }
- GURL full_url(url);
- DCHECK(full_url.is_valid());
- request->url_ = full_url;
- request->type_ = type_;
- request->topic_ = topic_;
- std::string body;
- if (type_ == SUBSCRIBE)
- body = BuildBody();
- net::HttpRequestHeaders headers = BuildHeaders();
- request->simple_loader_ = BuildURLFetcher(headers, body, full_url);
- // Log the request for debugging network issues.
- DVLOG(1) << "Building a subscription request to " << full_url << ":\n"
- << headers.ToString() << "\n"
- << body;
- return request;
- }
- PerUserTopicSubscriptionRequest::Builder&
- PerUserTopicSubscriptionRequest::Builder::SetInstanceIdToken(
- const std::string& token) {
- instance_id_token_ = token;
- return *this;
- }
- PerUserTopicSubscriptionRequest::Builder&
- PerUserTopicSubscriptionRequest::Builder::SetScope(const std::string& scope) {
- scope_ = scope;
- return *this;
- }
- PerUserTopicSubscriptionRequest::Builder&
- PerUserTopicSubscriptionRequest::Builder::SetAuthenticationHeader(
- const std::string& auth_header) {
- auth_header_ = auth_header;
- return *this;
- }
- PerUserTopicSubscriptionRequest::Builder&
- PerUserTopicSubscriptionRequest::Builder::SetPublicTopicName(
- const Topic& topic) {
- topic_ = topic;
- return *this;
- }
- PerUserTopicSubscriptionRequest::Builder&
- PerUserTopicSubscriptionRequest::Builder::SetProjectId(
- const std::string& project_id) {
- project_id_ = project_id;
- return *this;
- }
- PerUserTopicSubscriptionRequest::Builder&
- PerUserTopicSubscriptionRequest::Builder::SetType(RequestType type) {
- type_ = type;
- return *this;
- }
- PerUserTopicSubscriptionRequest::Builder&
- PerUserTopicSubscriptionRequest::Builder::SetTopicIsPublic(
- bool topic_is_public) {
- topic_is_public_ = topic_is_public;
- return *this;
- }
- HttpRequestHeaders PerUserTopicSubscriptionRequest::Builder::BuildHeaders()
- const {
- HttpRequestHeaders headers;
- if (!auth_header_.empty()) {
- headers.SetHeader(HttpRequestHeaders::kAuthorization, auth_header_);
- }
- return headers;
- }
- std::string PerUserTopicSubscriptionRequest::Builder::BuildBody() const {
- base::Value request(base::Value::Type::DICTIONARY);
- request.SetStringKey("public_topic_name", topic_);
- if (topic_is_public_)
- request.SetBoolKey("is_public", true);
- std::string request_json;
- bool success = base::JSONWriter::Write(request, &request_json);
- DCHECK(success);
- return request_json;
- }
- std::unique_ptr<network::SimpleURLLoader>
- PerUserTopicSubscriptionRequest::Builder::BuildURLFetcher(
- const HttpRequestHeaders& headers,
- const std::string& body,
- const GURL& url) const {
- net::NetworkTrafficAnnotationTag traffic_annotation =
- net::DefineNetworkTrafficAnnotation("per_user_topic_registration_request",
- R"(
- semantics {
- sender:
- "Subscribe the Sync client for listening to the specific topic"
- description:
- "Chromium can receive Sync invalidations via FCM messages."
- "This request subscribes the client for receiving messages for the"
- "concrete topic. In case of Chrome Sync topic is a ModelType,"
- "e.g. BOOKMARK"
- trigger:
- "Subscription takes place only once per profile per topic. "
- data:
- "An OAuth2 token is sent as an authorization header."
- destination: GOOGLE_OWNED_SERVICE
- }
- policy {
- cookies_allowed: NO
- setting:
- "This feature can not be disabled by settings now"
- chrome_policy: {
- SyncDisabled {
- policy_options {mode: MANDATORY}
- SyncDisabled: false
- }
- }
- })");
- auto request = std::make_unique<network::ResourceRequest>();
- switch (type_) {
- case SUBSCRIBE:
- request->method = "POST";
- break;
- case UNSUBSCRIBE:
- request->method = "DELETE";
- break;
- }
- request->url = url;
- request->headers = headers;
- // Disable cookies for this request.
- request->credentials_mode = network::mojom::CredentialsMode::kOmit;
- std::unique_ptr<network::SimpleURLLoader> url_loader =
- network::SimpleURLLoader::Create(std::move(request), traffic_annotation);
- url_loader->AttachStringForUpload(body, "application/json; charset=UTF-8");
- return url_loader;
- }
- } // namespace invalidation
|