Home | History | Annotate | Download | only in geolocation
      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/geolocation/simple_geolocation_request.h"
      6 
      7 #include <algorithm>
      8 #include <string>
      9 
     10 #include "base/json/json_reader.h"
     11 #include "base/metrics/histogram.h"
     12 #include "base/metrics/sparse_histogram.h"
     13 #include "base/strings/string_number_conversions.h"
     14 #include "base/strings/stringprintf.h"
     15 #include "base/time/time.h"
     16 #include "base/values.h"
     17 #include "chrome/browser/chromeos/geolocation/geoposition.h"
     18 #include "chrome/browser/chromeos/geolocation/simple_geolocation_provider.h"
     19 #include "google_apis/google_api_keys.h"
     20 #include "net/base/escape.h"
     21 #include "net/base/load_flags.h"
     22 #include "net/http/http_status_code.h"
     23 #include "net/url_request/url_fetcher.h"
     24 #include "net/url_request/url_request_context_getter.h"
     25 #include "net/url_request/url_request_status.h"
     26 
     27 // Location resolve timeout is usually 1 minute, so 2 minutes with 50 buckets
     28 // should be enough.
     29 #define UMA_HISTOGRAM_LOCATION_RESPONSE_TIMES(name, sample)         \
     30   UMA_HISTOGRAM_CUSTOM_TIMES(name,                                  \
     31                              sample,                                \
     32                              base::TimeDelta::FromMilliseconds(10), \
     33                              base::TimeDelta::FromMinutes(2),       \
     34                              50)
     35 
     36 namespace chromeos {
     37 
     38 namespace {
     39 
     40 // The full request text. (no parameters are supported by now)
     41 const char kSimpleGeolocationRequestBody[] = "{\"considerIP\": \"true\"}";
     42 
     43 // Response data.
     44 const char kLocationString[] = "location";
     45 const char kLatString[] = "lat";
     46 const char kLngString[] = "lng";
     47 const char kAccuracyString[] = "accuracy";
     48 // Error object and its contents.
     49 const char kErrorString[] = "error";
     50 // "errors" array in "erorr" object is ignored.
     51 const char kCodeString[] = "code";
     52 const char kMessageString[] = "message";
     53 
     54 // We are using "sparse" histograms for the number of retry attempts,
     55 // so we need to explicitly limit maximum value (in case something goes wrong).
     56 const size_t kMaxRetriesValueInHistograms = 20;
     57 
     58 // Sleep between geolocation request retry on HTTP error.
     59 const unsigned int kResolveGeolocationRetrySleepOnServerErrorSeconds = 5;
     60 
     61 // Sleep between geolocation request retry on bad server response.
     62 const unsigned int kResolveGeolocationRetrySleepBadResponseSeconds = 10;
     63 
     64 enum SimpleGeolocationRequestEvent {
     65   // NOTE: Do not renumber these as that would confuse interpretation of
     66   // previously logged data. When making changes, also update the enum list
     67   // in tools/metrics/histograms/histograms.xml to keep it in sync.
     68   SIMPLE_GEOLOCATION_REQUEST_EVENT_REQUEST_START = 0,
     69   SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_SUCCESS = 1,
     70   SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_NOT_OK = 2,
     71   SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_EMPTY = 3,
     72   SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_MALFORMED = 4,
     73 
     74   // NOTE: Add entries only immediately above this line.
     75   SIMPLE_GEOLOCATION_REQUEST_EVENT_COUNT = 5
     76 };
     77 
     78 enum SimpleGeolocationRequestResult {
     79   // NOTE: Do not renumber these as that would confuse interpretation of
     80   // previously logged data. When making changes, also update the enum list
     81   // in tools/metrics/histograms/histograms.xml to keep it in sync.
     82   SIMPLE_GEOLOCATION_REQUEST_RESULT_SUCCESS = 0,
     83   SIMPLE_GEOLOCATION_REQUEST_RESULT_FAILURE = 1,
     84   SIMPLE_GEOLOCATION_REQUEST_RESULT_SERVER_ERROR = 2,
     85   SIMPLE_GEOLOCATION_REQUEST_RESULT_CANCELLED = 3,
     86 
     87   // NOTE: Add entries only immediately above this line.
     88   SIMPLE_GEOLOCATION_REQUEST_RESULT_COUNT = 4
     89 };
     90 
     91 // Too many requests (more than 1) mean there is a problem in implementation.
     92 void RecordUmaEvent(SimpleGeolocationRequestEvent event) {
     93   UMA_HISTOGRAM_ENUMERATION("SimpleGeolocation.Request.Event",
     94                             event,
     95                             SIMPLE_GEOLOCATION_REQUEST_EVENT_COUNT);
     96 }
     97 
     98 void RecordUmaResponseCode(int code) {
     99   UMA_HISTOGRAM_SPARSE_SLOWLY("SimpleGeolocation.Request.ResponseCode", code);
    100 }
    101 
    102 // Slow geolocation resolve leads to bad user experience.
    103 void RecordUmaResponseTime(base::TimeDelta elapsed, bool success) {
    104   if (success) {
    105     UMA_HISTOGRAM_LOCATION_RESPONSE_TIMES(
    106         "SimpleGeolocation.Request.ResponseSuccessTime", elapsed);
    107   } else {
    108     UMA_HISTOGRAM_LOCATION_RESPONSE_TIMES(
    109         "SimpleGeolocation.Request.ResponseFailureTime", elapsed);
    110   }
    111 }
    112 
    113 void RecordUmaResult(SimpleGeolocationRequestResult result, size_t retries) {
    114   UMA_HISTOGRAM_ENUMERATION("SimpleGeolocation.Request.Result",
    115                             result,
    116                             SIMPLE_GEOLOCATION_REQUEST_RESULT_COUNT);
    117   UMA_HISTOGRAM_SPARSE_SLOWLY("SimpleGeolocation.Request.Retries",
    118                               std::min(retries, kMaxRetriesValueInHistograms));
    119 }
    120 
    121 // Creates the request url to send to the server.
    122 GURL GeolocationRequestURL(const GURL& url) {
    123   if (url != SimpleGeolocationProvider::DefaultGeolocationProviderURL())
    124     return url;
    125 
    126   std::string api_key = google_apis::GetAPIKey();
    127   if (api_key.empty())
    128     return url;
    129 
    130   std::string query(url.query());
    131   if (!query.empty())
    132     query += "&";
    133   query += "key=" + net::EscapeQueryParamValue(api_key, true);
    134   GURL::Replacements replacements;
    135   replacements.SetQueryStr(query);
    136   return url.ReplaceComponents(replacements);
    137 }
    138 
    139 void PrintGeolocationError(const GURL& server_url,
    140                            const std::string& message,
    141                            Geoposition* position) {
    142   position->status = Geoposition::STATUS_SERVER_ERROR;
    143   position->error_message =
    144       base::StringPrintf("SimpleGeolocation provider at '%s' : %s.",
    145                          server_url.GetOrigin().spec().c_str(),
    146                          message.c_str());
    147   VLOG(1) << "SimpleGeolocationRequest::GetGeolocationFromResponse() : "
    148           << position->error_message;
    149 }
    150 
    151 // Parses the server response body. Returns true if parsing was successful.
    152 // Sets |*position| to the parsed Geolocation if a valid position was received,
    153 // otherwise leaves it unchanged.
    154 bool ParseServerResponse(const GURL& server_url,
    155                          const std::string& response_body,
    156                          Geoposition* position) {
    157   DCHECK(position);
    158 
    159   if (response_body.empty()) {
    160     PrintGeolocationError(
    161         server_url, "Server returned empty response", position);
    162     RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_EMPTY);
    163     return false;
    164   }
    165   VLOG(1) << "SimpleGeolocationRequest::ParseServerResponse() : "
    166              "Parsing response '" << response_body << "'";
    167 
    168   // Parse the response, ignoring comments.
    169   std::string error_msg;
    170   scoped_ptr<base::Value> response_value(base::JSONReader::ReadAndReturnError(
    171       response_body, base::JSON_PARSE_RFC, NULL, &error_msg));
    172   if (response_value == NULL) {
    173     PrintGeolocationError(
    174         server_url, "JSONReader failed: " + error_msg, position);
    175     RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_MALFORMED);
    176     return false;
    177   }
    178 
    179   base::DictionaryValue* response_object = NULL;
    180   if (!response_value->GetAsDictionary(&response_object)) {
    181     PrintGeolocationError(
    182         server_url,
    183         "Unexpected response type : " +
    184             base::StringPrintf("%u", response_value->GetType()),
    185         position);
    186     RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_MALFORMED);
    187     return false;
    188   }
    189 
    190   base::DictionaryValue* error_object = NULL;
    191   base::DictionaryValue* location_object = NULL;
    192   response_object->GetDictionaryWithoutPathExpansion(kLocationString,
    193                                                      &location_object);
    194   response_object->GetDictionaryWithoutPathExpansion(kErrorString,
    195                                                      &error_object);
    196 
    197   position->timestamp = base::Time::Now();
    198 
    199   if (error_object) {
    200     if (!error_object->GetStringWithoutPathExpansion(
    201             kMessageString, &(position->error_message))) {
    202       position->error_message = "Server returned error without message.";
    203     }
    204 
    205     // Ignore result (code defaults to zero).
    206     error_object->GetIntegerWithoutPathExpansion(kCodeString,
    207                                                  &(position->error_code));
    208   } else {
    209     position->error_message.erase();
    210   }
    211 
    212   if (location_object) {
    213     if (!location_object->GetDoubleWithoutPathExpansion(
    214             kLatString, &(position->latitude))) {
    215       PrintGeolocationError(server_url, "Missing 'lat' attribute.", position);
    216       RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_MALFORMED);
    217       return false;
    218     }
    219     if (!location_object->GetDoubleWithoutPathExpansion(
    220             kLngString, &(position->longitude))) {
    221       PrintGeolocationError(server_url, "Missing 'lon' attribute.", position);
    222       RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_MALFORMED);
    223       return false;
    224     }
    225     if (!response_object->GetDoubleWithoutPathExpansion(
    226             kAccuracyString, &(position->accuracy))) {
    227       PrintGeolocationError(
    228           server_url, "Missing 'accuracy' attribute.", position);
    229       RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_MALFORMED);
    230       return false;
    231     }
    232   }
    233 
    234   if (error_object) {
    235     position->status = Geoposition::STATUS_SERVER_ERROR;
    236     return false;
    237   }
    238   // Empty response is STATUS_OK but not Valid().
    239   position->status = Geoposition::STATUS_OK;
    240   return true;
    241 }
    242 
    243 // Attempts to extract a position from the response. Detects and indicates
    244 // various failure cases.
    245 bool GetGeolocationFromResponse(bool http_success,
    246                                 int status_code,
    247                                 const std::string& response_body,
    248                                 const GURL& server_url,
    249                                 Geoposition* position) {
    250 
    251   // HttpPost can fail for a number of reasons. Most likely this is because
    252   // we're offline, or there was no response.
    253   if (!http_success) {
    254     PrintGeolocationError(server_url, "No response received", position);
    255     RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_EMPTY);
    256     return false;
    257   }
    258   if (status_code != net::HTTP_OK) {
    259     std::string message = "Returned error code ";
    260     message += base::IntToString(status_code);
    261     PrintGeolocationError(server_url, message, position);
    262     RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_NOT_OK);
    263     return false;
    264   }
    265 
    266   return ParseServerResponse(server_url, response_body, position);
    267 }
    268 
    269 }  // namespace
    270 
    271 SimpleGeolocationRequest::SimpleGeolocationRequest(
    272     net::URLRequestContextGetter* url_context_getter,
    273     const GURL& service_url,
    274     base::TimeDelta timeout)
    275     : url_context_getter_(url_context_getter),
    276       service_url_(service_url),
    277       retry_sleep_on_server_error_(base::TimeDelta::FromSeconds(
    278           kResolveGeolocationRetrySleepOnServerErrorSeconds)),
    279       retry_sleep_on_bad_response_(base::TimeDelta::FromSeconds(
    280           kResolveGeolocationRetrySleepBadResponseSeconds)),
    281       timeout_(timeout),
    282       retries_(0) {
    283 }
    284 
    285 SimpleGeolocationRequest::~SimpleGeolocationRequest() {
    286   DCHECK(thread_checker_.CalledOnValidThread());
    287 
    288   // If callback is not empty, request is cancelled.
    289   if (!callback_.is_null()) {
    290     RecordUmaResponseTime(base::Time::Now() - request_started_at_, false);
    291     RecordUmaResult(SIMPLE_GEOLOCATION_REQUEST_RESULT_CANCELLED, retries_);
    292   }
    293 }
    294 
    295 void SimpleGeolocationRequest::StartRequest() {
    296   DCHECK(thread_checker_.CalledOnValidThread());
    297   RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_REQUEST_START);
    298   ++retries_;
    299 
    300   url_fetcher_.reset(
    301       net::URLFetcher::Create(request_url_, net::URLFetcher::POST, this));
    302   url_fetcher_->SetRequestContext(url_context_getter_.get());
    303   url_fetcher_->SetUploadData("application/json",
    304                               std::string(kSimpleGeolocationRequestBody));
    305   url_fetcher_->SetLoadFlags(net::LOAD_BYPASS_CACHE |
    306                              net::LOAD_DISABLE_CACHE |
    307                              net::LOAD_DO_NOT_SAVE_COOKIES |
    308                              net::LOAD_DO_NOT_SEND_COOKIES |
    309                              net::LOAD_DO_NOT_SEND_AUTH_DATA);
    310   url_fetcher_->Start();
    311 }
    312 
    313 void SimpleGeolocationRequest::MakeRequest(const ResponseCallback& callback) {
    314   callback_ = callback;
    315   request_url_ = GeolocationRequestURL(service_url_);
    316   timeout_timer_.Start(
    317       FROM_HERE, timeout_, this, &SimpleGeolocationRequest::OnTimeout);
    318   request_started_at_ = base::Time::Now();
    319   StartRequest();
    320 }
    321 
    322 void SimpleGeolocationRequest::Retry(bool server_error) {
    323   base::TimeDelta delay(server_error ? retry_sleep_on_server_error_
    324                                      : retry_sleep_on_bad_response_);
    325   request_scheduled_.Start(
    326       FROM_HERE, delay, this, &SimpleGeolocationRequest::StartRequest);
    327 }
    328 
    329 void SimpleGeolocationRequest::OnURLFetchComplete(
    330     const net::URLFetcher* source) {
    331   DCHECK_EQ(url_fetcher_.get(), source);
    332 
    333   net::URLRequestStatus status = source->GetStatus();
    334   int response_code = source->GetResponseCode();
    335   RecordUmaResponseCode(response_code);
    336 
    337   std::string data;
    338   source->GetResponseAsString(&data);
    339   const bool parse_success = GetGeolocationFromResponse(
    340       status.is_success(), response_code, data, source->GetURL(), &position_);
    341   const bool server_error =
    342       !status.is_success() || (response_code >= 500 && response_code < 600);
    343   const bool success = parse_success && position_.Valid();
    344   url_fetcher_.reset();
    345 
    346   DVLOG(1) << "SimpleGeolocationRequest::OnURLFetchComplete(): position={"
    347            << position_.ToString() << "}";
    348 
    349   if (!success) {
    350     Retry(server_error);
    351     return;
    352   }
    353   const base::TimeDelta elapsed = base::Time::Now() - request_started_at_;
    354   RecordUmaResponseTime(elapsed, success);
    355 
    356   RecordUmaResult(SIMPLE_GEOLOCATION_REQUEST_RESULT_SUCCESS, retries_);
    357 
    358   ReplyAndDestroySelf(elapsed, server_error);
    359   // "this" is already destroyed here.
    360 }
    361 
    362 void SimpleGeolocationRequest::ReplyAndDestroySelf(
    363     const base::TimeDelta elapsed,
    364     bool server_error) {
    365   url_fetcher_.reset();
    366   timeout_timer_.Stop();
    367   request_scheduled_.Stop();
    368 
    369   ResponseCallback callback = callback_;
    370 
    371   // Empty callback is used to identify "completed or not yet started request".
    372   callback_.Reset();
    373 
    374   // callback.Run() usually destroys SimpleGeolocationRequest, because this is
    375   // the way callback is implemented in GeolocationProvider.
    376   callback.Run(position_, server_error, elapsed);
    377   // "this" is already destroyed here.
    378 }
    379 
    380 void SimpleGeolocationRequest::OnTimeout() {
    381   const SimpleGeolocationRequestResult result =
    382       (position_.status == Geoposition::STATUS_SERVER_ERROR
    383            ? SIMPLE_GEOLOCATION_REQUEST_RESULT_SERVER_ERROR
    384            : SIMPLE_GEOLOCATION_REQUEST_RESULT_FAILURE);
    385   RecordUmaResult(result, retries_);
    386   position_.status = Geoposition::STATUS_TIMEOUT;
    387   const base::TimeDelta elapsed = base::Time::Now() - request_started_at_;
    388   ReplyAndDestroySelf(elapsed, true /* server_error */);
    389   // "this" is already destroyed here.
    390 }
    391 
    392 }  // namespace chromeos
    393