| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "google_apis/gcm/engine/registration_request.h" |
| |
| #include <stddef.h> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/location.h" |
| #include "base/metrics/histogram.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "base/values.h" |
| #include "google_apis/gcm/base/gcm_util.h" |
| #include "google_apis/gcm/monitoring/gcm_stats_recorder.h" |
| #include "net/base/load_flags.h" |
| #include "net/http/http_request_headers.h" |
| #include "net/http/http_status_code.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "services/network/public/cpp/simple_url_loader.h" |
| #include "url/gurl.h" |
| |
| namespace gcm { |
| |
| namespace { |
| |
| const char kRegistrationRequestContentType[] = |
| "application/x-www-form-urlencoded"; |
| |
| // Request constants. |
| const char kCategoryKey[] = "app"; |
| const char kSubtypeKey[] = "X-subtype"; |
| const char kDeviceIdKey[] = "device"; |
| const char kLoginHeader[] = "AidLogin"; |
| |
| // Response constants. |
| const char kErrorPrefix[] = "Error="; |
| const char kTokenPrefix[] = "token="; |
| const char kDeviceRegistrationError[] = "PHONE_REGISTRATION_ERROR"; |
| const char kAuthenticationFailed[] = "AUTHENTICATION_FAILED"; |
| const char kInvalidSender[] = "INVALID_SENDER"; |
| const char kInvalidParameters[] = "INVALID_PARAMETERS"; |
| const char kInternalServerError[] = "InternalServerError"; |
| const char kQuotaExceeded[] = "QUOTA_EXCEEDED"; |
| const char kTooManyRegistrations[] = "TOO_MANY_REGISTRATIONS"; |
| |
| // Gets correct status from the error message. |
| RegistrationRequest::Status GetStatusFromError(const std::string& error) { |
| if (error.find(kDeviceRegistrationError) != std::string::npos) |
| return RegistrationRequest::DEVICE_REGISTRATION_ERROR; |
| if (error.find(kAuthenticationFailed) != std::string::npos) |
| return RegistrationRequest::AUTHENTICATION_FAILED; |
| if (error.find(kInvalidSender) != std::string::npos) |
| return RegistrationRequest::INVALID_SENDER; |
| if (error.find(kInvalidParameters) != std::string::npos) |
| return RegistrationRequest::INVALID_PARAMETERS; |
| if (error.find(kInternalServerError) != std::string::npos) |
| return RegistrationRequest::INTERNAL_SERVER_ERROR; |
| if (error.find(kQuotaExceeded) != std::string::npos) |
| return RegistrationRequest::QUOTA_EXCEEDED; |
| if (error.find(kTooManyRegistrations) != std::string::npos) |
| return RegistrationRequest::TOO_MANY_REGISTRATIONS; |
| // Should not be reached, unless the server adds new error types. |
| return RegistrationRequest::UNKNOWN_ERROR; |
| } |
| |
| // Determines whether to retry based on the status of the last request. |
| bool ShouldRetryWithStatus(RegistrationRequest::Status status) { |
| switch (status) { |
| case RegistrationRequest::AUTHENTICATION_FAILED: |
| case RegistrationRequest::DEVICE_REGISTRATION_ERROR: |
| case RegistrationRequest::UNKNOWN_ERROR: |
| case RegistrationRequest::URL_FETCHING_FAILED: |
| case RegistrationRequest::HTTP_NOT_OK: |
| case RegistrationRequest::NO_RESPONSE_BODY: |
| case RegistrationRequest::RESPONSE_PARSING_FAILED: |
| case RegistrationRequest::INTERNAL_SERVER_ERROR: |
| return true; |
| case RegistrationRequest::SUCCESS: |
| case RegistrationRequest::INVALID_PARAMETERS: |
| case RegistrationRequest::INVALID_SENDER: |
| case RegistrationRequest::QUOTA_EXCEEDED: |
| case RegistrationRequest::TOO_MANY_REGISTRATIONS: |
| case RegistrationRequest::REACHED_MAX_RETRIES: |
| return false; |
| case RegistrationRequest::STATUS_COUNT: |
| NOTREACHED(); |
| break; |
| } |
| return false; |
| } |
| |
| } // namespace |
| |
| RegistrationRequest::RequestInfo::RequestInfo(uint64_t android_id, |
| uint64_t security_token, |
| const std::string& category, |
| const std::string& subtype) |
| : android_id(android_id), |
| security_token(security_token), |
| category(category), |
| subtype(subtype) { |
| DCHECK(android_id != 0UL); |
| DCHECK(security_token != 0UL); |
| DCHECK(!category.empty()); |
| } |
| |
| RegistrationRequest::RequestInfo::~RequestInfo() {} |
| |
| RegistrationRequest::CustomRequestHandler::CustomRequestHandler() {} |
| |
| RegistrationRequest::CustomRequestHandler::~CustomRequestHandler() {} |
| |
| RegistrationRequest::RegistrationRequest( |
| const GURL& registration_url, |
| const RequestInfo& request_info, |
| std::unique_ptr<CustomRequestHandler> custom_request_handler, |
| const net::BackoffEntry::Policy& backoff_policy, |
| const RegistrationCallback& callback, |
| int max_retry_count, |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory, |
| GCMStatsRecorder* recorder, |
| const std::string& source_to_record) |
| : callback_(callback), |
| request_info_(request_info), |
| custom_request_handler_(std::move(custom_request_handler)), |
| registration_url_(registration_url), |
| backoff_entry_(&backoff_policy), |
| url_loader_factory_(std::move(url_loader_factory)), |
| retries_left_(max_retry_count), |
| recorder_(recorder), |
| source_to_record_(source_to_record), |
| weak_ptr_factory_(this) { |
| DCHECK_GE(max_retry_count, 0); |
| } |
| |
| RegistrationRequest::~RegistrationRequest() {} |
| |
| void RegistrationRequest::Start() { |
| DCHECK(!callback_.is_null()); |
| DCHECK(!url_loader_.get()); |
| net::NetworkTrafficAnnotationTag traffic_annotation = |
| net::DefineNetworkTrafficAnnotation("gcm_registration", R"( |
| semantics { |
| sender: "GCM Driver" |
| description: |
| "Chromium interacts with Google Cloud Messaging to receive push " |
| "messages for various browser features, as well as on behalf of " |
| "websites and extensions. This requests Google Cloud Messaging to " |
| "create a new subscription through which messages can be sent to " |
| "the registering entity, through Chromium." |
| trigger: |
| "Immediately after a feature, website or extension creates a new " |
| "registration with the GCM Driver. Repeated registration requests " |
| "will be served from the cache instead." |
| data: |
| "The profile-bound Android ID and associated secret, and the " |
| "identifiers for the feature, website or extension that is " |
| "creating the registration." |
| destination: GOOGLE_OWNED_SERVICE |
| } |
| policy { |
| cookies_allowed: NO |
| setting: |
| "Support for interacting with Google Cloud Messaging is enabled by " |
| "default, and there is no configuration option to completely " |
| "disable it." |
| policy_exception_justification: |
| "Not implemented, considered not useful." |
| })"); |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = registration_url_; |
| request->method = "POST"; |
| request->load_flags = |
| net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SAVE_COOKIES; |
| BuildRequestHeaders(&request->headers); |
| |
| std::string body; |
| BuildRequestBody(&body); |
| |
| DVLOG(1) << "Performing registration for: " << request_info_.app_id(); |
| DVLOG(1) << "Registration request: " << body; |
| url_loader_ = |
| network::SimpleURLLoader::Create(std::move(request), traffic_annotation); |
| url_loader_->AttachStringForUpload(body, kRegistrationRequestContentType); |
| recorder_->RecordRegistrationSent(request_info_.app_id(), source_to_record_); |
| request_start_time_ = base::TimeTicks::Now(); |
| url_loader_->SetAllowHttpErrorResults(true); |
| url_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie( |
| url_loader_factory_.get(), |
| base::BindOnce(&RegistrationRequest::OnURLLoadComplete, |
| base::Unretained(this), url_loader_.get())); |
| } |
| |
| void RegistrationRequest::BuildRequestHeaders( |
| net::HttpRequestHeaders* headers) { |
| headers->SetHeader(net::HttpRequestHeaders::kAuthorization, |
| std::string(kLoginHeader) + " " + |
| base::NumberToString(request_info_.android_id) + ":" + |
| base::NumberToString(request_info_.security_token)); |
| } |
| |
| void RegistrationRequest::BuildRequestBody(std::string* body) { |
| BuildFormEncoding(kCategoryKey, request_info_.category, body); |
| if (!request_info_.subtype.empty()) |
| BuildFormEncoding(kSubtypeKey, request_info_.subtype, body); |
| |
| BuildFormEncoding(kDeviceIdKey, |
| base::NumberToString(request_info_.android_id), body); |
| |
| DCHECK(custom_request_handler_.get()); |
| custom_request_handler_->BuildRequestBody(body); |
| } |
| |
| void RegistrationRequest::RetryWithBackoff() { |
| DCHECK_GT(retries_left_, 0); |
| --retries_left_; |
| url_loader_.reset(); |
| backoff_entry_.InformOfRequest(false); |
| |
| DVLOG(1) << "Delaying GCM registration of app: " << request_info_.app_id() |
| << ", for " << backoff_entry_.GetTimeUntilRelease().InMilliseconds() |
| << " milliseconds."; |
| recorder_->RecordRegistrationRetryDelayed( |
| request_info_.app_id(), source_to_record_, |
| backoff_entry_.GetTimeUntilRelease().InMilliseconds(), retries_left_ + 1); |
| DCHECK(!weak_ptr_factory_.HasWeakPtrs()); |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&RegistrationRequest::Start, |
| weak_ptr_factory_.GetWeakPtr()), |
| backoff_entry_.GetTimeUntilRelease()); |
| } |
| |
| RegistrationRequest::Status RegistrationRequest::ParseResponse( |
| const network::SimpleURLLoader* source, |
| std::unique_ptr<std::string> body, |
| std::string* token) { |
| if (source->NetError() != net::OK) { |
| DVLOG(1) << "Registration URL fetching failed."; |
| return URL_FETCHING_FAILED; |
| } |
| |
| std::string response; |
| if (!body) { |
| DVLOG(1) << "Failed to get registration response body."; |
| return NO_RESPONSE_BODY; |
| } |
| response = std::move(*body); |
| |
| // If we are able to parse a meaningful known error, let's do so. Note that |
| // some errors will have HTTP_OK response code! |
| size_t error_pos = response.find(kErrorPrefix); |
| if (error_pos != std::string::npos) { |
| std::string error = |
| response.substr(error_pos + base::size(kErrorPrefix) - 1); |
| DVLOG(1) << "Registration response error message: " << error; |
| return GetStatusFromError(error); |
| } |
| |
| // Can't even get any header info. |
| if (!source->ResponseInfo() || !source->ResponseInfo()->headers) { |
| DVLOG(1) << "Registration HTTP response info or header missing"; |
| return HTTP_NOT_OK; |
| } |
| |
| // If we cannot tell what the error is, but at least we know response code was |
| // not OK. |
| if (source->ResponseInfo()->headers->response_code() != net::HTTP_OK) { |
| DVLOG(1) << "Registration HTTP response code not OK: " |
| << source->ResponseInfo()->headers->response_code(); |
| return HTTP_NOT_OK; |
| } |
| |
| size_t token_pos = response.find(kTokenPrefix); |
| if (token_pos != std::string::npos) { |
| *token = response.substr(token_pos + base::size(kTokenPrefix) - 1); |
| return SUCCESS; |
| } |
| |
| return RESPONSE_PARSING_FAILED; |
| } |
| |
| void RegistrationRequest::OnURLLoadComplete( |
| const network::SimpleURLLoader* source, |
| std::unique_ptr<std::string> body) { |
| std::string token; |
| Status status = ParseResponse(source, std::move(body), &token); |
| recorder_->RecordRegistrationResponse(request_info_.app_id(), |
| source_to_record_, status); |
| |
| DCHECK(custom_request_handler_.get()); |
| custom_request_handler_->ReportUMAs( |
| status, |
| backoff_entry_.failure_count(), |
| base::TimeTicks::Now() - request_start_time_); |
| |
| if (ShouldRetryWithStatus(status)) { |
| if (retries_left_ > 0) { |
| RetryWithBackoff(); |
| return; |
| } |
| |
| status = REACHED_MAX_RETRIES; |
| recorder_->RecordRegistrationResponse(request_info_.app_id(), |
| source_to_record_, status); |
| |
| // Only REACHED_MAX_RETRIES is reported because the function will skip |
| // reporting count and time when status is not SUCCESS. |
| DCHECK(custom_request_handler_.get()); |
| custom_request_handler_->ReportUMAs(status, 0, base::TimeDelta()); |
| } |
| |
| callback_.Run(status, token); |
| } |
| |
| } // namespace gcm |