blob: aea3125f25351f6a710f1c77eb91dcb3f2e42970 [file] [log] [blame]
Alex Chaua76a6e32019-06-26 16:20:011// Copyright 2019 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "components/gcm_driver/web_push_sender.h"
6
7#include <limits.h>
8
9#include "base/base64url.h"
10#include "base/bind.h"
Richard Knoll1b846ce2019-07-24 09:37:1211#include "base/no_destructor.h"
Alex Chaua76a6e32019-06-26 16:20:0112#include "base/strings/string_number_conversions.h"
13#include "base/strings/stringprintf.h"
14#include "components/gcm_driver/common/gcm_message.h"
15#include "components/gcm_driver/crypto/json_web_token_util.h"
16#include "components/gcm_driver/crypto/p256_key_util.h"
Alex Chaua4abf92f2019-07-19 10:32:5617#include "components/gcm_driver/web_push_metrics.h"
Alex Chaua76a6e32019-06-26 16:20:0118#include "net/http/http_request_headers.h"
19#include "net/http/http_status_code.h"
20#include "services/network/public/cpp/cors/cors.h"
21#include "services/network/public/cpp/resource_request.h"
22#include "services/network/public/cpp/simple_url_loader.h"
23#include "url/gurl.h"
24
25namespace gcm {
26
27namespace {
28
29// VAPID header constants.
30const char kClaimsKeyAudience[] = "aud";
31const char kFCMServerAudience[] = "https://fcm.googleapis.com";
32
33const char kClaimsKeyExpirationTime[] = "exp";
34
35const char kAuthorizationRequestHeaderFormat[] = "vapid t=%s, k=%s";
36
37// Endpoint constants.
38const char kFCMServerUrlFormat[] = "https://fcm.googleapis.com/fcm/send/%s";
39
40// HTTP header constants.
41const char kTTL[] = "TTL";
Richard Knoll1b846ce2019-07-24 09:37:1242const char kUrgency[] = "Urgency";
Alex Chaua76a6e32019-06-26 16:20:0143
44const char kContentEncodingProperty[] = "content-encoding";
45const char kContentCodingAes128Gcm[] = "aes128gcm";
46
47// Other constants.
48const char kContentEncodingOctetStream[] = "application/octet-stream";
49const int kMaximumBodySize = 4096;
50
51base::Optional<std::string> GetAuthHeader(crypto::ECPrivateKey* vapid_key,
52 int time_to_live) {
53 base::Value claims(base::Value::Type::DICTIONARY);
54 claims.SetKey(kClaimsKeyAudience, base::Value(kFCMServerAudience));
55
56 int64_t exp =
57 (base::Time::Now() - base::Time::UnixEpoch()).InSeconds() + time_to_live;
58 // TODO: Year 2038 problem, base::Value does not support int64_t.
59 if (exp > INT_MAX)
60 return base::nullopt;
61
62 claims.SetKey(kClaimsKeyExpirationTime,
63 base::Value(static_cast<int32_t>(exp)));
64
65 base::Optional<std::string> jwt = CreateJSONWebToken(claims, vapid_key);
66 if (!jwt)
67 return base::nullopt;
68
69 std::string public_key;
70 if (!GetRawPublicKey(*vapid_key, &public_key))
71 return base::nullopt;
72
73 std::string base64_public_key;
74 base::Base64UrlEncode(public_key, base::Base64UrlEncodePolicy::OMIT_PADDING,
75 &base64_public_key);
76
77 return base::StringPrintf(kAuthorizationRequestHeaderFormat, jwt->c_str(),
78 base64_public_key.c_str());
79}
80
Richard Knoll1b846ce2019-07-24 09:37:1281std::string GetUrgencyHeader(WebPushMessage::Urgency urgency) {
82 switch (urgency) {
83 case WebPushMessage::Urgency::kVeryLow:
84 return "very-low";
85 case WebPushMessage::Urgency::kLow:
86 return "low";
87 case WebPushMessage::Urgency::kNormal:
88 return "normal";
89 case WebPushMessage::Urgency::kHigh:
90 return "high";
91 }
92}
93
Alex Chaua76a6e32019-06-26 16:20:0194std::unique_ptr<network::SimpleURLLoader> BuildURLLoader(
95 const std::string& fcm_token,
96 int time_to_live,
Richard Knoll1b846ce2019-07-24 09:37:1297 const std::string& urgency_header,
Alex Chaua76a6e32019-06-26 16:20:0198 const std::string& auth_header,
99 const std::string& message) {
100 auto resource_request = std::make_unique<network::ResourceRequest>();
101 std::string server_url =
102 base::StringPrintf(kFCMServerUrlFormat, fcm_token.c_str());
103 resource_request->url = GURL(server_url);
David Benjamin86aa9282019-08-13 22:28:47104 resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
Alex Chaua76a6e32019-06-26 16:20:01105 resource_request->method = "POST";
106 resource_request->headers.SetHeader(net::HttpRequestHeaders::kAuthorization,
107 auth_header);
108 resource_request->headers.SetHeader(kTTL, base::NumberToString(time_to_live));
109 resource_request->headers.SetHeader(kContentEncodingProperty,
110 kContentCodingAes128Gcm);
Richard Knoll1b846ce2019-07-24 09:37:12111 resource_request->headers.SetHeader(kUrgency, urgency_header);
Alex Chaua76a6e32019-06-26 16:20:01112
113 net::NetworkTrafficAnnotationTag traffic_annotation =
114 net::DefineNetworkTrafficAnnotation("web_push_message", R"(
115 semantics {
116 sender: "GCMDriver WebPushSender"
117 description:
118 "Send a request via Firebase to another device that is signed in"
119 "with the same account."
120 trigger: "Users send data to another owned device."
121 data: "Web push message."
122 destination: GOOGLE_OWNED_SERVICE
123 }
124 policy {
125 cookies_allowed: NO
126 setting:
127 "You can enable or disable this feature in Chrome's settings under"
128 "'Sync and Google services'."
129 policy_exception_justification: "Not implemented."
130 }
131 )");
132 std::unique_ptr<network::SimpleURLLoader> loader =
133 network::SimpleURLLoader::Create(std::move(resource_request),
134 traffic_annotation);
135 loader->AttachStringForUpload(message, kContentEncodingOctetStream);
136 loader->SetRetryOptions(1, network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE);
137 loader->SetAllowHttpErrorResults(true);
138
139 return loader;
140}
141
142} // namespace
143
144WebPushSender::WebPushSender(
145 scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
Jeremy Roman5c341f6d2019-07-15 15:56:10146 : url_loader_factory_(std::move(url_loader_factory)) {}
Alex Chaua76a6e32019-06-26 16:20:01147
148WebPushSender::~WebPushSender() = default;
149
150void WebPushSender::SendMessage(const std::string& fcm_token,
151 crypto::ECPrivateKey* vapid_key,
152 const WebPushMessage& message,
153 SendMessageCallback callback) {
154 DCHECK(!fcm_token.empty());
155 DCHECK(vapid_key);
156 DCHECK_LE(message.time_to_live, message.kMaximumTTL);
157
158 base::Optional<std::string> auth_header =
159 GetAuthHeader(vapid_key, message.time_to_live);
160 if (!auth_header) {
161 LOG(ERROR) << "Failed to create JWT";
Alex Chaua4abf92f2019-07-19 10:32:56162 LogSendWebPushMessageResult(SendWebPushMessageResult::kCreateJWTFailed);
Alex Chau0046e012019-07-04 15:28:11163 std::move(callback).Run(base::nullopt);
Alex Chaua76a6e32019-06-26 16:20:01164 return;
165 }
166
Alex Chau55dccbf2019-07-23 16:27:09167 LogSendWebPushMessagePayloadSize(message.payload.size());
Alex Chaua76a6e32019-06-26 16:20:01168 std::unique_ptr<network::SimpleURLLoader> url_loader = BuildURLLoader(
Richard Knoll1b846ce2019-07-24 09:37:12169 fcm_token, message.time_to_live, GetUrgencyHeader(message.urgency),
170 *auth_header, message.payload);
Alex Chaua76a6e32019-06-26 16:20:01171 url_loader->DownloadToString(
172 url_loader_factory_.get(),
173 base::BindOnce(&WebPushSender::OnMessageSent,
Alex Chau0046e012019-07-04 15:28:11174 weak_ptr_factory_.GetWeakPtr(), std::move(url_loader),
175 std::move(callback)),
Alex Chaua76a6e32019-06-26 16:20:01176 kMaximumBodySize);
177}
178
179void WebPushSender::OnMessageSent(
Alex Chaua76a6e32019-06-26 16:20:01180 std::unique_ptr<network::SimpleURLLoader> url_loader,
181 SendMessageCallback callback,
182 std::unique_ptr<std::string> response_body) {
183 int net_error = url_loader->NetError();
184 if (net_error != net::OK) {
185 LOG(ERROR) << "Network Error: " << net_error;
Alex Chaua4abf92f2019-07-19 10:32:56186 LogSendWebPushMessageResult(SendWebPushMessageResult::kNetworkError);
Alex Chau0046e012019-07-04 15:28:11187 std::move(callback).Run(base::nullopt);
Alex Chaua76a6e32019-06-26 16:20:01188 return;
189 }
190
Alex Chau0046e012019-07-04 15:28:11191 scoped_refptr<net::HttpResponseHeaders> response_headers =
192 url_loader->ResponseInfo()->headers;
193 if (!url_loader->ResponseInfo() || !response_headers) {
Alex Chaua76a6e32019-06-26 16:20:01194 LOG(ERROR) << "Response info not found";
Alex Chaua4abf92f2019-07-19 10:32:56195 LogSendWebPushMessageResult(SendWebPushMessageResult::kServerError);
Alex Chau0046e012019-07-04 15:28:11196 std::move(callback).Run(base::nullopt);
Alex Chaua76a6e32019-06-26 16:20:01197 return;
198 }
199
Alex Chau0046e012019-07-04 15:28:11200 int response_code = response_headers->response_code();
Alex Chaua76a6e32019-06-26 16:20:01201 if (!network::cors::IsOkStatus(response_code)) {
202 LOG(ERROR) << "HTTP Error: " << response_code;
Alex Chaua4abf92f2019-07-19 10:32:56203 LogSendWebPushMessageResult(SendWebPushMessageResult::kServerError);
Alex Chau0046e012019-07-04 15:28:11204 std::move(callback).Run(base::nullopt);
Alex Chaua76a6e32019-06-26 16:20:01205 return;
206 }
207
Alex Chau0046e012019-07-04 15:28:11208 std::string location;
209 if (!response_headers->EnumerateHeader(nullptr, "location", &location)) {
210 LOG(ERROR) << "Failed to get location header from response";
Alex Chaua4abf92f2019-07-19 10:32:56211 LogSendWebPushMessageResult(SendWebPushMessageResult::kParseResponseFailed);
Alex Chau0046e012019-07-04 15:28:11212 std::move(callback).Run(base::nullopt);
213 return;
214 }
215
216 size_t slash_pos = location.rfind("/");
217 if (slash_pos == std::string::npos) {
218 LOG(ERROR) << "Failed to parse message_id from location header";
Alex Chaua4abf92f2019-07-19 10:32:56219 LogSendWebPushMessageResult(SendWebPushMessageResult::kParseResponseFailed);
Alex Chau0046e012019-07-04 15:28:11220 std::move(callback).Run(base::nullopt);
221 return;
222 }
223
Alex Chaua4abf92f2019-07-19 10:32:56224 LogSendWebPushMessageResult(SendWebPushMessageResult::kSuccessful);
Alex Chau0046e012019-07-04 15:28:11225 std::move(callback).Run(base::make_optional(location.substr(slash_pos + 1)));
Alex Chaua76a6e32019-06-26 16:20:01226}
227
228} // namespace gcm