blob: 145ebd340e0744b99ec093fe4269f8f980a5e483 [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"
16#include "net/base/load_flags.h"
17#include "net/http/http_request_headers.h"
18#include "net/http/http_status_code.h"
19#include "services/network/public/cpp/cors/cors.h"
20#include "services/network/public/cpp/resource_request.h"
21#include "services/network/public/cpp/simple_url_loader.h"
22#include "url/gurl.h"
23
24namespace gcm {
25
26namespace {
27
28// VAPID header constants.
29const char kClaimsKeyAudience[] = "aud";
30const char kFCMServerAudience[] = "https://fcm.googleapis.com";
31
32const char kClaimsKeyExpirationTime[] = "exp";
33
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";
41
42const char kContentEncodingProperty[] = "content-encoding";
43const char kContentCodingAes128Gcm[] = "aes128gcm";
44
45// Other constants.
46const char kContentEncodingOctetStream[] = "application/octet-stream";
47const int kMaximumBodySize = 4096;
48
49base::Optional<std::string> GetAuthHeader(crypto::ECPrivateKey* vapid_key,
50 int time_to_live) {
51 base::Value claims(base::Value::Type::DICTIONARY);
52 claims.SetKey(kClaimsKeyAudience, base::Value(kFCMServerAudience));
53
54 int64_t exp =
55 (base::Time::Now() - base::Time::UnixEpoch()).InSeconds() + time_to_live;
56 // TODO: Year 2038 problem, base::Value does not support int64_t.
57 if (exp > INT_MAX)
58 return base::nullopt;
59
60 claims.SetKey(kClaimsKeyExpirationTime,
61 base::Value(static_cast<int32_t>(exp)));
62
63 base::Optional<std::string> jwt = CreateJSONWebToken(claims, vapid_key);
64 if (!jwt)
65 return base::nullopt;
66
67 std::string public_key;
68 if (!GetRawPublicKey(*vapid_key, &public_key))
69 return base::nullopt;
70
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
79std::unique_ptr<network::SimpleURLLoader> BuildURLLoader(
80 const std::string& fcm_token,
81 int time_to_live,
82 const std::string& auth_header,
83 const std::string& message) {
84 auto resource_request = std::make_unique<network::ResourceRequest>();
85 std::string server_url =
86 base::StringPrintf(kFCMServerUrlFormat, fcm_token.c_str());
87 resource_request->url = GURL(server_url);
88 resource_request->load_flags =
89 net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SAVE_COOKIES;
90 resource_request->method = "POST";
91 resource_request->headers.SetHeader(net::HttpRequestHeaders::kAuthorization,
92 auth_header);
93 resource_request->headers.SetHeader(kTTL, base::NumberToString(time_to_live));
94 resource_request->headers.SetHeader(kContentEncodingProperty,
95 kContentCodingAes128Gcm);
96
97 net::NetworkTrafficAnnotationTag traffic_annotation =
98 net::DefineNetworkTrafficAnnotation("web_push_message", R"(
99 semantics {
100 sender: "GCMDriver WebPushSender"
101 description:
102 "Send a request via Firebase to another device that is signed in"
103 "with the same account."
104 trigger: "Users send data to another owned device."
105 data: "Web push message."
106 destination: GOOGLE_OWNED_SERVICE
107 }
108 policy {
109 cookies_allowed: NO
110 setting:
111 "You can enable or disable this feature in Chrome's settings under"
112 "'Sync and Google services'."
113 policy_exception_justification: "Not implemented."
114 }
115 )");
116 std::unique_ptr<network::SimpleURLLoader> loader =
117 network::SimpleURLLoader::Create(std::move(resource_request),
118 traffic_annotation);
119 loader->AttachStringForUpload(message, kContentEncodingOctetStream);
120 loader->SetRetryOptions(1, network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE);
121 loader->SetAllowHttpErrorResults(true);
122
123 return loader;
124}
125
126} // namespace
127
128WebPushSender::WebPushSender(
129 scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
130 : url_loader_factory_(std::move(url_loader_factory)),
131 weak_ptr_factory_(this) {}
132
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 Chau0046e012019-07-04 15:28:11147 std::move(callback).Run(base::nullopt);
Alex Chaua76a6e32019-06-26 16:20:01148 return;
149 }
150
151 std::unique_ptr<network::SimpleURLLoader> url_loader = BuildURLLoader(
152 fcm_token, message.time_to_live, *auth_header, message.payload);
153 url_loader->DownloadToString(
154 url_loader_factory_.get(),
155 base::BindOnce(&WebPushSender::OnMessageSent,
Alex Chau0046e012019-07-04 15:28:11156 weak_ptr_factory_.GetWeakPtr(), std::move(url_loader),
157 std::move(callback)),
Alex Chaua76a6e32019-06-26 16:20:01158 kMaximumBodySize);
159}
160
161void WebPushSender::OnMessageSent(
Alex Chaua76a6e32019-06-26 16:20:01162 std::unique_ptr<network::SimpleURLLoader> url_loader,
163 SendMessageCallback callback,
164 std::unique_ptr<std::string> response_body) {
165 int net_error = url_loader->NetError();
166 if (net_error != net::OK) {
167 LOG(ERROR) << "Network Error: " << net_error;
Alex Chau0046e012019-07-04 15:28:11168 std::move(callback).Run(base::nullopt);
Alex Chaua76a6e32019-06-26 16:20:01169 return;
170 }
171
Alex Chau0046e012019-07-04 15:28:11172 scoped_refptr<net::HttpResponseHeaders> response_headers =
173 url_loader->ResponseInfo()->headers;
174 if (!url_loader->ResponseInfo() || !response_headers) {
Alex Chaua76a6e32019-06-26 16:20:01175 LOG(ERROR) << "Response info not found";
Alex Chau0046e012019-07-04 15:28:11176 std::move(callback).Run(base::nullopt);
Alex Chaua76a6e32019-06-26 16:20:01177 return;
178 }
179
Alex Chau0046e012019-07-04 15:28:11180 int response_code = response_headers->response_code();
Alex Chaua76a6e32019-06-26 16:20:01181 if (!network::cors::IsOkStatus(response_code)) {
182 LOG(ERROR) << "HTTP Error: " << response_code;
Alex Chau0046e012019-07-04 15:28:11183 std::move(callback).Run(base::nullopt);
Alex Chaua76a6e32019-06-26 16:20:01184 return;
185 }
186
Alex Chau0046e012019-07-04 15:28:11187 std::string location;
188 if (!response_headers->EnumerateHeader(nullptr, "location", &location)) {
189 LOG(ERROR) << "Failed to get location header from response";
190 std::move(callback).Run(base::nullopt);
191 return;
192 }
193
194 size_t slash_pos = location.rfind("/");
195 if (slash_pos == std::string::npos) {
196 LOG(ERROR) << "Failed to parse message_id from location header";
197 std::move(callback).Run(base::nullopt);
198 return;
199 }
200
201 std::move(callback).Run(base::make_optional(location.substr(slash_pos + 1)));
Alex Chaua76a6e32019-06-26 16:20:01202}
203
204} // namespace gcm