chromium/chromeos/ash/components/carrier_lock/topic_subscription_request.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/components/carrier_lock/topic_subscription_request.h"

#include "base/json/json_writer.h"
#include "base/location.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/escape.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/values.h"
#include "google_apis/credentials_mode.h"
#include "net/base/load_flags.h"
#include "net/http/http_request_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/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "url/gurl.h"

namespace ash::carrier_lock {

namespace {

const char kTopicSubscriptionRequestContentType[] =
    "application/x-www-form-urlencoded";
const char kFcmRegistrationUrl[] =
    "https://android.clients.google.com/c2dm/register3";

// Request constants.
const char kAppKey[] = "app";
const char kSenderKey[] = "sender";
const char kDeviceKey[] = "device";
const char kTopicKey[] = "X-gcm.topic";
const char kDeleteKey[] = "delete";
const char kLoginHeader[] = "AidLogin";

// Response constants.
const char kErrorPrefix[] = "Error=";
const char kAuthenticationFailed[] = "AUTHENTICATION_FAILED";
const char kInvalidSender[] = "INVALID_SENDER";
const char kInvalidParameters[] = "INVALID_PARAMETERS";
const char kInternalServerError[] = "InternalServerError";
const char kQuotaExceeded[] = "QUOTA_EXCEEDED";
const char kTooManySubscribers[] = "TOO_MANY_SUBSCRIBERS";

// Gets correct status from the error message.
Result GetStatusFromError(std::string& error) {
  if (error.find(kAuthenticationFailed) != std::string::npos) {
    error = kAuthenticationFailed;
    return Result::kInvalidInput;
  }
  if (error.find(kInvalidSender) != std::string::npos) {
    error = kInvalidSender;
    return Result::kInvalidInput;
  }
  if (error.find(kInvalidParameters) != std::string::npos) {
    error = kInvalidParameters;
    return Result::kInvalidInput;
  }
  if (error.find(kInternalServerError) != std::string::npos) {
    error = kInternalServerError;
    return Result::kServerInternalError;
  }
  if (error.find(kQuotaExceeded) != std::string::npos) {
    error = kQuotaExceeded;
    return Result::kServerInternalError;
  }
  if (error.find(kTooManySubscribers) != std::string::npos) {
    error = kTooManySubscribers;
    return Result::kServerInternalError;
  }

  // Handle unknown errors.
  size_t pos = std::size(kErrorPrefix);
  error = error.substr(pos, error.size() - pos);
  return Result::kRequestFailed;
}

// Create encoding: "key=value" and append it to "out" string
void BuildFormEncoding(const std::string& key,
                       const std::string& value,
                       std::string* out) {
  if (!out->empty()) {
    out->append("&");
  }
  out->append(key + "=" + base::EscapeUrlEncodedData(value, true));
}

}  // namespace

TopicSubscriptionRequest::RequestInfo::RequestInfo(uint64_t android_id,
                                                   uint64_t security_token,
                                                   const std::string& app_id,
                                                   const std::string& token,
                                                   const std::string& topic,
                                                   bool unsubscribe)
    : android_id(android_id),
      security_token(security_token),
      app_id(app_id),
      token(token),
      topic(topic),
      unsubscribe(unsubscribe) {
  DCHECK(android_id != 0UL);
  DCHECK(security_token != 0UL);
  DCHECK(!app_id.empty());
  DCHECK(!token.empty());
  DCHECK(!topic.empty());
}

TopicSubscriptionRequest::RequestInfo::RequestInfo(const RequestInfo&) =
    default;

TopicSubscriptionRequest::RequestInfo::~RequestInfo() = default;

TopicSubscriptionRequest::TopicSubscriptionRequest(
    const RequestInfo& request_info,
    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
    Callback callback)
    : request_callback_(std::move(callback)),
      request_info_(request_info),
      url_loader_factory_(std::move(url_loader_factory)) {
  topic_subscription_url_ = GURL(kFcmRegistrationUrl);
}

TopicSubscriptionRequest::~TopicSubscriptionRequest() = default;

void TopicSubscriptionRequest::Start() {
  DCHECK(!request_callback_.is_null());
  DCHECK(!url_loader_.get());

  net::NetworkTrafficAnnotationTag traffic_annotation =
      net::DefineNetworkTrafficAnnotation("carrier_lock_manager_fcm_topic", R"(
        semantics {
          sender: "Carrier Lock manager"
          description:
            "Carrier Lock Manager subscribes to public topics on FCM"
            "to receive push notifications in case of lock changes."
          trigger: "This request happens once on every boot if the device"
                   "has carrier lock enabled."
          data:
            "The topic name and a registration token."
          destination: GOOGLE_OWNED_SERVICE
          internal {
            contacts {
                email: "[email protected]"
            }
          }
          user_data {
            type: ACCESS_TOKEN
          }
          last_reviewed: "2023-10-24"
        }
        policy {
          cookies_allowed: NO
          setting: "This feature cannot be disabled in settings."
          policy_exception_justification: "Carrier Lock is always enforced."
        })");
  auto request = std::make_unique<network::ResourceRequest>();
  request->url = topic_subscription_url_;
  request->method = "POST";
  request->credentials_mode =
      google_apis::GetOmitCredentialsModeForGaiaRequests();

  request->headers.SetHeader(
      net::HttpRequestHeaders::kAuthorization,
      std::string(kLoginHeader) + " " +
          base::NumberToString(request_info_.android_id) + ":" +
          base::NumberToString(request_info_.security_token));

  std::string body;
  BuildFormEncoding(kDeviceKey, base::NumberToString(request_info_.android_id),
                    &body);
  BuildFormEncoding(kAppKey, request_info_.app_id, &body);
  BuildFormEncoding(kSenderKey, request_info_.token, &body);
  BuildFormEncoding(kTopicKey, request_info_.topic, &body);
  if (request_info_.unsubscribe) {
    BuildFormEncoding(kDeleteKey, "true", &body);
  }

  url_loader_ =
      network::SimpleURLLoader::Create(std::move(request), traffic_annotation);
  url_loader_->AttachStringForUpload(body,
                                     kTopicSubscriptionRequestContentType);
  url_loader_->SetAllowHttpErrorResults(true);
  url_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
      url_loader_factory_.get(),
      base::BindOnce(&TopicSubscriptionRequest::OnUrlLoadComplete,
                     base::Unretained(this), url_loader_.get()));
}

void TopicSubscriptionRequest::OnUrlLoadComplete(
    const network::SimpleURLLoader* source,
    std::unique_ptr<std::string> body) {
  if (source->NetError() != net::OK) {
    LOG(ERROR) << "Failed to fetch URL.";
    ReturnResult(Result::kConnectionError);
    return;
  }

  std::string response;
  if (!body) {
    LOG(ERROR) << "Failed to get response.";
    ReturnResult(Result::kConnectionError);
    return;
  }
  response = std::move(*body);

  // If we are able to parse a meaningful known error, let's do so. Note that
  // some errors will have HTTP_OK response code!
  size_t error_pos = response.find(kErrorPrefix);
  if (error_pos != std::string::npos) {
    std::string error = response.substr(error_pos);
    ReturnResult(GetStatusFromError(error));
    LOG(ERROR) << "Received error in response: " << error;
    return;
  }

  // Can't even get any header info.
  if (!source->ResponseInfo() || !source->ResponseInfo()->headers) {
    LOG(ERROR) << "Missing header in response.";
    ReturnResult(Result::kInvalidResponse);
    return;
  }

  // If we cannot tell what the error is, but at least we know response code was
  // not OK.
  if (source->ResponseInfo()->headers->response_code() != net::HTTP_OK) {
    LOG(ERROR) << "HTTP response code not OK: "
               << source->ResponseInfo()->headers->response_code();
    ReturnResult(Result::kInvalidResponse);
    return;
  }

  ReturnResult(Result::kSuccess);
}

void TopicSubscriptionRequest::ReturnResult(Result result) {
  std::move(request_callback_).Run(result);
}

}  // namespace ash::carrier_lock