per_user_topic_subscription_request.cc 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. // Copyright 2018 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. #include "components/invalidation/impl/per_user_topic_subscription_request.h"
  5. #include <memory>
  6. #include "base/bind.h"
  7. #include "base/json/json_writer.h"
  8. #include "base/memory/ptr_util.h"
  9. #include "base/metrics/histogram_functions.h"
  10. #include "base/strings/stringprintf.h"
  11. #include "base/values.h"
  12. #include "components/sync/base/model_type.h"
  13. #include "net/http/http_status_code.h"
  14. #include "services/network/public/cpp/resource_request.h"
  15. #include "services/network/public/mojom/fetch_api.mojom-shared.h"
  16. #include "services/network/public/mojom/url_response_head.mojom.h"
  17. using net::HttpRequestHeaders;
  18. namespace invalidation {
  19. namespace {
  20. const char kPublicTopicNameKey[] = "publicTopicName";
  21. const char kPrivateTopicNameKey[] = "privateTopicName";
  22. const std::string* GetTopicName(const base::Value& value) {
  23. if (!value.is_dict())
  24. return nullptr;
  25. if (value.FindBoolKey("isPublic").value_or(false))
  26. return value.FindStringKey(kPublicTopicNameKey);
  27. return value.FindStringKey(kPrivateTopicNameKey);
  28. }
  29. bool IsNetworkError(int net_error) {
  30. // Note: ERR_HTTP_RESPONSE_CODE_FAILURE isn't a real network error - it
  31. // indicates that the network request itself succeeded, but we received an
  32. // HTTP error. The alternative to special-casing this error would be to call
  33. // SetAllowHttpErrorResults(true) on the SimpleUrlLoader, but that also means
  34. // we'd download the response body in case of HTTP errors, which we don't
  35. // need.
  36. return net_error != net::OK &&
  37. net_error != net::ERR_HTTP_RESPONSE_CODE_FAILURE;
  38. }
  39. // Subscription status values for UMA_HISTOGRAM.
  40. enum class SubscriptionStatus {
  41. kSuccess = 0,
  42. kNetworkFailure = 1,
  43. kHttpFailure = 2,
  44. kParsingFailure = 3,
  45. kFailure = 4,
  46. kMaxValue = kFailure,
  47. };
  48. void RecordRequestStatus(SubscriptionStatus status,
  49. PerUserTopicSubscriptionRequest::RequestType type,
  50. const std::string& topic,
  51. int net_error = net::OK,
  52. int response_code = 200) {
  53. switch (type) {
  54. case PerUserTopicSubscriptionRequest::SUBSCRIBE: {
  55. base::UmaHistogramEnumeration(
  56. "FCMInvalidations.SubscriptionRequestStatus", status);
  57. break;
  58. }
  59. case PerUserTopicSubscriptionRequest::UNSUBSCRIBE: {
  60. base::UmaHistogramEnumeration(
  61. "FCMInvalidations.UnsubscriptionRequestStatus", status);
  62. break;
  63. }
  64. }
  65. if (type != PerUserTopicSubscriptionRequest::SUBSCRIBE) {
  66. return;
  67. }
  68. if (IsNetworkError(net_error)) {
  69. // Tracks the cases, when request fails due to network error.
  70. base::UmaHistogramSparse("FCMInvalidations.FailedSubscriptionsErrorCode",
  71. -net_error);
  72. DVLOG(1) << "Subscription request failed with error: " << net_error << ": "
  73. << net::ErrorToString(net_error);
  74. } else {
  75. // Log a histogram to track response success vs. failure rates.
  76. base::UmaHistogramSparse("FCMInvalidations.SubscriptionResponseCode",
  77. response_code);
  78. // If the topic corresponds to a Sync ModelType, use that as the histogram
  79. // suffix. Otherwise (e.g. Drive or Policy), just use "OTHER" for now.
  80. // TODO(crbug.com/1029698): Depending on sync is a layering violation.
  81. // Eventually the "whitelisted for metrics" bit should be part of a Topic.
  82. syncer::ModelType model_type; // Unused.
  83. std::string suffix =
  84. syncer::NotificationTypeToRealModelType(topic, &model_type) ? topic
  85. : "OTHER";
  86. base::UmaHistogramSparse(
  87. "FCMInvalidations.SubscriptionResponseCodeForTopic." + suffix,
  88. response_code);
  89. }
  90. }
  91. } // namespace
  92. PerUserTopicSubscriptionRequest::PerUserTopicSubscriptionRequest() = default;
  93. PerUserTopicSubscriptionRequest::~PerUserTopicSubscriptionRequest() = default;
  94. void PerUserTopicSubscriptionRequest::Start(
  95. CompletedCallback callback,
  96. network::mojom::URLLoaderFactory* loader_factory) {
  97. DCHECK(request_completed_callback_.is_null()) << "Request already running!";
  98. request_completed_callback_ = std::move(callback);
  99. simple_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
  100. loader_factory,
  101. base::BindOnce(&PerUserTopicSubscriptionRequest::OnURLFetchComplete,
  102. weak_ptr_factory_.GetWeakPtr()));
  103. }
  104. void PerUserTopicSubscriptionRequest::OnURLFetchComplete(
  105. std::unique_ptr<std::string> response_body) {
  106. int response_code = 0;
  107. if (simple_loader_->ResponseInfo() &&
  108. simple_loader_->ResponseInfo()->headers) {
  109. response_code = simple_loader_->ResponseInfo()->headers->response_code();
  110. }
  111. OnURLFetchCompleteInternal(simple_loader_->NetError(), response_code,
  112. std::move(response_body));
  113. // Potentially dead after the above invocation; nothing to do except return.
  114. }
  115. void PerUserTopicSubscriptionRequest::OnURLFetchCompleteInternal(
  116. int net_error,
  117. int response_code,
  118. std::unique_ptr<std::string> response_body) {
  119. if (IsNetworkError(net_error)) {
  120. RecordRequestStatus(SubscriptionStatus::kNetworkFailure, type_, topic_,
  121. net_error, response_code);
  122. RunCompletedCallbackAndMaybeDie(
  123. Status(StatusCode::FAILED, base::StringPrintf("Network Error")),
  124. std::string());
  125. // Potentially dead after the above invocation; nothing to do except return.
  126. return;
  127. }
  128. if (response_code != net::HTTP_OK) {
  129. StatusCode status = StatusCode::FAILED;
  130. if (response_code == net::HTTP_UNAUTHORIZED) {
  131. status = StatusCode::AUTH_FAILURE;
  132. } else if (response_code >= 400 && response_code <= 499) {
  133. status = StatusCode::FAILED_NON_RETRIABLE;
  134. }
  135. RecordRequestStatus(SubscriptionStatus::kHttpFailure, type_, topic_,
  136. net_error, response_code);
  137. RunCompletedCallbackAndMaybeDie(
  138. Status(status, base::StringPrintf("HTTP Error: %d", response_code)),
  139. std::string());
  140. // Potentially dead after the above invocation; nothing to do except return.
  141. return;
  142. }
  143. if (type_ == UNSUBSCRIBE) {
  144. // No response body expected for DELETE requests.
  145. RecordRequestStatus(SubscriptionStatus::kSuccess, type_, topic_, net_error,
  146. response_code);
  147. RunCompletedCallbackAndMaybeDie(Status(StatusCode::SUCCESS, std::string()),
  148. std::string());
  149. // Potentially dead after the above invocation; nothing to do except return.
  150. return;
  151. }
  152. if (!response_body || response_body->empty()) {
  153. RecordRequestStatus(SubscriptionStatus::kParsingFailure, type_, topic_,
  154. net_error, response_code);
  155. RunCompletedCallbackAndMaybeDie(
  156. Status(StatusCode::FAILED, base::StringPrintf("Body missing")),
  157. std::string());
  158. // Potentially dead after the above invocation; nothing to do except return.
  159. return;
  160. }
  161. data_decoder::DataDecoder::ParseJsonIsolated(
  162. *response_body,
  163. base::BindOnce(&PerUserTopicSubscriptionRequest::OnJsonParse,
  164. weak_ptr_factory_.GetWeakPtr()));
  165. }
  166. void PerUserTopicSubscriptionRequest::OnJsonParse(
  167. data_decoder::DataDecoder::ValueOrError result) {
  168. if (!result.has_value()) {
  169. RecordRequestStatus(SubscriptionStatus::kParsingFailure, type_, topic_);
  170. RunCompletedCallbackAndMaybeDie(
  171. Status(StatusCode::FAILED, base::StringPrintf("Body parse error")),
  172. std::string());
  173. // Potentially dead after the above invocation; nothing to do except return.
  174. return;
  175. }
  176. const std::string* topic_name = GetTopicName(*result);
  177. if (topic_name) {
  178. RecordRequestStatus(SubscriptionStatus::kSuccess, type_, topic_);
  179. RunCompletedCallbackAndMaybeDie(Status(StatusCode::SUCCESS, std::string()),
  180. *topic_name);
  181. // Potentially dead after the above invocation; nothing to do except return.
  182. } else {
  183. RecordRequestStatus(SubscriptionStatus::kParsingFailure, type_, topic_);
  184. RunCompletedCallbackAndMaybeDie(
  185. Status(StatusCode::FAILED, base::StringPrintf("Missing topic name")),
  186. std::string());
  187. // Potentially dead after the above invocation; nothing to do except return.
  188. }
  189. }
  190. void PerUserTopicSubscriptionRequest::RunCompletedCallbackAndMaybeDie(
  191. Status status,
  192. std::string topic_name) {
  193. std::move(request_completed_callback_)
  194. .Run(std::move(status), std::move(topic_name));
  195. }
  196. PerUserTopicSubscriptionRequest::Builder::Builder() = default;
  197. PerUserTopicSubscriptionRequest::Builder::~Builder() = default;
  198. std::unique_ptr<PerUserTopicSubscriptionRequest>
  199. PerUserTopicSubscriptionRequest::Builder::Build() const {
  200. DCHECK(!scope_.empty());
  201. auto request = base::WrapUnique(new PerUserTopicSubscriptionRequest());
  202. std::string url;
  203. switch (type_) {
  204. case SUBSCRIBE:
  205. url = base::StringPrintf(
  206. "%s/v1/perusertopics/%s/rel/topics/?subscriber_token=%s",
  207. scope_.c_str(), project_id_.c_str(), instance_id_token_.c_str());
  208. break;
  209. case UNSUBSCRIBE:
  210. std::string public_param;
  211. if (topic_is_public_) {
  212. public_param = "subscription.is_public=true&";
  213. }
  214. url = base::StringPrintf(
  215. "%s/v1/perusertopics/%s/rel/topics/%s?%ssubscriber_token=%s",
  216. scope_.c_str(), project_id_.c_str(), topic_.c_str(),
  217. public_param.c_str(), instance_id_token_.c_str());
  218. break;
  219. }
  220. GURL full_url(url);
  221. DCHECK(full_url.is_valid());
  222. request->url_ = full_url;
  223. request->type_ = type_;
  224. request->topic_ = topic_;
  225. std::string body;
  226. if (type_ == SUBSCRIBE)
  227. body = BuildBody();
  228. net::HttpRequestHeaders headers = BuildHeaders();
  229. request->simple_loader_ = BuildURLFetcher(headers, body, full_url);
  230. // Log the request for debugging network issues.
  231. DVLOG(1) << "Building a subscription request to " << full_url << ":\n"
  232. << headers.ToString() << "\n"
  233. << body;
  234. return request;
  235. }
  236. PerUserTopicSubscriptionRequest::Builder&
  237. PerUserTopicSubscriptionRequest::Builder::SetInstanceIdToken(
  238. const std::string& token) {
  239. instance_id_token_ = token;
  240. return *this;
  241. }
  242. PerUserTopicSubscriptionRequest::Builder&
  243. PerUserTopicSubscriptionRequest::Builder::SetScope(const std::string& scope) {
  244. scope_ = scope;
  245. return *this;
  246. }
  247. PerUserTopicSubscriptionRequest::Builder&
  248. PerUserTopicSubscriptionRequest::Builder::SetAuthenticationHeader(
  249. const std::string& auth_header) {
  250. auth_header_ = auth_header;
  251. return *this;
  252. }
  253. PerUserTopicSubscriptionRequest::Builder&
  254. PerUserTopicSubscriptionRequest::Builder::SetPublicTopicName(
  255. const Topic& topic) {
  256. topic_ = topic;
  257. return *this;
  258. }
  259. PerUserTopicSubscriptionRequest::Builder&
  260. PerUserTopicSubscriptionRequest::Builder::SetProjectId(
  261. const std::string& project_id) {
  262. project_id_ = project_id;
  263. return *this;
  264. }
  265. PerUserTopicSubscriptionRequest::Builder&
  266. PerUserTopicSubscriptionRequest::Builder::SetType(RequestType type) {
  267. type_ = type;
  268. return *this;
  269. }
  270. PerUserTopicSubscriptionRequest::Builder&
  271. PerUserTopicSubscriptionRequest::Builder::SetTopicIsPublic(
  272. bool topic_is_public) {
  273. topic_is_public_ = topic_is_public;
  274. return *this;
  275. }
  276. HttpRequestHeaders PerUserTopicSubscriptionRequest::Builder::BuildHeaders()
  277. const {
  278. HttpRequestHeaders headers;
  279. if (!auth_header_.empty()) {
  280. headers.SetHeader(HttpRequestHeaders::kAuthorization, auth_header_);
  281. }
  282. return headers;
  283. }
  284. std::string PerUserTopicSubscriptionRequest::Builder::BuildBody() const {
  285. base::Value request(base::Value::Type::DICTIONARY);
  286. request.SetStringKey("public_topic_name", topic_);
  287. if (topic_is_public_)
  288. request.SetBoolKey("is_public", true);
  289. std::string request_json;
  290. bool success = base::JSONWriter::Write(request, &request_json);
  291. DCHECK(success);
  292. return request_json;
  293. }
  294. std::unique_ptr<network::SimpleURLLoader>
  295. PerUserTopicSubscriptionRequest::Builder::BuildURLFetcher(
  296. const HttpRequestHeaders& headers,
  297. const std::string& body,
  298. const GURL& url) const {
  299. net::NetworkTrafficAnnotationTag traffic_annotation =
  300. net::DefineNetworkTrafficAnnotation("per_user_topic_registration_request",
  301. R"(
  302. semantics {
  303. sender:
  304. "Subscribe the Sync client for listening to the specific topic"
  305. description:
  306. "Chromium can receive Sync invalidations via FCM messages."
  307. "This request subscribes the client for receiving messages for the"
  308. "concrete topic. In case of Chrome Sync topic is a ModelType,"
  309. "e.g. BOOKMARK"
  310. trigger:
  311. "Subscription takes place only once per profile per topic. "
  312. data:
  313. "An OAuth2 token is sent as an authorization header."
  314. destination: GOOGLE_OWNED_SERVICE
  315. }
  316. policy {
  317. cookies_allowed: NO
  318. setting:
  319. "This feature can not be disabled by settings now"
  320. chrome_policy: {
  321. SyncDisabled {
  322. policy_options {mode: MANDATORY}
  323. SyncDisabled: false
  324. }
  325. }
  326. })");
  327. auto request = std::make_unique<network::ResourceRequest>();
  328. switch (type_) {
  329. case SUBSCRIBE:
  330. request->method = "POST";
  331. break;
  332. case UNSUBSCRIBE:
  333. request->method = "DELETE";
  334. break;
  335. }
  336. request->url = url;
  337. request->headers = headers;
  338. // Disable cookies for this request.
  339. request->credentials_mode = network::mojom::CredentialsMode::kOmit;
  340. std::unique_ptr<network::SimpleURLLoader> url_loader =
  341. network::SimpleURLLoader::Create(std::move(request), traffic_annotation);
  342. url_loader->AttachStringForUpload(body, "application/json; charset=UTF-8");
  343. return url_loader;
  344. }
  345. } // namespace invalidation