blob: 506ec5dfdc745e4e96356c09b0f9e91594f21bdb [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"
11#include "base/strings/string_number_conversions.h"
12#include "base/strings/stringprintf.h"
13#include "components/gcm_driver/common/gcm_message.h"
14#include "components/gcm_driver/crypto/json_web_token_util.h"
15#include "components/gcm_driver/crypto/p256_key_util.h"
Alex Chaua4abf92f2019-07-19 10:32:5616#include "components/gcm_driver/web_push_metrics.h"
Alex Chaua76a6e32019-06-26 16:20:0117#include "net/base/load_flags.h"
18#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";
42
43const char kContentEncodingProperty[] = "content-encoding";
44const char kContentCodingAes128Gcm[] = "aes128gcm";
45
46// Other constants.
47const char kContentEncodingOctetStream[] = "application/octet-stream";
48const int kMaximumBodySize = 4096;
49
50base::Optional<std::string> GetAuthHeader(crypto::ECPrivateKey* vapid_key,
51 int time_to_live) {
52 base::Value claims(base::Value::Type::DICTIONARY);
53 claims.SetKey(kClaimsKeyAudience, base::Value(kFCMServerAudience));
54
55 int64_t exp =
56 (base::Time::Now() - base::Time::UnixEpoch()).InSeconds() + time_to_live;
57 // TODO: Year 2038 problem, base::Value does not support int64_t.
58 if (exp > INT_MAX)
59 return base::nullopt;
60
61 claims.SetKey(kClaimsKeyExpirationTime,
62 base::Value(static_cast<int32_t>(exp)));
63
64 base::Optional<std::string> jwt = CreateJSONWebToken(claims, vapid_key);
65 if (!jwt)
66 return base::nullopt;
67
68 std::string public_key;
69 if (!GetRawPublicKey(*vapid_key, &public_key))
70 return base::nullopt;
71
72 std::string base64_public_key;
73 base::Base64UrlEncode(public_key, base::Base64UrlEncodePolicy::OMIT_PADDING,
74 &base64_public_key);
75
76 return base::StringPrintf(kAuthorizationRequestHeaderFormat, jwt->c_str(),
77 base64_public_key.c_str());
78}
79
80std::unique_ptr<network::SimpleURLLoader> BuildURLLoader(
81 const std::string& fcm_token,
82 int time_to_live,
83 const std::string& auth_header,
84 const std::string& message) {
85 auto resource_request = std::make_unique<network::ResourceRequest>();
86 std::string server_url =
87 base::StringPrintf(kFCMServerUrlFormat, fcm_token.c_str());
88 resource_request->url = GURL(server_url);
89 resource_request->load_flags =
90 net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SAVE_COOKIES;
91 resource_request->method = "POST";
92 resource_request->headers.SetHeader(net::HttpRequestHeaders::kAuthorization,
93 auth_header);
94 resource_request->headers.SetHeader(kTTL, base::NumberToString(time_to_live));
95 resource_request->headers.SetHeader(kContentEncodingProperty,
96 kContentCodingAes128Gcm);
97
98 net::NetworkTrafficAnnotationTag traffic_annotation =
99 net::DefineNetworkTrafficAnnotation("web_push_message", R"(
100 semantics {
101 sender: "GCMDriver WebPushSender"
102 description:
103 "Send a request via Firebase to another device that is signed in"
104 "with the same account."
105 trigger: "Users send data to another owned device."
106 data: "Web push message."
107 destination: GOOGLE_OWNED_SERVICE
108 }
109 policy {
110 cookies_allowed: NO
111 setting:
112 "You can enable or disable this feature in Chrome's settings under"
113 "'Sync and Google services'."
114 policy_exception_justification: "Not implemented."
115 }
116 )");
117 std::unique_ptr<network::SimpleURLLoader> loader =
118 network::SimpleURLLoader::Create(std::move(resource_request),
119 traffic_annotation);
120 loader->AttachStringForUpload(message, kContentEncodingOctetStream);
121 loader->SetRetryOptions(1, network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE);
122 loader->SetAllowHttpErrorResults(true);
123
124 return loader;
125}
126
127} // namespace
128
129WebPushSender::WebPushSender(
130 scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
Jeremy Roman5c341f6d2019-07-15 15:56:10131 : url_loader_factory_(std::move(url_loader_factory)) {}
Alex Chaua76a6e32019-06-26 16:20:01132
133WebPushSender::~WebPushSender() = default;
134
135void WebPushSender::SendMessage(const std::string& fcm_token,
136 crypto::ECPrivateKey* vapid_key,
137 const WebPushMessage& message,
138 SendMessageCallback callback) {
139 DCHECK(!fcm_token.empty());
140 DCHECK(vapid_key);
141 DCHECK_LE(message.time_to_live, message.kMaximumTTL);
142
143 base::Optional<std::string> auth_header =
144 GetAuthHeader(vapid_key, message.time_to_live);
145 if (!auth_header) {
146 LOG(ERROR) << "Failed to create JWT";
Alex Chaua4abf92f2019-07-19 10:32:56147 LogSendWebPushMessageResult(SendWebPushMessageResult::kCreateJWTFailed);
Alex Chau0046e012019-07-04 15:28:11148 std::move(callback).Run(base::nullopt);
Alex Chaua76a6e32019-06-26 16:20:01149 return;
150 }
151
152 std::unique_ptr<network::SimpleURLLoader> url_loader = BuildURLLoader(
153 fcm_token, message.time_to_live, *auth_header, message.payload);
154 url_loader->DownloadToString(
155 url_loader_factory_.get(),
156 base::BindOnce(&WebPushSender::OnMessageSent,
Alex Chau0046e012019-07-04 15:28:11157 weak_ptr_factory_.GetWeakPtr(), std::move(url_loader),
158 std::move(callback)),
Alex Chaua76a6e32019-06-26 16:20:01159 kMaximumBodySize);
160}
161
162void WebPushSender::OnMessageSent(
Alex Chaua76a6e32019-06-26 16:20:01163 std::unique_ptr<network::SimpleURLLoader> url_loader,
164 SendMessageCallback callback,
165 std::unique_ptr<std::string> response_body) {
166 int net_error = url_loader->NetError();
167 if (net_error != net::OK) {
168 LOG(ERROR) << "Network Error: " << net_error;
Alex Chaua4abf92f2019-07-19 10:32:56169 LogSendWebPushMessageResult(SendWebPushMessageResult::kNetworkError);
Alex Chau0046e012019-07-04 15:28:11170 std::move(callback).Run(base::nullopt);
Alex Chaua76a6e32019-06-26 16:20:01171 return;
172 }
173
Alex Chau0046e012019-07-04 15:28:11174 scoped_refptr<net::HttpResponseHeaders> response_headers =
175 url_loader->ResponseInfo()->headers;
176 if (!url_loader->ResponseInfo() || !response_headers) {
Alex Chaua76a6e32019-06-26 16:20:01177 LOG(ERROR) << "Response info not found";
Alex Chaua4abf92f2019-07-19 10:32:56178 LogSendWebPushMessageResult(SendWebPushMessageResult::kServerError);
Alex Chau0046e012019-07-04 15:28:11179 std::move(callback).Run(base::nullopt);
Alex Chaua76a6e32019-06-26 16:20:01180 return;
181 }
182
Alex Chau0046e012019-07-04 15:28:11183 int response_code = response_headers->response_code();
Alex Chaua76a6e32019-06-26 16:20:01184 if (!network::cors::IsOkStatus(response_code)) {
185 LOG(ERROR) << "HTTP Error: " << response_code;
Alex Chaua4abf92f2019-07-19 10:32:56186 LogSendWebPushMessageResult(SendWebPushMessageResult::kServerError);
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 std::string location;
192 if (!response_headers->EnumerateHeader(nullptr, "location", &location)) {
193 LOG(ERROR) << "Failed to get location header from response";
Alex Chaua4abf92f2019-07-19 10:32:56194 LogSendWebPushMessageResult(SendWebPushMessageResult::kParseResponseFailed);
Alex Chau0046e012019-07-04 15:28:11195 std::move(callback).Run(base::nullopt);
196 return;
197 }
198
199 size_t slash_pos = location.rfind("/");
200 if (slash_pos == std::string::npos) {
201 LOG(ERROR) << "Failed to parse message_id from location header";
Alex Chaua4abf92f2019-07-19 10:32:56202 LogSendWebPushMessageResult(SendWebPushMessageResult::kParseResponseFailed);
Alex Chau0046e012019-07-04 15:28:11203 std::move(callback).Run(base::nullopt);
204 return;
205 }
206
Alex Chaua4abf92f2019-07-19 10:32:56207 LogSendWebPushMessageResult(SendWebPushMessageResult::kSuccessful);
Alex Chau0046e012019-07-04 15:28:11208 std::move(callback).Run(base::make_optional(location.substr(slash_pos + 1)));
Alex Chaua76a6e32019-06-26 16:20:01209}
210
211} // namespace gcm