Home | History | Annotate | Download | only in timezone
      1 // Copyright 2014 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 "chrome/browser/chromeos/timezone/timezone_request.h"
      6 
      7 #include <string>
      8 
      9 #include "base/json/json_reader.h"
     10 #include "base/metrics/histogram.h"
     11 #include "base/metrics/sparse_histogram.h"
     12 #include "base/strings/string_number_conversions.h"
     13 #include "base/strings/stringprintf.h"
     14 #include "base/time/time.h"
     15 #include "base/values.h"
     16 #include "chrome/browser/chromeos/geolocation/geoposition.h"
     17 #include "google_apis/google_api_keys.h"
     18 #include "net/base/escape.h"
     19 #include "net/base/load_flags.h"
     20 #include "net/http/http_status_code.h"
     21 #include "net/url_request/url_fetcher.h"
     22 #include "net/url_request/url_request_context_getter.h"
     23 #include "net/url_request/url_request_status.h"
     24 
     25 namespace chromeos {
     26 
     27 namespace {
     28 
     29 const char kDefaultTimezoneProviderUrl[] =
     30     "https://maps.googleapis.com/maps/api/timezone/json?";
     31 
     32 const char kKeyString[] = "key";
     33 // Language parameter is unsupported for now.
     34 // const char kLanguageString[] = "language";
     35 const char kLocationString[] = "location";
     36 const char kSensorString[] = "sensor";
     37 const char kTimestampString[] = "timestamp";
     38 
     39 const char kDstOffsetString[] = "dstOffset";
     40 const char kRawOffsetString[] = "rawOffset";
     41 const char kTimeZoneIdString[] = "timeZoneId";
     42 const char kTimeZoneNameString[] = "timeZoneName";
     43 const char kStatusString[] = "status";
     44 const char kErrorMessageString[] = "error_message";
     45 
     46 // Sleep between timezone request retry on HTTP error.
     47 const unsigned int kResolveTimeZoneRetrySleepOnServerErrorSeconds = 5;
     48 
     49 // Sleep between timezone request retry on bad server response.
     50 const unsigned int kResolveTimeZoneRetrySleepBadResponseSeconds = 10;
     51 
     52 struct StatusString2Enum {
     53   const char* string;
     54   TimeZoneResponseData::Status value;
     55 };
     56 
     57 const StatusString2Enum statusString2Enum[] = {
     58     {"OK", TimeZoneResponseData::OK},
     59     {"INVALID_REQUEST", TimeZoneResponseData::INVALID_REQUEST},
     60     {"OVER_QUERY_LIMIT", TimeZoneResponseData::OVER_QUERY_LIMIT},
     61     {"REQUEST_DENIED", TimeZoneResponseData::REQUEST_DENIED},
     62     {"UNKNOWN_ERROR", TimeZoneResponseData::UNKNOWN_ERROR},
     63     {"ZERO_RESULTS", TimeZoneResponseData::ZERO_RESULTS}, };
     64 
     65 enum TimeZoneRequestEvent {
     66   // NOTE: Do not renumber these as that would confuse interpretation of
     67   // previously logged data. When making changes, also update the enum list
     68   // in tools/metrics/histograms/histograms.xml to keep it in sync.
     69   TIMEZONE_REQUEST_EVENT_REQUEST_START = 0,
     70   TIMEZONE_REQUEST_EVENT_RESPONSE_SUCCESS = 1,
     71   TIMEZONE_REQUEST_EVENT_RESPONSE_NOT_OK = 2,
     72   TIMEZONE_REQUEST_EVENT_RESPONSE_EMPTY = 3,
     73   TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED = 4,
     74 
     75   // NOTE: Add entries only immediately above this line.
     76   TIMEZONE_REQUEST_EVENT_COUNT = 5
     77 };
     78 
     79 enum TimeZoneRequestResult {
     80   // NOTE: Do not renumber these as that would confuse interpretation of
     81   // previously logged data. When making changes, also update the enum list
     82   // in tools/metrics/histograms/histograms.xml to keep it in sync.
     83   TIMEZONE_REQUEST_RESULT_SUCCESS = 0,
     84   TIMEZONE_REQUEST_RESULT_FAILURE = 1,
     85   TIMEZONE_REQUEST_RESULT_SERVER_ERROR = 2,
     86   TIMEZONE_REQUEST_RESULT_CANCELLED = 3,
     87 
     88   // NOTE: Add entries only immediately above this line.
     89   TIMEZONE_REQUEST_RESULT_COUNT = 4
     90 };
     91 
     92 // Too many requests (more than 1) mean there is a problem in implementation.
     93 void RecordUmaEvent(TimeZoneRequestEvent event) {
     94   UMA_HISTOGRAM_ENUMERATION(
     95       "TimeZone.TimeZoneRequest.Event", event, TIMEZONE_REQUEST_EVENT_COUNT);
     96 }
     97 
     98 void RecordUmaResponseCode(int code) {
     99   UMA_HISTOGRAM_SPARSE_SLOWLY("TimeZone.TimeZoneRequest.ResponseCode", code);
    100 }
    101 
    102 // Slow timezone resolve leads to bad user experience.
    103 void RecordUmaResponseTime(base::TimeDelta elapsed, bool success) {
    104   if (success) {
    105     UMA_HISTOGRAM_TIMES("TimeZone.TimeZoneRequest.ResponseSuccessTime",
    106                         elapsed);
    107   } else {
    108     UMA_HISTOGRAM_TIMES("TimeZone.TimeZoneRequest.ResponseFailureTime",
    109                         elapsed);
    110   }
    111 }
    112 
    113 void RecordUmaResult(TimeZoneRequestResult result, unsigned retries) {
    114   UMA_HISTOGRAM_ENUMERATION(
    115       "TimeZone.TimeZoneRequest.Result", result, TIMEZONE_REQUEST_RESULT_COUNT);
    116   UMA_HISTOGRAM_SPARSE_SLOWLY("TimeZone.TimeZoneRequest.Retries", retries);
    117 }
    118 
    119 // Creates the request url to send to the server.
    120 GURL TimeZoneRequestURL(const GURL& url,
    121                         const Geoposition& geoposition,
    122                         bool sensor) {
    123   std::string query(url.query());
    124   query += base::StringPrintf(
    125       "%s=%f,%f", kLocationString, geoposition.latitude, geoposition.longitude);
    126   if (url == DefaultTimezoneProviderURL()) {
    127     std::string api_key = google_apis::GetAPIKey();
    128     if (!api_key.empty()) {
    129       query += "&";
    130       query += kKeyString;
    131       query += "=";
    132       query += net::EscapeQueryParamValue(api_key, true);
    133     }
    134   }
    135   if (!geoposition.timestamp.is_null()) {
    136     query += base::StringPrintf(
    137         "&%s=%ld", kTimestampString, geoposition.timestamp.ToTimeT());
    138   }
    139   query += "&";
    140   query += kSensorString;
    141   query += "=";
    142   query += (sensor ? "true" : "false");
    143 
    144   GURL::Replacements replacements;
    145   replacements.SetQueryStr(query);
    146   return url.ReplaceComponents(replacements);
    147 }
    148 
    149 void PrintTimeZoneError(const GURL& server_url,
    150                         const std::string& message,
    151                         TimeZoneResponseData* timezone) {
    152   timezone->status = TimeZoneResponseData::REQUEST_ERROR;
    153   timezone->error_message =
    154       base::StringPrintf("TimeZone provider at '%s' : %s.",
    155                          server_url.GetOrigin().spec().c_str(),
    156                          message.c_str());
    157   LOG(WARNING) << "TimeZoneRequest::GetTimeZoneFromResponse() : "
    158                << timezone->error_message;
    159 }
    160 
    161 // Parses the server response body. Returns true if parsing was successful.
    162 // Sets |*timezone| to the parsed TimeZone if a valid timezone was received,
    163 // otherwise leaves it unchanged.
    164 bool ParseServerResponse(const GURL& server_url,
    165                          const std::string& response_body,
    166                          TimeZoneResponseData* timezone) {
    167   DCHECK(timezone);
    168 
    169   if (response_body.empty()) {
    170     PrintTimeZoneError(server_url, "Server returned empty response", timezone);
    171     RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_EMPTY);
    172     return false;
    173   }
    174   VLOG(1) << "TimeZoneRequest::ParseServerResponse() : Parsing response "
    175           << response_body;
    176 
    177   // Parse the response, ignoring comments.
    178   std::string error_msg;
    179   scoped_ptr<base::Value> response_value(base::JSONReader::ReadAndReturnError(
    180       response_body, base::JSON_PARSE_RFC, NULL, &error_msg));
    181   if (response_value == NULL) {
    182     PrintTimeZoneError(server_url, "JSONReader failed: " + error_msg, timezone);
    183     RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
    184     return false;
    185   }
    186 
    187   const base::DictionaryValue* response_object = NULL;
    188   if (!response_value->GetAsDictionary(&response_object)) {
    189     PrintTimeZoneError(server_url,
    190                        "Unexpected response type : " +
    191                            base::StringPrintf("%u", response_value->GetType()),
    192                        timezone);
    193     RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
    194     return false;
    195   }
    196 
    197   std::string status;
    198 
    199   if (!response_object->GetStringWithoutPathExpansion(kStatusString, &status)) {
    200     PrintTimeZoneError(server_url, "Missing status attribute.", timezone);
    201     RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
    202     return false;
    203   }
    204 
    205   bool found = false;
    206   for (size_t i = 0; i < arraysize(statusString2Enum); ++i) {
    207     if (status != statusString2Enum[i].string)
    208       continue;
    209 
    210     timezone->status = statusString2Enum[i].value;
    211     found = true;
    212     break;
    213   }
    214 
    215   if (!found) {
    216     PrintTimeZoneError(
    217         server_url, "Bad status attribute value: '" + status + "'", timezone);
    218     RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
    219     return false;
    220   }
    221 
    222   const bool status_ok = (timezone->status == TimeZoneResponseData::OK);
    223 
    224   if (!response_object->GetDoubleWithoutPathExpansion(kDstOffsetString,
    225                                                       &timezone->dstOffset) &&
    226       status_ok) {
    227     PrintTimeZoneError(server_url, "Missing dstOffset attribute.", timezone);
    228     RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
    229     return false;
    230   }
    231 
    232   if (!response_object->GetDoubleWithoutPathExpansion(kRawOffsetString,
    233                                                       &timezone->rawOffset) &&
    234       status_ok) {
    235     PrintTimeZoneError(server_url, "Missing rawOffset attribute.", timezone);
    236     RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
    237     return false;
    238   }
    239 
    240   if (!response_object->GetStringWithoutPathExpansion(kTimeZoneIdString,
    241                                                       &timezone->timeZoneId) &&
    242       status_ok) {
    243     PrintTimeZoneError(server_url, "Missing timeZoneId attribute.", timezone);
    244     RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
    245     return false;
    246   }
    247 
    248   if (!response_object->GetStringWithoutPathExpansion(
    249           kTimeZoneNameString, &timezone->timeZoneName) &&
    250       status_ok) {
    251     PrintTimeZoneError(server_url, "Missing timeZoneName attribute.", timezone);
    252     RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
    253     return false;
    254   }
    255 
    256   // "error_message" field is optional. Ignore result.
    257   response_object->GetStringWithoutPathExpansion(kErrorMessageString,
    258                                                  &timezone->error_message);
    259 
    260   return true;
    261 }
    262 
    263 // Attempts to extract a position from the response. Detects and indicates
    264 // various failure cases.
    265 scoped_ptr<TimeZoneResponseData> GetTimeZoneFromResponse(
    266     bool http_success,
    267     int status_code,
    268     const std::string& response_body,
    269     const GURL& server_url) {
    270   scoped_ptr<TimeZoneResponseData> timezone(new TimeZoneResponseData);
    271 
    272   // HttpPost can fail for a number of reasons. Most likely this is because
    273   // we're offline, or there was no response.
    274   if (!http_success) {
    275     PrintTimeZoneError(server_url, "No response received", timezone.get());
    276     RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_EMPTY);
    277     return timezone.Pass();
    278   }
    279   if (status_code != net::HTTP_OK) {
    280     std::string message = "Returned error code ";
    281     message += base::IntToString(status_code);
    282     PrintTimeZoneError(server_url, message, timezone.get());
    283     RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_NOT_OK);
    284     return timezone.Pass();
    285   }
    286 
    287   if (!ParseServerResponse(server_url, response_body, timezone.get()))
    288     return timezone.Pass();
    289 
    290   RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_SUCCESS);
    291   return timezone.Pass();
    292 }
    293 
    294 }  // namespace
    295 
    296 TimeZoneResponseData::TimeZoneResponseData()
    297     : dstOffset(0), rawOffset(0), status(ZERO_RESULTS) {
    298 }
    299 
    300 GURL DefaultTimezoneProviderURL() {
    301   return GURL(kDefaultTimezoneProviderUrl);
    302 }
    303 
    304 TimeZoneRequest::TimeZoneRequest(
    305     net::URLRequestContextGetter* url_context_getter,
    306     const GURL& service_url,
    307     const Geoposition& geoposition,
    308     bool sensor,
    309     base::TimeDelta retry_timeout)
    310     : url_context_getter_(url_context_getter),
    311       service_url_(service_url),
    312       geoposition_(geoposition),
    313       sensor_(sensor),
    314       retry_timeout_abs_(base::Time::Now() + retry_timeout),
    315       retry_sleep_on_server_error_(base::TimeDelta::FromSeconds(
    316           kResolveTimeZoneRetrySleepOnServerErrorSeconds)),
    317       retry_sleep_on_bad_response_(base::TimeDelta::FromSeconds(
    318           kResolveTimeZoneRetrySleepBadResponseSeconds)),
    319       retries_(0) {
    320 }
    321 
    322 TimeZoneRequest::~TimeZoneRequest() {
    323   DCHECK(thread_checker_.CalledOnValidThread());
    324 
    325   // If callback is not empty, request is cancelled.
    326   if (!callback_.is_null()) {
    327     RecordUmaResponseTime(base::Time::Now() - request_started_at_, false);
    328     RecordUmaResult(TIMEZONE_REQUEST_RESULT_CANCELLED, retries_);
    329   }
    330 }
    331 
    332 void TimeZoneRequest::StartRequest() {
    333   DCHECK(thread_checker_.CalledOnValidThread());
    334   RecordUmaEvent(TIMEZONE_REQUEST_EVENT_REQUEST_START);
    335   request_started_at_ = base::Time::Now();
    336   ++retries_;
    337 
    338   url_fetcher_.reset(
    339       net::URLFetcher::Create(request_url_, net::URLFetcher::GET, this));
    340   url_fetcher_->SetRequestContext(url_context_getter_.get());
    341   url_fetcher_->SetLoadFlags(net::LOAD_BYPASS_CACHE |
    342                              net::LOAD_DISABLE_CACHE |
    343                              net::LOAD_DO_NOT_SAVE_COOKIES |
    344                              net::LOAD_DO_NOT_SEND_COOKIES |
    345                              net::LOAD_DO_NOT_SEND_AUTH_DATA);
    346   url_fetcher_->Start();
    347 }
    348 
    349 void TimeZoneRequest::MakeRequest(TimeZoneResponseCallback callback) {
    350   callback_ = callback;
    351   request_url_ =
    352       TimeZoneRequestURL(service_url_, geoposition_, false /* sensor */);
    353   StartRequest();
    354 }
    355 
    356 void TimeZoneRequest::Retry(bool server_error) {
    357   const base::TimeDelta delay(server_error ? retry_sleep_on_server_error_
    358                                            : retry_sleep_on_bad_response_);
    359   timezone_request_scheduled_.Start(
    360       FROM_HERE, delay, this, &TimeZoneRequest::StartRequest);
    361 }
    362 
    363 void TimeZoneRequest::OnURLFetchComplete(const net::URLFetcher* source) {
    364   DCHECK_EQ(url_fetcher_.get(), source);
    365 
    366   net::URLRequestStatus status = source->GetStatus();
    367   int response_code = source->GetResponseCode();
    368   RecordUmaResponseCode(response_code);
    369 
    370   std::string data;
    371   source->GetResponseAsString(&data);
    372   scoped_ptr<TimeZoneResponseData> timezone = GetTimeZoneFromResponse(
    373       status.is_success(), response_code, data, source->GetURL());
    374   const bool server_error =
    375       !status.is_success() || (response_code >= 500 && response_code < 600);
    376   url_fetcher_.reset();
    377 
    378   DVLOG(1) << "TimeZoneRequest::OnURLFetchComplete(): timezone={"
    379            << timezone->ToStringForDebug() << "}";
    380 
    381   const base::Time now = base::Time::Now();
    382   const bool retry_timeout = (now >= retry_timeout_abs_);
    383 
    384   const bool success = (timezone->status == TimeZoneResponseData::OK);
    385   if (!success && !retry_timeout) {
    386     Retry(server_error);
    387     return;
    388   }
    389   RecordUmaResponseTime(base::Time::Now() - request_started_at_, success);
    390 
    391   const TimeZoneRequestResult result =
    392       (server_error ? TIMEZONE_REQUEST_RESULT_SERVER_ERROR
    393                     : (success ? TIMEZONE_REQUEST_RESULT_SUCCESS
    394                                : TIMEZONE_REQUEST_RESULT_FAILURE));
    395   RecordUmaResult(result, retries_);
    396 
    397   TimeZoneResponseCallback callback = callback_;
    398 
    399   // Empty callback is used to identify "completed or not yet started request".
    400   callback_.Reset();
    401 
    402   // callback.Run() usually destroys TimeZoneRequest, because this is the way
    403   // callback is implemented in TimeZoneProvider.
    404   callback.Run(timezone.Pass(), server_error);
    405   // "this" is already destroyed here.
    406 }
    407 
    408 std::string TimeZoneResponseData::ToStringForDebug() const {
    409   static const char* const status2string[] = {
    410       "OK",
    411       "INVALID_REQUEST",
    412       "OVER_QUERY_LIMIT",
    413       "REQUEST_DENIED",
    414       "UNKNOWN_ERROR",
    415       "ZERO_RESULTS",
    416       "REQUEST_ERROR"
    417   };
    418 
    419   return base::StringPrintf(
    420       "dstOffset=%f, rawOffset=%f, timeZoneId='%s', timeZoneName='%s', "
    421       "error_message='%s', status=%u (%s)",
    422       dstOffset,
    423       rawOffset,
    424       timeZoneId.c_str(),
    425       timeZoneName.c_str(),
    426       error_message.c_str(),
    427       (unsigned)status,
    428       (status < arraysize(status2string) ? status2string[status] : "unknown"));
    429 }
    430 
    431 }  // namespace chromeos
    432