// 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/checkin_request.h" #include "base/bind.h" #include "base/message_loop/message_loop.h" #include "base/metrics/histogram.h" #include "google_apis/gcm/monitoring/gcm_stats_recorder.h" #include "google_apis/gcm/protocol/checkin.pb.h" #include "net/base/load_flags.h" #include "net/http/http_status_code.h" #include "net/url_request/url_fetcher.h" #include "net/url_request/url_request_status.h" namespace gcm { namespace { const char kRequestContentType[] = "application/x-protobuf"; const int kRequestVersionValue = 3; const int kDefaultUserSerialNumber = 0; // This enum is also used in an UMA histogram (GCMCheckinRequestStatus // enum defined in tools/metrics/histograms/histogram.xml). Hence the entries // here shouldn't be deleted or re-ordered and new ones should be added to // the end, and update the GetCheckinRequestStatusString(...) below. enum CheckinRequestStatus { SUCCESS, // Checkin completed successfully. URL_FETCHING_FAILED, // URL fetching failed. HTTP_BAD_REQUEST, // The request was malformed. HTTP_UNAUTHORIZED, // The security token didn't match the android id. HTTP_NOT_OK, // HTTP status was not OK. RESPONSE_PARSING_FAILED, // Check in response parsing failed. ZERO_ID_OR_TOKEN, // Either returned android id or security token // was zero. // NOTE: always keep this entry at the end. Add new status types only // immediately above this line. Make sure to update the corresponding // histogram enum accordingly. STATUS_COUNT }; // Returns string representation of enum CheckinRequestStatus. std::string GetCheckinRequestStatusString(CheckinRequestStatus status) { switch (status) { case SUCCESS: return "SUCCESS"; case URL_FETCHING_FAILED: return "URL_FETCHING_FAILED"; case HTTP_BAD_REQUEST: return "HTTP_BAD_REQUEST"; case HTTP_UNAUTHORIZED: return "HTTP_UNAUTHORIZED"; case HTTP_NOT_OK: return "HTTP_NOT_OK"; case RESPONSE_PARSING_FAILED: return "RESPONSE_PARSING_FAILED"; case ZERO_ID_OR_TOKEN: return "ZERO_ID_OR_TOKEN"; default: NOTREACHED(); return "UNKNOWN_STATUS"; } } // Records checkin status to both stats recorder and reports to UMA. void RecordCheckinStatusAndReportUMA(CheckinRequestStatus status, GCMStatsRecorder* recorder, bool will_retry) { UMA_HISTOGRAM_ENUMERATION("GCM.CheckinRequestStatus", status, STATUS_COUNT); if (status == SUCCESS) recorder->RecordCheckinSuccess(); else { recorder->RecordCheckinFailure(GetCheckinRequestStatusString(status), will_retry); } } } // namespace CheckinRequest::RequestInfo::RequestInfo( uint64 android_id, uint64 security_token, const std::map& account_tokens, const std::string& settings_digest, const checkin_proto::ChromeBuildProto& chrome_build_proto) : android_id(android_id), security_token(security_token), account_tokens(account_tokens), settings_digest(settings_digest), chrome_build_proto(chrome_build_proto) { } CheckinRequest::RequestInfo::~RequestInfo() {} CheckinRequest::CheckinRequest( const GURL& checkin_url, const RequestInfo& request_info, const net::BackoffEntry::Policy& backoff_policy, const CheckinRequestCallback& callback, net::URLRequestContextGetter* request_context_getter, GCMStatsRecorder* recorder) : request_context_getter_(request_context_getter), callback_(callback), backoff_entry_(&backoff_policy), checkin_url_(checkin_url), request_info_(request_info), recorder_(recorder), weak_ptr_factory_(this) { } CheckinRequest::~CheckinRequest() {} void CheckinRequest::Start() { DCHECK(!url_fetcher_.get()); checkin_proto::AndroidCheckinRequest request; request.set_id(request_info_.android_id); request.set_security_token(request_info_.security_token); request.set_user_serial_number(kDefaultUserSerialNumber); request.set_version(kRequestVersionValue); if (!request_info_.settings_digest.empty()) request.set_digest(request_info_.settings_digest); checkin_proto::AndroidCheckinProto* checkin = request.mutable_checkin(); checkin->mutable_chrome_build()->CopyFrom(request_info_.chrome_build_proto); #if defined(CHROME_OS) checkin->set_type(checkin_proto::DEVICE_CHROME_OS); #else checkin->set_type(checkin_proto::DEVICE_CHROME_BROWSER); #endif // Pack a map of email -> token mappings into a repeated field, where odd // entries are email addresses, while even ones are respective OAuth2 tokens. for (std::map::const_iterator iter = request_info_.account_tokens.begin(); iter != request_info_.account_tokens.end(); ++iter) { request.add_account_cookie(iter->first); request.add_account_cookie(iter->second); } std::string upload_data; CHECK(request.SerializeToString(&upload_data)); url_fetcher_.reset( net::URLFetcher::Create(checkin_url_, net::URLFetcher::POST, this)); url_fetcher_->SetRequestContext(request_context_getter_); url_fetcher_->SetUploadData(kRequestContentType, upload_data); url_fetcher_->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SAVE_COOKIES); recorder_->RecordCheckinInitiated(request_info_.android_id); request_start_time_ = base::TimeTicks::Now(); url_fetcher_->Start(); } void CheckinRequest::RetryWithBackoff(bool update_backoff) { if (update_backoff) { backoff_entry_.InformOfRequest(false); url_fetcher_.reset(); } if (backoff_entry_.ShouldRejectRequest()) { DVLOG(1) << "Delay GCM checkin for: " << backoff_entry_.GetTimeUntilRelease().InMilliseconds() << " milliseconds."; recorder_->RecordCheckinDelayedDueToBackoff( backoff_entry_.GetTimeUntilRelease().InMilliseconds()); base::MessageLoop::current()->PostDelayedTask( FROM_HERE, base::Bind(&CheckinRequest::RetryWithBackoff, weak_ptr_factory_.GetWeakPtr(), false), backoff_entry_.GetTimeUntilRelease()); return; } Start(); } void CheckinRequest::OnURLFetchComplete(const net::URLFetcher* source) { std::string response_string; checkin_proto::AndroidCheckinResponse response_proto; if (!source->GetStatus().is_success()) { LOG(ERROR) << "Failed to get checkin response. Fetcher failed. Retrying."; RecordCheckinStatusAndReportUMA(URL_FETCHING_FAILED, recorder_, true); RetryWithBackoff(true); return; } net::HttpStatusCode response_status = static_cast( source->GetResponseCode()); if (response_status == net::HTTP_BAD_REQUEST || response_status == net::HTTP_UNAUTHORIZED) { // BAD_REQUEST indicates that the request was malformed. // UNAUTHORIZED indicates that security token didn't match the android id. LOG(ERROR) << "No point retrying the checkin with status: " << response_status << ". Checkin failed."; CheckinRequestStatus status = response_status == net::HTTP_BAD_REQUEST ? HTTP_BAD_REQUEST : HTTP_UNAUTHORIZED; RecordCheckinStatusAndReportUMA(status, recorder_, false); callback_.Run(response_proto); return; } if (response_status != net::HTTP_OK || !source->GetResponseAsString(&response_string) || !response_proto.ParseFromString(response_string)) { LOG(ERROR) << "Failed to get checkin response. HTTP Status: " << response_status << ". Retrying."; CheckinRequestStatus status = response_status != net::HTTP_OK ? HTTP_NOT_OK : RESPONSE_PARSING_FAILED; RecordCheckinStatusAndReportUMA(status, recorder_, true); RetryWithBackoff(true); return; } if (!response_proto.has_android_id() || !response_proto.has_security_token() || response_proto.android_id() == 0 || response_proto.security_token() == 0) { LOG(ERROR) << "Android ID or security token is 0. Retrying."; RecordCheckinStatusAndReportUMA(ZERO_ID_OR_TOKEN, recorder_, true); RetryWithBackoff(true); return; } RecordCheckinStatusAndReportUMA(SUCCESS, recorder_, false); UMA_HISTOGRAM_COUNTS("GCM.CheckinRetryCount", backoff_entry_.failure_count()); UMA_HISTOGRAM_TIMES("GCM.CheckinCompleteTime", base::TimeTicks::Now() - request_start_time_); callback_.Run(response_proto); } } // namespace gcm