blob: 30b28608aabc5e57a4b8888f34c68097b6fed56c [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/base/load_flags.h"
19#include "net/http/http_request_headers.h"
20#include "net/http/http_status_code.h"
21#include "services/network/public/cpp/cors/cors.h"
22#include "services/network/public/cpp/resource_request.h"
23#include "services/network/public/cpp/simple_url_loader.h"
24#include "url/gurl.h"
25
26namespace gcm {
27
28namespace {
29
30// VAPID header constants.
31const char kClaimsKeyAudience[] = "aud";
32const char kFCMServerAudience[] = "https://fcm.googleapis.com";
33
34const char kClaimsKeyExpirationTime[] = "exp";
35
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";
50const int kMaximumBodySize = 4096;
51
52base::Optional<std::string> GetAuthHeader(crypto::ECPrivateKey* vapid_key,
53 int time_to_live) {
54 base::Value claims(base::Value::Type::DICTIONARY);
55 claims.SetKey(kClaimsKeyAudience, base::Value(kFCMServerAudience));
56
57 int64_t exp =
58 (base::Time::Now() - base::Time::UnixEpoch()).InSeconds() + time_to_live;
59 // TODO: Year 2038 problem, base::Value does not support int64_t.
60 if (exp > INT_MAX)
61 return base::nullopt;
62
63 claims.SetKey(kClaimsKeyExpirationTime,
64 base::Value(static_cast<int32_t>(exp)));
65
66 base::Optional<std::string> jwt = CreateJSONWebToken(claims, vapid_key);
67 if (!jwt)
68 return base::nullopt;
69
70 std::string public_key;
71 if (!GetRawPublicKey(*vapid_key, &public_key))
72 return base::nullopt;
73
74 std::string base64_public_key;
75 base::Base64UrlEncode(public_key, base::Base64UrlEncodePolicy::OMIT_PADDING,
76 &base64_public_key);
77
78 return base::StringPrintf(kAuthorizationRequestHeaderFormat, jwt->c_str(),
79 base64_public_key.c_str());
80}
81
Richard Knoll1b846ce2019-07-24 09:37:1282std::string GetUrgencyHeader(WebPushMessage::Urgency urgency) {
83 switch (urgency) {
84 case WebPushMessage::Urgency::kVeryLow:
85 return "very-low";
86 case WebPushMessage::Urgency::kLow:
87 return "low";
88 case WebPushMessage::Urgency::kNormal:
89 return "normal";
90 case WebPushMessage::Urgency::kHigh:
91 return "high";
92 }
93}
94
Alex Chaua76a6e32019-06-26 16:20:0195std::unique_ptr<network::SimpleURLLoader> BuildURLLoader(
96 const std::string& fcm_token,
97 int time_to_live,
Richard Knoll1b846ce2019-07-24 09:37:1298 const std::string& urgency_header,
Alex Chaua76a6e32019-06-26 16:20:0199 const std::string& auth_header,
100 const std::string& message) {
101 auto resource_request = std::make_unique<network::ResourceRequest>();
102 std::string server_url =
103 base::StringPrintf(kFCMServerUrlFormat, fcm_token.c_str());
104 resource_request->url = GURL(server_url);
105 resource_request->load_flags =
106 net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SAVE_COOKIES;
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 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
125 }
126 policy {
127 cookies_allowed: NO
128 setting:
129 "You can enable or disable this feature in Chrome's settings under"
130 "'Sync and Google services'."
131 policy_exception_justification: "Not implemented."
132 }
133 )");
134 std::unique_ptr<network::SimpleURLLoader> loader =
135 network::SimpleURLLoader::Create(std::move(resource_request),
136 traffic_annotation);
137 loader->AttachStringForUpload(message, kContentEncodingOctetStream);
138 loader->SetRetryOptions(1, network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE);
139 loader->SetAllowHttpErrorResults(true);
140
141 return loader;
142}
143
144} // namespace
145
146WebPushSender::WebPushSender(
147 scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
Jeremy Roman5c341f6d2019-07-15 15:56:10148 : url_loader_factory_(std::move(url_loader_factory)) {}
Alex Chaua76a6e32019-06-26 16:20:01149
150WebPushSender::~WebPushSender() = default;
151
152void WebPushSender::SendMessage(const std::string& fcm_token,
153 crypto::ECPrivateKey* vapid_key,
154 const WebPushMessage& message,
155 SendMessageCallback callback) {
156 DCHECK(!fcm_token.empty());
157 DCHECK(vapid_key);
158 DCHECK_LE(message.time_to_live, message.kMaximumTTL);
159
160 base::Optional<std::string> auth_header =
161 GetAuthHeader(vapid_key, message.time_to_live);
162 if (!auth_header) {
163 LOG(ERROR) << "Failed to create JWT";
Alex Chaua4abf92f2019-07-19 10:32:56164 LogSendWebPushMessageResult(SendWebPushMessageResult::kCreateJWTFailed);
Alex Chau0046e012019-07-04 15:28:11165 std::move(callback).Run(base::nullopt);
Alex Chaua76a6e32019-06-26 16:20:01166 return;
167 }
168
Alex Chau55dccbf2019-07-23 16:27:09169 LogSendWebPushMessagePayloadSize(message.payload.size());
Alex Chaua76a6e32019-06-26 16:20:01170 std::unique_ptr<network::SimpleURLLoader> url_loader = BuildURLLoader(
Richard Knoll1b846ce2019-07-24 09:37:12171 fcm_token, message.time_to_live, GetUrgencyHeader(message.urgency),
172 *auth_header, message.payload);
Alex Chaua76a6e32019-06-26 16:20:01173 url_loader->DownloadToString(
174 url_loader_factory_.get(),
175 base::BindOnce(&WebPushSender::OnMessageSent,
Alex Chau0046e012019-07-04 15:28:11176 weak_ptr_factory_.GetWeakPtr(), std::move(url_loader),
177 std::move(callback)),
Alex Chaua76a6e32019-06-26 16:20:01178 kMaximumBodySize);
179}
180
181void WebPushSender::OnMessageSent(
Alex Chaua76a6e32019-06-26 16:20:01182 std::unique_ptr<network::SimpleURLLoader> url_loader,
183 SendMessageCallback callback,
184 std::unique_ptr<std::string> response_body) {
185 int net_error = url_loader->NetError();
186 if (net_error != net::OK) {
187 LOG(ERROR) << "Network Error: " << net_error;
Alex Chaua4abf92f2019-07-19 10:32:56188 LogSendWebPushMessageResult(SendWebPushMessageResult::kNetworkError);
Alex Chau0046e012019-07-04 15:28:11189 std::move(callback).Run(base::nullopt);
Alex Chaua76a6e32019-06-26 16:20:01190 return;
191 }
192
Alex Chau0046e012019-07-04 15:28:11193 scoped_refptr<net::HttpResponseHeaders> response_headers =
194 url_loader->ResponseInfo()->headers;
195 if (!url_loader->ResponseInfo() || !response_headers) {
Alex Chaua76a6e32019-06-26 16:20:01196 LOG(ERROR) << "Response info not found";
Alex Chaua4abf92f2019-07-19 10:32:56197 LogSendWebPushMessageResult(SendWebPushMessageResult::kServerError);
Alex Chau0046e012019-07-04 15:28:11198 std::move(callback).Run(base::nullopt);
Alex Chaua76a6e32019-06-26 16:20:01199 return;
200 }
201
Alex Chau0046e012019-07-04 15:28:11202 int response_code = response_headers->response_code();
Alex Chaua76a6e32019-06-26 16:20:01203 if (!network::cors::IsOkStatus(response_code)) {
204 LOG(ERROR) << "HTTP Error: " << response_code;
Alex Chaua4abf92f2019-07-19 10:32:56205 LogSendWebPushMessageResult(SendWebPushMessageResult::kServerError);
Alex Chau0046e012019-07-04 15:28:11206 std::move(callback).Run(base::nullopt);
Alex Chaua76a6e32019-06-26 16:20:01207 return;
208 }
209
Alex Chau0046e012019-07-04 15:28:11210 std::string location;
211 if (!response_headers->EnumerateHeader(nullptr, "location", &location)) {
212 LOG(ERROR) << "Failed to get location header from response";
Alex Chaua4abf92f2019-07-19 10:32:56213 LogSendWebPushMessageResult(SendWebPushMessageResult::kParseResponseFailed);
Alex Chau0046e012019-07-04 15:28:11214 std::move(callback).Run(base::nullopt);
215 return;
216 }
217
218 size_t slash_pos = location.rfind("/");
219 if (slash_pos == std::string::npos) {
220 LOG(ERROR) << "Failed to parse message_id from location header";
Alex Chaua4abf92f2019-07-19 10:32:56221 LogSendWebPushMessageResult(SendWebPushMessageResult::kParseResponseFailed);
Alex Chau0046e012019-07-04 15:28:11222 std::move(callback).Run(base::nullopt);
223 return;
224 }
225
Alex Chaua4abf92f2019-07-19 10:32:56226 LogSendWebPushMessageResult(SendWebPushMessageResult::kSuccessful);
Alex Chau0046e012019-07-04 15:28:11227 std::move(callback).Run(base::make_optional(location.substr(slash_pos + 1)));
Alex Chaua76a6e32019-06-26 16:20:01228}
229
230} // namespace gcm