blob: 5527cf8f31e3fb53974ac0a6d6ee4cfc95d24f7a [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
Alex Chauda2073d62020-01-29 11:26:085#include "chrome/browser/sharing/web_push/web_push_sender.h"
Alex Chaua76a6e32019-06-26 16:20:016
7#include <limits.h>
8
9#include "base/base64url.h"
10#include "base/bind.h"
Alex Chaua76a6e32019-06-26 16:20:0111#include "base/strings/string_number_conversions.h"
12#include "base/strings/stringprintf.h"
Alex Chauda2073d62020-01-29 11:26:0813#include "chrome/browser/sharing/web_push/json_web_token_util.h"
Alex Chaua76a6e32019-06-26 16:20:0114#include "components/gcm_driver/crypto/p256_key_util.h"
Alex Chaua76a6e32019-06-26 16:20:0115#include "net/http/http_request_headers.h"
16#include "net/http/http_status_code.h"
Wang Hui5eb97d92022-09-28 07:37:3117#include "services/network/public/cpp/header_util.h"
Alex Chaua76a6e32019-06-26 16:20:0118#include "services/network/public/cpp/resource_request.h"
19#include "services/network/public/cpp/simple_url_loader.h"
Matt Menkeff79bc82021-07-15 02:01:1720#include "services/network/public/mojom/url_response_head.mojom.h"
Alex Chaua76a6e32019-06-26 16:20:0121#include "url/gurl.h"
22
Alex Chaua76a6e32019-06-26 16:20:0123namespace {
24
25// VAPID header constants.
26const char kClaimsKeyAudience[] = "aud";
27const char kFCMServerAudience[] = "https://fcm.googleapis.com";
28
29const char kClaimsKeyExpirationTime[] = "exp";
Alex Chaueb9c9c112019-12-05 16:38:2030// It's 12 hours rather than 24 hours to avoid any issues with clock differences
31// between the sending application and the push service.
Peter Kastinge5a38ed2021-10-02 03:06:3532constexpr base::TimeDelta kClaimsValidPeriod = base::Hours(12);
Alex Chaua76a6e32019-06-26 16:20:0133
34const char kAuthorizationRequestHeaderFormat[] = "vapid t=%s, k=%s";
35
36// Endpoint constants.
37const char kFCMServerUrlFormat[] = "https://fcm.googleapis.com/fcm/send/%s";
38
39// HTTP header constants.
40const char kTTL[] = "TTL";
Richard Knoll1b846ce2019-07-24 09:37:1241const char kUrgency[] = "Urgency";
Alex Chaua76a6e32019-06-26 16:20:0142
43const char kContentEncodingProperty[] = "content-encoding";
44const char kContentCodingAes128Gcm[] = "aes128gcm";
45
46// Other constants.
47const char kContentEncodingOctetStream[] = "application/octet-stream";
Alex Chaua76a6e32019-06-26 16:20:0148
Anton Bikineev46bbb972021-05-15 17:53:5349absl::optional<std::string> GetAuthHeader(crypto::ECPrivateKey* vapid_key) {
Alex Chaua76a6e32019-06-26 16:20:0150 base::Value claims(base::Value::Type::DICTIONARY);
51 claims.SetKey(kClaimsKeyAudience, base::Value(kFCMServerAudience));
52
53 int64_t exp =
Alex Chaueb9c9c112019-12-05 16:38:2054 (base::Time::Now() + kClaimsValidPeriod - base::Time::UnixEpoch())
55 .InSeconds();
Alex Chaua76a6e32019-06-26 16:20:0156 // TODO: Year 2038 problem, base::Value does not support int64_t.
57 if (exp > INT_MAX)
Anton Bikineev46bbb972021-05-15 17:53:5358 return absl::nullopt;
Alex Chaua76a6e32019-06-26 16:20:0159
60 claims.SetKey(kClaimsKeyExpirationTime,
61 base::Value(static_cast<int32_t>(exp)));
62
Anton Bikineev46bbb972021-05-15 17:53:5363 absl::optional<std::string> jwt = CreateJSONWebToken(claims, vapid_key);
Alex Chaua76a6e32019-06-26 16:20:0164 if (!jwt)
Anton Bikineev46bbb972021-05-15 17:53:5365 return absl::nullopt;
Alex Chaua76a6e32019-06-26 16:20:0166
67 std::string public_key;
Alex Chauda2073d62020-01-29 11:26:0868 if (!gcm::GetRawPublicKey(*vapid_key, &public_key))
Anton Bikineev46bbb972021-05-15 17:53:5369 return absl::nullopt;
Alex Chaua76a6e32019-06-26 16:20:0170
71 std::string base64_public_key;
72 base::Base64UrlEncode(public_key, base::Base64UrlEncodePolicy::OMIT_PADDING,
73 &base64_public_key);
74
75 return base::StringPrintf(kAuthorizationRequestHeaderFormat, jwt->c_str(),
76 base64_public_key.c_str());
77}
78
Richard Knoll1b846ce2019-07-24 09:37:1279std::string GetUrgencyHeader(WebPushMessage::Urgency urgency) {
80 switch (urgency) {
81 case WebPushMessage::Urgency::kVeryLow:
82 return "very-low";
83 case WebPushMessage::Urgency::kLow:
84 return "low";
85 case WebPushMessage::Urgency::kNormal:
86 return "normal";
87 case WebPushMessage::Urgency::kHigh:
88 return "high";
89 }
90}
91
Alex Chaua76a6e32019-06-26 16:20:0192std::unique_ptr<network::SimpleURLLoader> BuildURLLoader(
93 const std::string& fcm_token,
94 int time_to_live,
Richard Knoll1b846ce2019-07-24 09:37:1295 const std::string& urgency_header,
Alex Chaua76a6e32019-06-26 16:20:0196 const std::string& auth_header,
97 const std::string& message) {
98 auto resource_request = std::make_unique<network::ResourceRequest>();
99 std::string server_url =
100 base::StringPrintf(kFCMServerUrlFormat, fcm_token.c_str());
101 resource_request->url = GURL(server_url);
David Benjamin86aa9282019-08-13 22:28:47102 resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
Alex Chaua76a6e32019-06-26 16:20:01103 resource_request->method = "POST";
104 resource_request->headers.SetHeader(net::HttpRequestHeaders::kAuthorization,
105 auth_header);
106 resource_request->headers.SetHeader(kTTL, base::NumberToString(time_to_live));
107 resource_request->headers.SetHeader(kContentEncodingProperty,
108 kContentCodingAes128Gcm);
Richard Knoll1b846ce2019-07-24 09:37:12109 resource_request->headers.SetHeader(kUrgency, urgency_header);
Alex Chaua76a6e32019-06-26 16:20:01110
111 net::NetworkTrafficAnnotationTag traffic_annotation =
112 net::DefineNetworkTrafficAnnotation("web_push_message", R"(
113 semantics {
114 sender: "GCMDriver WebPushSender"
115 description:
116 "Send a request via Firebase to another device that is signed in"
117 "with the same account."
118 trigger: "Users send data to another owned device."
119 data: "Web push message."
120 destination: GOOGLE_OWNED_SERVICE
121 }
122 policy {
123 cookies_allowed: NO
124 setting:
125 "You can enable or disable this feature in Chrome's settings under"
126 "'Sync and Google services'."
127 policy_exception_justification: "Not implemented."
128 }
129 )");
130 std::unique_ptr<network::SimpleURLLoader> loader =
131 network::SimpleURLLoader::Create(std::move(resource_request),
132 traffic_annotation);
133 loader->AttachStringForUpload(message, kContentEncodingOctetStream);
134 loader->SetRetryOptions(1, network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE);
135 loader->SetAllowHttpErrorResults(true);
136
137 return loader;
138}
139
140} // namespace
141
142WebPushSender::WebPushSender(
143 scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
Jeremy Roman5c341f6d2019-07-15 15:56:10144 : url_loader_factory_(std::move(url_loader_factory)) {}
Alex Chaua76a6e32019-06-26 16:20:01145
146WebPushSender::~WebPushSender() = default;
147
148void WebPushSender::SendMessage(const std::string& fcm_token,
149 crypto::ECPrivateKey* vapid_key,
Alex Chaud6eff3c2019-08-20 15:57:09150 WebPushMessage message,
151 WebPushCallback callback) {
Alex Chaua76a6e32019-06-26 16:20:01152 DCHECK(!fcm_token.empty());
153 DCHECK(vapid_key);
154 DCHECK_LE(message.time_to_live, message.kMaximumTTL);
155
Anton Bikineev46bbb972021-05-15 17:53:53156 absl::optional<std::string> auth_header = GetAuthHeader(vapid_key);
Alex Chaua76a6e32019-06-26 16:20:01157 if (!auth_header) {
Alex Chaud6eff3c2019-08-20 15:57:09158 DLOG(ERROR) << "Failed to create JWT";
159 InvokeWebPushCallback(std::move(callback),
160 SendWebPushMessageResult::kCreateJWTFailed);
Alex Chaua76a6e32019-06-26 16:20:01161 return;
162 }
163
Alex Chau55dccbf2019-07-23 16:27:09164 LogSendWebPushMessagePayloadSize(message.payload.size());
Alex Chaua76a6e32019-06-26 16:20:01165 std::unique_ptr<network::SimpleURLLoader> url_loader = BuildURLLoader(
Richard Knoll1b846ce2019-07-24 09:37:12166 fcm_token, message.time_to_live, GetUrgencyHeader(message.urgency),
167 *auth_header, message.payload);
Maksim Ivanov2e1e82ab2020-09-01 22:19:22168 network::SimpleURLLoader* const url_loader_ptr = url_loader.get();
169 url_loader_ptr->DownloadToString(
Alex Chaua76a6e32019-06-26 16:20:01170 url_loader_factory_.get(),
171 base::BindOnce(&WebPushSender::OnMessageSent,
Alex Chau0046e012019-07-04 15:28:11172 weak_ptr_factory_.GetWeakPtr(), std::move(url_loader),
173 std::move(callback)),
Alex Chaud6eff3c2019-08-20 15:57:09174 message.payload.size());
Alex Chaua76a6e32019-06-26 16:20:01175}
176
177void WebPushSender::OnMessageSent(
Alex Chaua76a6e32019-06-26 16:20:01178 std::unique_ptr<network::SimpleURLLoader> url_loader,
Alex Chaud6eff3c2019-08-20 15:57:09179 WebPushCallback callback,
Alex Chaua76a6e32019-06-26 16:20:01180 std::unique_ptr<std::string> response_body) {
181 int net_error = url_loader->NetError();
Alex Chaua76a6e32019-06-26 16:20:01182 if (net_error != net::OK) {
Alex Chau36b830a2019-11-12 14:19:15183 LogSendWebPushMessageStatusCode(net_error);
184 if (net_error == net::ERR_INSUFFICIENT_RESOURCES) {
185 DLOG(ERROR) << "VAPID key invalid";
186 InvokeWebPushCallback(std::move(callback),
187 SendWebPushMessageResult::kVapidKeyInvalid);
188 } else {
189 DLOG(ERROR) << "Network Error: " << net_error;
190 InvokeWebPushCallback(std::move(callback),
191 SendWebPushMessageResult::kNetworkError);
192 }
Alex Chaua76a6e32019-06-26 16:20:01193 return;
194 }
195
Alex Chau36b830a2019-11-12 14:19:15196 if (!url_loader->ResponseInfo() || !url_loader->ResponseInfo()->headers) {
197 LogSendWebPushMessageStatusCode(net::OK);
Alex Chaud6eff3c2019-08-20 15:57:09198 DLOG(ERROR) << "Response info not found";
199 InvokeWebPushCallback(std::move(callback),
200 SendWebPushMessageResult::kServerError);
Alex Chaua76a6e32019-06-26 16:20:01201 return;
202 }
203
Alex Chau36b830a2019-11-12 14:19:15204 scoped_refptr<net::HttpResponseHeaders> response_headers =
205 url_loader->ResponseInfo()->headers;
Alex Chau0046e012019-07-04 15:28:11206 int response_code = response_headers->response_code();
Alex Chau36b830a2019-11-12 14:19:15207 LogSendWebPushMessageStatusCode(response_code);
Alex Chaud6eff3c2019-08-20 15:57:09208 if (response_code == net::HTTP_NOT_FOUND || response_code == net::HTTP_GONE) {
209 DLOG(ERROR) << "Device no longer registered";
210 InvokeWebPushCallback(std::move(callback),
211 SendWebPushMessageResult::kDeviceGone);
212 return;
213 }
214
215 // Note: FCM is not following spec and returns 400 for payload too large.
216 if (response_code == net::HTTP_REQUEST_ENTITY_TOO_LARGE ||
217 response_code == net::HTTP_BAD_REQUEST) {
218 DLOG(ERROR) << "Payload too large";
219 InvokeWebPushCallback(std::move(callback),
220 SendWebPushMessageResult::kPayloadTooLarge);
221 return;
222 }
223
Wang Hui5eb97d92022-09-28 07:37:31224 if (!network::IsSuccessfulStatus(response_code)) {
Alex Chaud6eff3c2019-08-20 15:57:09225 DLOG(ERROR) << "HTTP Error: " << response_code;
Alex Chaud6eff3c2019-08-20 15:57:09226 InvokeWebPushCallback(std::move(callback),
227 SendWebPushMessageResult::kServerError);
Alex Chaua76a6e32019-06-26 16:20:01228 return;
229 }
230
Alex Chau0046e012019-07-04 15:28:11231 std::string location;
232 if (!response_headers->EnumerateHeader(nullptr, "location", &location)) {
Alex Chaud6eff3c2019-08-20 15:57:09233 DLOG(ERROR) << "Failed to get location header from response";
234 InvokeWebPushCallback(std::move(callback),
235 SendWebPushMessageResult::kParseResponseFailed);
Alex Chau0046e012019-07-04 15:28:11236 return;
237 }
238
239 size_t slash_pos = location.rfind("/");
240 if (slash_pos == std::string::npos) {
Alex Chaud6eff3c2019-08-20 15:57:09241 DLOG(ERROR) << "Failed to parse message_id from location header";
242 InvokeWebPushCallback(std::move(callback),
243 SendWebPushMessageResult::kParseResponseFailed);
Alex Chau0046e012019-07-04 15:28:11244 return;
245 }
246
Alex Chaud6eff3c2019-08-20 15:57:09247 InvokeWebPushCallback(std::move(callback),
248 SendWebPushMessageResult::kSuccessful,
Alex Chau36b830a2019-11-12 14:19:15249 /*message_id=*/location.substr(slash_pos + 1));
Alex Chaua76a6e32019-06-26 16:20:01250}