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