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