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 36namespace chromeos { 37 38namespace { 39 40// The full request text. (no parameters are supported by now) 41const char kSimpleGeolocationRequestBody[] = "{\"considerIP\": \"true\"}"; 42 43// Response data. 44const char kLocationString[] = "location"; 45const char kLatString[] = "lat"; 46const char kLngString[] = "lng"; 47const char kAccuracyString[] = "accuracy"; 48// Error object and its contents. 49const char kErrorString[] = "error"; 50// "errors" array in "erorr" object is ignored. 51const char kCodeString[] = "code"; 52const 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). 56const size_t kMaxRetriesValueInHistograms = 20; 57 58// Sleep between geolocation request retry on HTTP error. 59const unsigned int kResolveGeolocationRetrySleepOnServerErrorSeconds = 5; 60 61// Sleep between geolocation request retry on bad server response. 62const unsigned int kResolveGeolocationRetrySleepBadResponseSeconds = 10; 63 64enum 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 78enum 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. 92void RecordUmaEvent(SimpleGeolocationRequestEvent event) { 93 UMA_HISTOGRAM_ENUMERATION("SimpleGeolocation.Request.Event", 94 event, 95 SIMPLE_GEOLOCATION_REQUEST_EVENT_COUNT); 96} 97 98void RecordUmaResponseCode(int code) { 99 UMA_HISTOGRAM_SPARSE_SLOWLY("SimpleGeolocation.Request.ResponseCode", code); 100} 101 102// Slow geolocation resolve leads to bad user experience. 103void 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 113void 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. 122GURL 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 139void 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. 154bool 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. 245bool 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 271SimpleGeolocationRequest::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 285SimpleGeolocationRequest::~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 295void 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 313void 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 322void 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 329void 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 362void 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 380void 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