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_); 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