blob: 6d6e2dbdc491dd9366fe33be79b2790666420faa [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/sharing_message/web_push/web_push_sender.h"
#include <limits.h>
#include <optional>
#include "base/base64url.h"
#include "base/functional/bind.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "components/gcm_driver/crypto/p256_key_util.h"
#include "components/sharing_message/web_push/json_web_token_util.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_status_code.h"
#include "services/network/public/cpp/header_util.h"
#include "services/network/public/cpp/resource_request.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 {
// VAPID header constants.
const char kClaimsKeyAudience[] = "aud";
const char kFCMServerAudience[] = "https://fcm.googleapis.com";
const char kClaimsKeyExpirationTime[] = "exp";
// It's 12 hours rather than 24 hours to avoid any issues with clock differences
// between the sending application and the push service.
constexpr base::TimeDelta kClaimsValidPeriod = base::Hours(12);
const char kAuthorizationRequestHeaderFormat[] = "vapid t=%s, k=%s";
// Endpoint constants.
const char kFCMServerUrlFormat[] = "https://fcm.googleapis.com/fcm/send/%s";
// HTTP header constants.
const char kTTL[] = "TTL";
const char kUrgency[] = "Urgency";
const char kContentEncodingProperty[] = "content-encoding";
const char kContentCodingAes128Gcm[] = "aes128gcm";
// Other constants.
const char kContentEncodingOctetStream[] = "application/octet-stream";
std::optional<std::string> GetAuthHeader(crypto::ECPrivateKey* vapid_key) {
base::Value::Dict claims;
claims.Set(kClaimsKeyAudience, base::Value(kFCMServerAudience));
int64_t exp =
(base::Time::Now() + kClaimsValidPeriod - base::Time::UnixEpoch())
.InSeconds();
// TODO: Year 2038 problem, base::Value does not support int64_t.
if (exp > INT_MAX) {
return std::nullopt;
}
claims.Set(kClaimsKeyExpirationTime, base::Value(static_cast<int32_t>(exp)));
std::optional<std::string> jwt = CreateJSONWebToken(claims, vapid_key);
if (!jwt) {
return std::nullopt;
}
std::string public_key;
if (!gcm::GetRawPublicKey(*vapid_key, &public_key)) {
return std::nullopt;
}
std::string base64_public_key;
base::Base64UrlEncode(public_key, base::Base64UrlEncodePolicy::OMIT_PADDING,
&base64_public_key);
return base::StringPrintf(kAuthorizationRequestHeaderFormat, jwt->c_str(),
base64_public_key.c_str());
}
std::string GetUrgencyHeader(WebPushMessage::Urgency urgency) {
switch (urgency) {
case WebPushMessage::Urgency::kVeryLow:
return "very-low";
case WebPushMessage::Urgency::kLow:
return "low";
case WebPushMessage::Urgency::kNormal:
return "normal";
case WebPushMessage::Urgency::kHigh:
return "high";
}
}
std::unique_ptr<network::SimpleURLLoader> BuildURLLoader(
const std::string& fcm_token,
int time_to_live,
const std::string& urgency_header,
const std::string& auth_header,
const std::string& message) {
auto resource_request = std::make_unique<network::ResourceRequest>();
std::string server_url =
base::StringPrintf(kFCMServerUrlFormat, fcm_token.c_str());
resource_request->url = GURL(server_url);
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
resource_request->method = "POST";
resource_request->headers.SetHeader(net::HttpRequestHeaders::kAuthorization,
auth_header);
resource_request->headers.SetHeader(kTTL, base::NumberToString(time_to_live));
resource_request->headers.SetHeader(kContentEncodingProperty,
kContentCodingAes128Gcm);
resource_request->headers.SetHeader(kUrgency, urgency_header);
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("web_push_message", R"(
semantics {
sender: "GCMDriver WebPushSender"
description:
"Send a request via Firebase to another device that is signed in"
"with the same account."
trigger: "Users send data to another owned device."
data: "Web push message."
destination: GOOGLE_OWNED_SERVICE
user_data {
type: SENSITIVE_URL
type: OTHER
}
internal {
contacts {
owners: "//components/sharing_message/OWNERS"
}
}
last_reviewed: "2024-07-16"
}
policy {
cookies_allowed: NO
setting:
"You can enable or disable this feature in Chrome's settings under"
"'Sync and Google services'."
policy_exception_justification: "Not implemented."
}
)");
std::unique_ptr<network::SimpleURLLoader> loader =
network::SimpleURLLoader::Create(std::move(resource_request),
traffic_annotation);
loader->AttachStringForUpload(message, kContentEncodingOctetStream);
loader->SetRetryOptions(1, network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE);
loader->SetAllowHttpErrorResults(true);
return loader;
}
} // namespace
WebPushSender::WebPushSender(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
: url_loader_factory_(std::move(url_loader_factory)) {}
WebPushSender::~WebPushSender() = default;
void WebPushSender::SendMessage(const std::string& fcm_token,
crypto::ECPrivateKey* vapid_key,
WebPushMessage message,
WebPushCallback callback) {
DCHECK(!fcm_token.empty());
DCHECK(vapid_key);
DCHECK_LE(message.time_to_live, message.kMaximumTTL);
std::optional<std::string> auth_header = GetAuthHeader(vapid_key);
if (!auth_header) {
DLOG(ERROR) << "Failed to create JWT";
InvokeWebPushCallback(std::move(callback),
SendWebPushMessageResult::kCreateJWTFailed);
return;
}
std::unique_ptr<network::SimpleURLLoader> url_loader = BuildURLLoader(
fcm_token, message.time_to_live, GetUrgencyHeader(message.urgency),
*auth_header, message.payload);
network::SimpleURLLoader* const url_loader_ptr = url_loader.get();
url_loader_ptr->DownloadToString(
url_loader_factory_.get(),
base::BindOnce(&WebPushSender::OnMessageSent,
weak_ptr_factory_.GetWeakPtr(), std::move(url_loader),
std::move(callback)),
message.payload.size());
}
void WebPushSender::OnMessageSent(
std::unique_ptr<network::SimpleURLLoader> url_loader,
WebPushCallback callback,
std::optional<std::string> response_body) {
int net_error = url_loader->NetError();
if (net_error != net::OK) {
if (net_error == net::ERR_INSUFFICIENT_RESOURCES) {
DLOG(ERROR) << "VAPID key invalid";
InvokeWebPushCallback(std::move(callback),
SendWebPushMessageResult::kVapidKeyInvalid);
} else {
DLOG(ERROR) << "Network Error: " << net_error;
InvokeWebPushCallback(std::move(callback),
SendWebPushMessageResult::kNetworkError);
}
return;
}
if (!url_loader->ResponseInfo() || !url_loader->ResponseInfo()->headers) {
DLOG(ERROR) << "Response info not found";
InvokeWebPushCallback(std::move(callback),
SendWebPushMessageResult::kServerError);
return;
}
scoped_refptr<net::HttpResponseHeaders> response_headers =
url_loader->ResponseInfo()->headers;
int response_code = response_headers->response_code();
if (response_code == net::HTTP_NOT_FOUND || response_code == net::HTTP_GONE) {
DLOG(ERROR) << "Device no longer registered";
InvokeWebPushCallback(std::move(callback),
SendWebPushMessageResult::kDeviceGone);
return;
}
// Note: FCM is not following spec and returns 400 for payload too large.
if (response_code == net::HTTP_REQUEST_ENTITY_TOO_LARGE ||
response_code == net::HTTP_BAD_REQUEST) {
DLOG(ERROR) << "Payload too large";
InvokeWebPushCallback(std::move(callback),
SendWebPushMessageResult::kPayloadTooLarge);
return;
}
if (!network::IsSuccessfulStatus(response_code)) {
DLOG(ERROR) << "HTTP Error: " << response_code;
InvokeWebPushCallback(std::move(callback),
SendWebPushMessageResult::kServerError);
return;
}
std::string location;
if (!response_headers->EnumerateHeader(nullptr, "location", &location)) {
DLOG(ERROR) << "Failed to get location header from response";
InvokeWebPushCallback(std::move(callback),
SendWebPushMessageResult::kParseResponseFailed);
return;
}
size_t slash_pos = location.rfind("/");
if (slash_pos == std::string::npos) {
DLOG(ERROR) << "Failed to parse message_id from location header";
InvokeWebPushCallback(std::move(callback),
SendWebPushMessageResult::kParseResponseFailed);
return;
}
InvokeWebPushCallback(std::move(callback),
SendWebPushMessageResult::kSuccessful,
/*message_id=*/location.substr(slash_pos + 1));
}