blob: 6d6e2dbdc491dd9366fe33be79b2790666420faa [file] [log] [blame]
Avi Drissman4a8573c2022-09-09 19:35:541// Copyright 2019 The Chromium Authors
Alex Chaua76a6e32019-06-26 16:20:012// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
Hira Mahmoodf8310de52024-07-31 02:01:265#include "components/sharing_message/web_push/web_push_sender.h"
Alex Chaua76a6e32019-06-26 16:20:016
7#include <limits.h>
8
Claudio DeSouza301cbd2cf2025-04-18 20:10:059#include <optional>
10
Alex Chaua76a6e32019-06-26 16:20:0111#include "base/base64url.h"
Avi Drissman9269d4ed2023-01-07 01:38:0612#include "base/functional/bind.h"
Alex Chaua76a6e32019-06-26 16:20:0113#include "base/strings/string_number_conversions.h"
14#include "base/strings/stringprintf.h"
Alex Chaua76a6e32019-06-26 16:20:0115#include "components/gcm_driver/crypto/p256_key_util.h"
Hira Mahmoodf8310de52024-07-31 02:01:2616#include "components/sharing_message/web_push/json_web_token_util.h"
Alex Chaua76a6e32019-06-26 16:20:0117#include "net/http/http_request_headers.h"
18#include "net/http/http_status_code.h"
Wang Hui5eb97d92022-09-28 07:37:3119#include "services/network/public/cpp/header_util.h"
Alex Chaua76a6e32019-06-26 16:20:0120#include "services/network/public/cpp/resource_request.h"
21#include "services/network/public/cpp/simple_url_loader.h"
Matt Menkeff79bc82021-07-15 02:01:1722#include "services/network/public/mojom/url_response_head.mojom.h"
Alex Chaua76a6e32019-06-26 16:20:0123#include "url/gurl.h"
24
Alex Chaua76a6e32019-06-26 16:20:0125namespace {
26
27// VAPID header constants.
28const char kClaimsKeyAudience[] = "aud";
29const char kFCMServerAudience[] = "https://fcm.googleapis.com";
30
31const char kClaimsKeyExpirationTime[] = "exp";
Alex Chaueb9c9c112019-12-05 16:38:2032// It's 12 hours rather than 24 hours to avoid any issues with clock differences
33// between the sending application and the push service.
Peter Kastinge5a38ed2021-10-02 03:06:3534constexpr base::TimeDelta kClaimsValidPeriod = base::Hours(12);
Alex Chaua76a6e32019-06-26 16:20:0135
36const char kAuthorizationRequestHeaderFormat[] = "vapid t=%s, k=%s";
37
38// Endpoint constants.
39const char kFCMServerUrlFormat[] = "https://fcm.googleapis.com/fcm/send/%s";
40
41// HTTP header constants.
42const char kTTL[] = "TTL";
Richard Knoll1b846ce2019-07-24 09:37:1243const char kUrgency[] = "Urgency";
Alex Chaua76a6e32019-06-26 16:20:0144
45const char kContentEncodingProperty[] = "content-encoding";
46const char kContentCodingAes128Gcm[] = "aes128gcm";
47
48// Other constants.
49const char kContentEncodingOctetStream[] = "application/octet-stream";
Alex Chaua76a6e32019-06-26 16:20:0150
Arthur Sonzognife132ee2024-01-15 11:01:0451std::optional<std::string> GetAuthHeader(crypto::ECPrivateKey* vapid_key) {
Victor Tanbe20e4e02023-03-23 17:15:1952 base::Value::Dict claims;
53 claims.Set(kClaimsKeyAudience, base::Value(kFCMServerAudience));
Alex Chaua76a6e32019-06-26 16:20:0154
55 int64_t exp =
Alex Chaueb9c9c112019-12-05 16:38:2056 (base::Time::Now() + kClaimsValidPeriod - base::Time::UnixEpoch())
57 .InSeconds();
Alex Chaua76a6e32019-06-26 16:20:0158 // TODO: Year 2038 problem, base::Value does not support int64_t.
Hira Mahmoodf8310de52024-07-31 02:01:2659 if (exp > INT_MAX) {
Arthur Sonzognife132ee2024-01-15 11:01:0460 return std::nullopt;
Hira Mahmoodf8310de52024-07-31 02:01:2661 }
Alex Chaua76a6e32019-06-26 16:20:0162
Victor Tanbe20e4e02023-03-23 17:15:1963 claims.Set(kClaimsKeyExpirationTime, base::Value(static_cast<int32_t>(exp)));
Alex Chaua76a6e32019-06-26 16:20:0164
Arthur Sonzognife132ee2024-01-15 11:01:0465 std::optional<std::string> jwt = CreateJSONWebToken(claims, vapid_key);
Hira Mahmoodf8310de52024-07-31 02:01:2666 if (!jwt) {
Arthur Sonzognife132ee2024-01-15 11:01:0467 return std::nullopt;
Hira Mahmoodf8310de52024-07-31 02:01:2668 }
Alex Chaua76a6e32019-06-26 16:20:0169
70 std::string public_key;
Hira Mahmoodf8310de52024-07-31 02:01:2671 if (!gcm::GetRawPublicKey(*vapid_key, &public_key)) {
Arthur Sonzognife132ee2024-01-15 11:01:0472 return std::nullopt;
Hira Mahmoodf8310de52024-07-31 02:01:2673 }
Alex Chaua76a6e32019-06-26 16:20:0174
75 std::string base64_public_key;
76 base::Base64UrlEncode(public_key, base::Base64UrlEncodePolicy::OMIT_PADDING,
77 &base64_public_key);
78
79 return base::StringPrintf(kAuthorizationRequestHeaderFormat, jwt->c_str(),
80 base64_public_key.c_str());
81}
82
Richard Knoll1b846ce2019-07-24 09:37:1283std::string GetUrgencyHeader(WebPushMessage::Urgency urgency) {
84 switch (urgency) {
85 case WebPushMessage::Urgency::kVeryLow:
86 return "very-low";
87 case WebPushMessage::Urgency::kLow:
88 return "low";
89 case WebPushMessage::Urgency::kNormal:
90 return "normal";
91 case WebPushMessage::Urgency::kHigh:
92 return "high";
93 }
94}
95
Alex Chaua76a6e32019-06-26 16:20:0196std::unique_ptr<network::SimpleURLLoader> BuildURLLoader(
97 const std::string& fcm_token,
98 int time_to_live,
Richard Knoll1b846ce2019-07-24 09:37:1299 const std::string& urgency_header,
Alex Chaua76a6e32019-06-26 16:20:01100 const std::string& auth_header,
101 const std::string& message) {
102 auto resource_request = std::make_unique<network::ResourceRequest>();
103 std::string server_url =
104 base::StringPrintf(kFCMServerUrlFormat, fcm_token.c_str());
105 resource_request->url = GURL(server_url);
David Benjamin86aa9282019-08-13 22:28:47106 resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
Alex Chaua76a6e32019-06-26 16:20:01107 resource_request->method = "POST";
108 resource_request->headers.SetHeader(net::HttpRequestHeaders::kAuthorization,
109 auth_header);
110 resource_request->headers.SetHeader(kTTL, base::NumberToString(time_to_live));
111 resource_request->headers.SetHeader(kContentEncodingProperty,
112 kContentCodingAes128Gcm);
Richard Knoll1b846ce2019-07-24 09:37:12113 resource_request->headers.SetHeader(kUrgency, urgency_header);
Alex Chaua76a6e32019-06-26 16:20:01114
115 net::NetworkTrafficAnnotationTag traffic_annotation =
116 net::DefineNetworkTrafficAnnotation("web_push_message", R"(
117 semantics {
118 sender: "GCMDriver WebPushSender"
119 description:
120 "Send a request via Firebase to another device that is signed in"
121 "with the same account."
122 trigger: "Users send data to another owned device."
123 data: "Web push message."
124 destination: GOOGLE_OWNED_SERVICE
Hira Mahmoodf8310de52024-07-31 02:01:26125 user_data {
126 type: SENSITIVE_URL
127 type: OTHER
128 }
129 internal {
130 contacts {
131 owners: "//components/sharing_message/OWNERS"
132 }
133 }
134 last_reviewed: "2024-07-16"
Alex Chaua76a6e32019-06-26 16:20:01135 }
136 policy {
137 cookies_allowed: NO
138 setting:
139 "You can enable or disable this feature in Chrome's settings under"
140 "'Sync and Google services'."
141 policy_exception_justification: "Not implemented."
142 }
143 )");
144 std::unique_ptr<network::SimpleURLLoader> loader =
145 network::SimpleURLLoader::Create(std::move(resource_request),
146 traffic_annotation);
147 loader->AttachStringForUpload(message, kContentEncodingOctetStream);
148 loader->SetRetryOptions(1, network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE);
149 loader->SetAllowHttpErrorResults(true);
150
151 return loader;
152}
153
154} // namespace
155
156WebPushSender::WebPushSender(
157 scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
Jeremy Roman5c341f6d2019-07-15 15:56:10158 : url_loader_factory_(std::move(url_loader_factory)) {}
Alex Chaua76a6e32019-06-26 16:20:01159
160WebPushSender::~WebPushSender() = default;
161
162void WebPushSender::SendMessage(const std::string& fcm_token,
163 crypto::ECPrivateKey* vapid_key,
Alex Chaud6eff3c2019-08-20 15:57:09164 WebPushMessage message,
165 WebPushCallback callback) {
Alex Chaua76a6e32019-06-26 16:20:01166 DCHECK(!fcm_token.empty());
167 DCHECK(vapid_key);
168 DCHECK_LE(message.time_to_live, message.kMaximumTTL);
169
Arthur Sonzognife132ee2024-01-15 11:01:04170 std::optional<std::string> auth_header = GetAuthHeader(vapid_key);
Alex Chaua76a6e32019-06-26 16:20:01171 if (!auth_header) {
Alex Chaud6eff3c2019-08-20 15:57:09172 DLOG(ERROR) << "Failed to create JWT";
173 InvokeWebPushCallback(std::move(callback),
174 SendWebPushMessageResult::kCreateJWTFailed);
Alex Chaua76a6e32019-06-26 16:20:01175 return;
176 }
177
178 std::unique_ptr<network::SimpleURLLoader> url_loader = BuildURLLoader(
Richard Knoll1b846ce2019-07-24 09:37:12179 fcm_token, message.time_to_live, GetUrgencyHeader(message.urgency),
180 *auth_header, message.payload);
Maksim Ivanov2e1e82ab2020-09-01 22:19:22181 network::SimpleURLLoader* const url_loader_ptr = url_loader.get();
182 url_loader_ptr->DownloadToString(
Alex Chaua76a6e32019-06-26 16:20:01183 url_loader_factory_.get(),
184 base::BindOnce(&WebPushSender::OnMessageSent,
Alex Chau0046e012019-07-04 15:28:11185 weak_ptr_factory_.GetWeakPtr(), std::move(url_loader),
186 std::move(callback)),
Alex Chaud6eff3c2019-08-20 15:57:09187 message.payload.size());
Alex Chaua76a6e32019-06-26 16:20:01188}
189
190void WebPushSender::OnMessageSent(
Alex Chaua76a6e32019-06-26 16:20:01191 std::unique_ptr<network::SimpleURLLoader> url_loader,
Alex Chaud6eff3c2019-08-20 15:57:09192 WebPushCallback callback,
Claudio DeSouza301cbd2cf2025-04-18 20:10:05193 std::optional<std::string> response_body) {
Alex Chaua76a6e32019-06-26 16:20:01194 int net_error = url_loader->NetError();
195 if (net_error != net::OK) {
Alex Chau36b830a2019-11-12 14:19:15196 if (net_error == net::ERR_INSUFFICIENT_RESOURCES) {
197 DLOG(ERROR) << "VAPID key invalid";
198 InvokeWebPushCallback(std::move(callback),
199 SendWebPushMessageResult::kVapidKeyInvalid);
200 } else {
201 DLOG(ERROR) << "Network Error: " << net_error;
202 InvokeWebPushCallback(std::move(callback),
203 SendWebPushMessageResult::kNetworkError);
204 }
Alex Chaua76a6e32019-06-26 16:20:01205 return;
206 }
207
Alex Chau36b830a2019-11-12 14:19:15208 if (!url_loader->ResponseInfo() || !url_loader->ResponseInfo()->headers) {
Alex Chaud6eff3c2019-08-20 15:57:09209 DLOG(ERROR) << "Response info not found";
210 InvokeWebPushCallback(std::move(callback),
211 SendWebPushMessageResult::kServerError);
Alex Chaua76a6e32019-06-26 16:20:01212 return;
213 }
214
Alex Chau36b830a2019-11-12 14:19:15215 scoped_refptr<net::HttpResponseHeaders> response_headers =
216 url_loader->ResponseInfo()->headers;
Alex Chau0046e012019-07-04 15:28:11217 int response_code = response_headers->response_code();
Alex Chaud6eff3c2019-08-20 15:57:09218 if (response_code == net::HTTP_NOT_FOUND || response_code == net::HTTP_GONE) {
219 DLOG(ERROR) << "Device no longer registered";
220 InvokeWebPushCallback(std::move(callback),
221 SendWebPushMessageResult::kDeviceGone);
222 return;
223 }
224
225 // Note: FCM is not following spec and returns 400 for payload too large.
226 if (response_code == net::HTTP_REQUEST_ENTITY_TOO_LARGE ||
227 response_code == net::HTTP_BAD_REQUEST) {
228 DLOG(ERROR) << "Payload too large";
229 InvokeWebPushCallback(std::move(callback),
230 SendWebPushMessageResult::kPayloadTooLarge);
231 return;
232 }
233
Wang Hui5eb97d92022-09-28 07:37:31234 if (!network::IsSuccessfulStatus(response_code)) {
Alex Chaud6eff3c2019-08-20 15:57:09235 DLOG(ERROR) << "HTTP Error: " << response_code;
236 InvokeWebPushCallback(std::move(callback),
237 SendWebPushMessageResult::kServerError);
Alex Chaua76a6e32019-06-26 16:20:01238 return;
239 }
240
Alex Chau0046e012019-07-04 15:28:11241 std::string location;
242 if (!response_headers->EnumerateHeader(nullptr, "location", &location)) {
Alex Chaud6eff3c2019-08-20 15:57:09243 DLOG(ERROR) << "Failed to get location header from response";
244 InvokeWebPushCallback(std::move(callback),
245 SendWebPushMessageResult::kParseResponseFailed);
Alex Chau0046e012019-07-04 15:28:11246 return;
247 }
248
249 size_t slash_pos = location.rfind("/");
250 if (slash_pos == std::string::npos) {
Alex Chaud6eff3c2019-08-20 15:57:09251 DLOG(ERROR) << "Failed to parse message_id from location header";
252 InvokeWebPushCallback(std::move(callback),
253 SendWebPushMessageResult::kParseResponseFailed);
Alex Chau0046e012019-07-04 15:28:11254 return;
255 }
256
Alex Chaud6eff3c2019-08-20 15:57:09257 InvokeWebPushCallback(std::move(callback),
258 SendWebPushMessageResult::kSuccessful,
Alex Chau36b830a2019-11-12 14:19:15259 /*message_id=*/location.substr(slash_pos + 1));
Alex Chaua76a6e32019-06-26 16:20:01260}