Avi Drissman | 4a8573c | 2022-09-09 19:35:54 | [diff] [blame] | 1 | // Copyright 2019 The Chromium Authors |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
Hira Mahmood | f8310de5 | 2024-07-31 02:01:26 | [diff] [blame] | 5 | #include "components/sharing_message/web_push/web_push_sender.h" |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 6 | |
| 7 | #include <limits.h> |
| 8 | |
Claudio DeSouza | 301cbd2cf | 2025-04-18 20:10:05 | [diff] [blame] | 9 | #include <optional> |
| 10 | |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 11 | #include "base/base64url.h" |
Avi Drissman | 9269d4ed | 2023-01-07 01:38:06 | [diff] [blame] | 12 | #include "base/functional/bind.h" |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 13 | #include "base/strings/string_number_conversions.h" |
| 14 | #include "base/strings/stringprintf.h" |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 15 | #include "components/gcm_driver/crypto/p256_key_util.h" |
Hira Mahmood | f8310de5 | 2024-07-31 02:01:26 | [diff] [blame] | 16 | #include "components/sharing_message/web_push/json_web_token_util.h" |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 17 | #include "net/http/http_request_headers.h" |
| 18 | #include "net/http/http_status_code.h" |
Wang Hui | 5eb97d9 | 2022-09-28 07:37:31 | [diff] [blame] | 19 | #include "services/network/public/cpp/header_util.h" |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 20 | #include "services/network/public/cpp/resource_request.h" |
| 21 | #include "services/network/public/cpp/simple_url_loader.h" |
Matt Menke | ff79bc8 | 2021-07-15 02:01:17 | [diff] [blame] | 22 | #include "services/network/public/mojom/url_response_head.mojom.h" |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 23 | #include "url/gurl.h" |
| 24 | |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 25 | namespace { |
| 26 | |
| 27 | // VAPID header constants. |
| 28 | const char kClaimsKeyAudience[] = "aud"; |
| 29 | const char kFCMServerAudience[] = "https://fcm.googleapis.com"; |
| 30 | |
| 31 | const char kClaimsKeyExpirationTime[] = "exp"; |
Alex Chau | eb9c9c11 | 2019-12-05 16:38:20 | [diff] [blame] | 32 | // 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 Kasting | e5a38ed | 2021-10-02 03:06:35 | [diff] [blame] | 34 | constexpr base::TimeDelta kClaimsValidPeriod = base::Hours(12); |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 35 | |
| 36 | const char kAuthorizationRequestHeaderFormat[] = "vapid t=%s, k=%s"; |
| 37 | |
| 38 | // Endpoint constants. |
| 39 | const char kFCMServerUrlFormat[] = "https://fcm.googleapis.com/fcm/send/%s"; |
| 40 | |
| 41 | // HTTP header constants. |
| 42 | const char kTTL[] = "TTL"; |
Richard Knoll | 1b846ce | 2019-07-24 09:37:12 | [diff] [blame] | 43 | const char kUrgency[] = "Urgency"; |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 44 | |
| 45 | const char kContentEncodingProperty[] = "content-encoding"; |
| 46 | const char kContentCodingAes128Gcm[] = "aes128gcm"; |
| 47 | |
| 48 | // Other constants. |
| 49 | const char kContentEncodingOctetStream[] = "application/octet-stream"; |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 50 | |
Arthur Sonzogni | fe132ee | 2024-01-15 11:01:04 | [diff] [blame] | 51 | std::optional<std::string> GetAuthHeader(crypto::ECPrivateKey* vapid_key) { |
Victor Tan | be20e4e0 | 2023-03-23 17:15:19 | [diff] [blame] | 52 | base::Value::Dict claims; |
| 53 | claims.Set(kClaimsKeyAudience, base::Value(kFCMServerAudience)); |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 54 | |
| 55 | int64_t exp = |
Alex Chau | eb9c9c11 | 2019-12-05 16:38:20 | [diff] [blame] | 56 | (base::Time::Now() + kClaimsValidPeriod - base::Time::UnixEpoch()) |
| 57 | .InSeconds(); |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 58 | // TODO: Year 2038 problem, base::Value does not support int64_t. |
Hira Mahmood | f8310de5 | 2024-07-31 02:01:26 | [diff] [blame] | 59 | if (exp > INT_MAX) { |
Arthur Sonzogni | fe132ee | 2024-01-15 11:01:04 | [diff] [blame] | 60 | return std::nullopt; |
Hira Mahmood | f8310de5 | 2024-07-31 02:01:26 | [diff] [blame] | 61 | } |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 62 | |
Victor Tan | be20e4e0 | 2023-03-23 17:15:19 | [diff] [blame] | 63 | claims.Set(kClaimsKeyExpirationTime, base::Value(static_cast<int32_t>(exp))); |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 64 | |
Arthur Sonzogni | fe132ee | 2024-01-15 11:01:04 | [diff] [blame] | 65 | std::optional<std::string> jwt = CreateJSONWebToken(claims, vapid_key); |
Hira Mahmood | f8310de5 | 2024-07-31 02:01:26 | [diff] [blame] | 66 | if (!jwt) { |
Arthur Sonzogni | fe132ee | 2024-01-15 11:01:04 | [diff] [blame] | 67 | return std::nullopt; |
Hira Mahmood | f8310de5 | 2024-07-31 02:01:26 | [diff] [blame] | 68 | } |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 69 | |
| 70 | std::string public_key; |
Hira Mahmood | f8310de5 | 2024-07-31 02:01:26 | [diff] [blame] | 71 | if (!gcm::GetRawPublicKey(*vapid_key, &public_key)) { |
Arthur Sonzogni | fe132ee | 2024-01-15 11:01:04 | [diff] [blame] | 72 | return std::nullopt; |
Hira Mahmood | f8310de5 | 2024-07-31 02:01:26 | [diff] [blame] | 73 | } |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 74 | |
| 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 Knoll | 1b846ce | 2019-07-24 09:37:12 | [diff] [blame] | 83 | std::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 Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 96 | std::unique_ptr<network::SimpleURLLoader> BuildURLLoader( |
| 97 | const std::string& fcm_token, |
| 98 | int time_to_live, |
Richard Knoll | 1b846ce | 2019-07-24 09:37:12 | [diff] [blame] | 99 | const std::string& urgency_header, |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 100 | 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 Benjamin | 86aa928 | 2019-08-13 22:28:47 | [diff] [blame] | 106 | resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit; |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 107 | 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 Knoll | 1b846ce | 2019-07-24 09:37:12 | [diff] [blame] | 113 | resource_request->headers.SetHeader(kUrgency, urgency_header); |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 114 | |
| 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 Mahmood | f8310de5 | 2024-07-31 02:01:26 | [diff] [blame] | 125 | 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 Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 135 | } |
| 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 | |
| 156 | WebPushSender::WebPushSender( |
| 157 | scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) |
Jeremy Roman | 5c341f6d | 2019-07-15 15:56:10 | [diff] [blame] | 158 | : url_loader_factory_(std::move(url_loader_factory)) {} |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 159 | |
| 160 | WebPushSender::~WebPushSender() = default; |
| 161 | |
| 162 | void WebPushSender::SendMessage(const std::string& fcm_token, |
| 163 | crypto::ECPrivateKey* vapid_key, |
Alex Chau | d6eff3c | 2019-08-20 15:57:09 | [diff] [blame] | 164 | WebPushMessage message, |
| 165 | WebPushCallback callback) { |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 166 | DCHECK(!fcm_token.empty()); |
| 167 | DCHECK(vapid_key); |
| 168 | DCHECK_LE(message.time_to_live, message.kMaximumTTL); |
| 169 | |
Arthur Sonzogni | fe132ee | 2024-01-15 11:01:04 | [diff] [blame] | 170 | std::optional<std::string> auth_header = GetAuthHeader(vapid_key); |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 171 | if (!auth_header) { |
Alex Chau | d6eff3c | 2019-08-20 15:57:09 | [diff] [blame] | 172 | DLOG(ERROR) << "Failed to create JWT"; |
| 173 | InvokeWebPushCallback(std::move(callback), |
| 174 | SendWebPushMessageResult::kCreateJWTFailed); |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 175 | return; |
| 176 | } |
| 177 | |
| 178 | std::unique_ptr<network::SimpleURLLoader> url_loader = BuildURLLoader( |
Richard Knoll | 1b846ce | 2019-07-24 09:37:12 | [diff] [blame] | 179 | fcm_token, message.time_to_live, GetUrgencyHeader(message.urgency), |
| 180 | *auth_header, message.payload); |
Maksim Ivanov | 2e1e82ab | 2020-09-01 22:19:22 | [diff] [blame] | 181 | network::SimpleURLLoader* const url_loader_ptr = url_loader.get(); |
| 182 | url_loader_ptr->DownloadToString( |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 183 | url_loader_factory_.get(), |
| 184 | base::BindOnce(&WebPushSender::OnMessageSent, |
Alex Chau | 0046e01 | 2019-07-04 15:28:11 | [diff] [blame] | 185 | weak_ptr_factory_.GetWeakPtr(), std::move(url_loader), |
| 186 | std::move(callback)), |
Alex Chau | d6eff3c | 2019-08-20 15:57:09 | [diff] [blame] | 187 | message.payload.size()); |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 188 | } |
| 189 | |
| 190 | void WebPushSender::OnMessageSent( |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 191 | std::unique_ptr<network::SimpleURLLoader> url_loader, |
Alex Chau | d6eff3c | 2019-08-20 15:57:09 | [diff] [blame] | 192 | WebPushCallback callback, |
Claudio DeSouza | 301cbd2cf | 2025-04-18 20:10:05 | [diff] [blame] | 193 | std::optional<std::string> response_body) { |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 194 | int net_error = url_loader->NetError(); |
| 195 | if (net_error != net::OK) { |
Alex Chau | 36b830a | 2019-11-12 14:19:15 | [diff] [blame] | 196 | 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 Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 205 | return; |
| 206 | } |
| 207 | |
Alex Chau | 36b830a | 2019-11-12 14:19:15 | [diff] [blame] | 208 | if (!url_loader->ResponseInfo() || !url_loader->ResponseInfo()->headers) { |
Alex Chau | d6eff3c | 2019-08-20 15:57:09 | [diff] [blame] | 209 | DLOG(ERROR) << "Response info not found"; |
| 210 | InvokeWebPushCallback(std::move(callback), |
| 211 | SendWebPushMessageResult::kServerError); |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 212 | return; |
| 213 | } |
| 214 | |
Alex Chau | 36b830a | 2019-11-12 14:19:15 | [diff] [blame] | 215 | scoped_refptr<net::HttpResponseHeaders> response_headers = |
| 216 | url_loader->ResponseInfo()->headers; |
Alex Chau | 0046e01 | 2019-07-04 15:28:11 | [diff] [blame] | 217 | int response_code = response_headers->response_code(); |
Alex Chau | d6eff3c | 2019-08-20 15:57:09 | [diff] [blame] | 218 | 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 Hui | 5eb97d9 | 2022-09-28 07:37:31 | [diff] [blame] | 234 | if (!network::IsSuccessfulStatus(response_code)) { |
Alex Chau | d6eff3c | 2019-08-20 15:57:09 | [diff] [blame] | 235 | DLOG(ERROR) << "HTTP Error: " << response_code; |
| 236 | InvokeWebPushCallback(std::move(callback), |
| 237 | SendWebPushMessageResult::kServerError); |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 238 | return; |
| 239 | } |
| 240 | |
Alex Chau | 0046e01 | 2019-07-04 15:28:11 | [diff] [blame] | 241 | std::string location; |
| 242 | if (!response_headers->EnumerateHeader(nullptr, "location", &location)) { |
Alex Chau | d6eff3c | 2019-08-20 15:57:09 | [diff] [blame] | 243 | DLOG(ERROR) << "Failed to get location header from response"; |
| 244 | InvokeWebPushCallback(std::move(callback), |
| 245 | SendWebPushMessageResult::kParseResponseFailed); |
Alex Chau | 0046e01 | 2019-07-04 15:28:11 | [diff] [blame] | 246 | return; |
| 247 | } |
| 248 | |
| 249 | size_t slash_pos = location.rfind("/"); |
| 250 | if (slash_pos == std::string::npos) { |
Alex Chau | d6eff3c | 2019-08-20 15:57:09 | [diff] [blame] | 251 | DLOG(ERROR) << "Failed to parse message_id from location header"; |
| 252 | InvokeWebPushCallback(std::move(callback), |
| 253 | SendWebPushMessageResult::kParseResponseFailed); |
Alex Chau | 0046e01 | 2019-07-04 15:28:11 | [diff] [blame] | 254 | return; |
| 255 | } |
| 256 | |
Alex Chau | d6eff3c | 2019-08-20 15:57:09 | [diff] [blame] | 257 | InvokeWebPushCallback(std::move(callback), |
| 258 | SendWebPushMessageResult::kSuccessful, |
Alex Chau | 36b830a | 2019-11-12 14:19:15 | [diff] [blame] | 259 | /*message_id=*/location.substr(slash_pos + 1)); |
Alex Chau | a76a6e3 | 2019-06-26 16:20:01 | [diff] [blame] | 260 | } |