15821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Copyright (c) 2012 The Chromium Authors. All rights reserved.
25821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Use of this source code is governed by a BSD-style license that can be
35821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// found in the LICENSE file.
45821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
55821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/browser/captive_portal/captive_portal_service.h"
65821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
75821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "base/bind.h"
85821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "base/bind_helpers.h"
95821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "base/logging.h"
109ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch#include "base/message_loop/message_loop.h"
115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "base/metrics/histogram.h"
122a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#include "base/prefs/pref_service.h"
137dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch#include "chrome/browser/chrome_notification_types.h"
145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/browser/profiles/profile.h"
155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/common/pref_names.h"
165c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu#include "components/captive_portal/captive_portal_types.h"
175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "content/public/browser/notification_service.h"
185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#if defined(OS_MACOSX)
205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "base/mac/mac_util.h"
215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#endif
225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#if defined(OS_WIN)
245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "base/win/windows_version.h"
255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#endif
265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
275c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liuusing captive_portal::CaptivePortalResult;
285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)namespace {
305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
31effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch// Make sure this enum is in sync with CaptivePortalDetectionResult enum
32effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch// in histograms.xml. This enum is append-only, don't modify existing values.
33effb81e5f8246d0db0270817048dc992db66e9fbBen Murdochenum CaptivePortalDetectionResult {
34effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  // There's a confirmed connection to the Internet.
35effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  DETECTION_RESULT_INTERNET_CONNECTED,
36effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  // Received a network or HTTP error, or a non-HTTP response.
37effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  DETECTION_RESULT_NO_RESPONSE,
38effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  // Encountered a captive portal with a non-HTTPS landing URL.
39effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL,
40effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  // Received a network or HTTP error with an HTTPS landing URL.
41effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  DETECTION_RESULT_NO_RESPONSE_HTTPS_LANDING_URL,
42effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  // Encountered a captive portal with an HTTPS landing URL.
43effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL_HTTPS_LANDING_URL,
44116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  // Received a network or HTTP error, or a non-HTTP response with IP address.
45116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  DETECTION_RESULT_NO_RESPONSE_IP_ADDRESS,
46116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  // Encountered a captive portal with a non-HTTPS, IP address landing URL.
47116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL_IP_ADDRESS,
48116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  // Received a network or HTTP error with an HTTPS, IP address landing URL.
49116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  DETECTION_RESULT_NO_RESPONSE_HTTPS_LANDING_URL_IP_ADDRESS,
50116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  // Encountered a captive portal with an HTTPS, IP address landing URL.
51116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL_HTTPS_LANDING_URL_IP_ADDRESS,
52effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  DETECTION_RESULT_COUNT
53effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch};
54effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Records histograms relating to how often captive portal detection attempts
565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// ended with |result| in a row, and for how long |result| was the last result
575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// of a detection attempt.  Recorded both on quit and on a new Result.
585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)//
595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// |repeat_count| may be 0 if there were no captive portal checks during
605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// a session.
615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)//
625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// |result_duration| is the time between when a captive portal check first
635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// returned |result| and when a check returned a different result, or when the
645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// CaptivePortalService was shut down.
655c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liuvoid RecordRepeatHistograms(CaptivePortalResult result,
665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                            int repeat_count,
675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                            base::TimeDelta result_duration) {
685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // Histogram macros can't be used with variable names, since they cache
695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // pointers, so have to use the histogram functions directly.
705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // Record number of times the last result was received in a row.
722a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  base::HistogramBase* result_repeated_histogram =
735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      base::Histogram::FactoryGet(
745c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu          "CaptivePortal.ResultRepeated." + CaptivePortalResultToString(result),
755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          1,  // min
765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          100,  // max
775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          100,  // bucket_count
785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          base::Histogram::kUmaTargetedHistogramFlag);
795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  result_repeated_histogram->Add(repeat_count);
805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if (repeat_count == 0)
825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    return;
835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // Time between first request that returned |result| and now.
852a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  base::HistogramBase* result_duration_histogram =
865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      base::Histogram::FactoryTimeGet(
875c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu          "CaptivePortal.ResultDuration." + CaptivePortalResultToString(result),
885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          base::TimeDelta::FromSeconds(1),  // min
895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          base::TimeDelta::FromHours(1),  // max
905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          50,  // bucket_count
915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          base::Histogram::kUmaTargetedHistogramFlag);
925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  result_duration_histogram->AddTime(result_duration);
935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}
945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
95effb81e5f8246d0db0270817048dc992db66e9fbBen Murdochint GetHistogramEntryForDetectionResult(
965c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu    const captive_portal::CaptivePortalDetector::Results& results) {
97effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  bool is_https = results.landing_url.SchemeIs("https");
98116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  bool is_ip = results.landing_url.HostIsIPAddress();
99effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  switch (results.result) {
1005c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu    case captive_portal::RESULT_INTERNET_CONNECTED:
101effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      return DETECTION_RESULT_INTERNET_CONNECTED;
1025c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu    case captive_portal::RESULT_NO_RESPONSE:
103116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch      if (is_ip) {
104116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch        return is_https ?
105116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch            DETECTION_RESULT_NO_RESPONSE_HTTPS_LANDING_URL_IP_ADDRESS :
106116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch            DETECTION_RESULT_NO_RESPONSE_IP_ADDRESS;
107116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch      }
108effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      return is_https ?
109effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch          DETECTION_RESULT_NO_RESPONSE_HTTPS_LANDING_URL :
110effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch          DETECTION_RESULT_NO_RESPONSE;
1115c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu    case captive_portal::RESULT_BEHIND_CAPTIVE_PORTAL:
112116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch      if (is_ip) {
113116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch        return is_https ?
114116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch          DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL_HTTPS_LANDING_URL_IP_ADDRESS :
115116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch          DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL_IP_ADDRESS;
116116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch      }
117effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      return is_https ?
118effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch          DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL_HTTPS_LANDING_URL :
119effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch          DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL;
120effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    default:
121effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      NOTREACHED();
122effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      return -1;
123effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  }
124effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch}
125effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
126a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)bool ShouldDeferToNativeCaptivePortalDetection() {
127a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  // On Windows 8, defer to the native captive portal detection.  OSX Lion and
128a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  // later also have captive portal detection, but experimentally, this code
129a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  // works in cases its does not.
130a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  //
131a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  // TODO(mmenke): Investigate how well Windows 8's captive portal detection
132a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  // works.
133a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)#if defined(OS_WIN)
1345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  return base::win::GetVersion() >= base::win::VERSION_WIN8;
1355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#else
1365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  return false;
1375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#endif
1385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}
1395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}  // namespace
1415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)CaptivePortalService::TestingState CaptivePortalService::testing_state_ =
1435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    NOT_TESTING;
1445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)class CaptivePortalService::RecheckBackoffEntry : public net::BackoffEntry {
1465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) public:
1475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  explicit RecheckBackoffEntry(CaptivePortalService* captive_portal_service)
1485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      : net::BackoffEntry(
1495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            &captive_portal_service->recheck_policy().backoff_policy),
1505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        captive_portal_service_(captive_portal_service) {
1515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  }
1525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) private:
1545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  virtual base::TimeTicks ImplGetTimeNow() const OVERRIDE {
1555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    return captive_portal_service_->GetCurrentTimeTicks();
1565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  }
1575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  CaptivePortalService* captive_portal_service_;
1595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  DISALLOW_COPY_AND_ASSIGN(RecheckBackoffEntry);
1615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)};
1625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)CaptivePortalService::RecheckPolicy::RecheckPolicy()
1645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    : initial_backoff_no_portal_ms(600 * 1000),
1655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      initial_backoff_portal_ms(20 * 1000) {
1665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // Receiving a new Result is considered a success.  All subsequent requests
1675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // that get the same Result are considered "failures", so a value of N
1685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // means exponential backoff starts after getting a result N + 2 times:
1695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // +1 for the initial success, and +1 because N failures are ignored.
1705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  //
1715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // A value of 6 means to start backoff on the 7th failure, which is the 8th
1725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // time the same result is received.
1735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  backoff_policy.num_errors_to_ignore = 6;
1745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // It doesn't matter what this is initialized to.  It will be overwritten
1765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // after the first captive portal detection request.
1775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  backoff_policy.initial_delay_ms = initial_backoff_no_portal_ms;
1785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  backoff_policy.multiply_factor = 2.0;
1805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  backoff_policy.jitter_factor = 0.3;
1815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  backoff_policy.maximum_backoff_ms = 2 * 60 * 1000;
1825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // -1 means the entry never expires.  This doesn't really matter, as the
1845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // service never checks for its expiration.
1855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  backoff_policy.entry_lifetime_ms = -1;
1865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  backoff_policy.always_use_initial_delay = true;
1885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}
1895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)CaptivePortalService::CaptivePortalService(Profile* profile)
1915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    : profile_(profile),
1925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      state_(STATE_IDLE),
1935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      captive_portal_detector_(profile->GetRequestContext()),
1945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      enabled_(false),
1955c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu      last_detection_result_(captive_portal::RESULT_INTERNET_CONNECTED),
1965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      num_checks_with_same_result_(0),
1975c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu      test_url_(captive_portal::CaptivePortalDetector::kDefaultURL) {
1985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // The order matters here:
1995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // |resolve_errors_with_web_service_| must be initialized and |backoff_entry_|
2005821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // created before the call to UpdateEnabledState.
2015821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  resolve_errors_with_web_service_.Init(
2025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      prefs::kAlternateErrorPagesEnabled,
2035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      profile_->GetPrefs(),
2042a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)      base::Bind(&CaptivePortalService::UpdateEnabledState,
2052a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                 base::Unretained(this)));
2065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  ResetBackoffEntry(last_detection_result_);
2075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  UpdateEnabledState();
2095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}
2105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)CaptivePortalService::~CaptivePortalService() {
2125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}
2135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)void CaptivePortalService::DetectCaptivePortal() {
2155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  DCHECK(CalledOnValidThread());
2165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // If a request is pending or running, do nothing.
2185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if (state_ == STATE_CHECKING_FOR_PORTAL || state_ == STATE_TIMER_RUNNING)
2195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    return;
2205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  base::TimeDelta time_until_next_check = backoff_entry_->GetTimeUntilRelease();
2225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // Start asynchronously.
2245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  state_ = STATE_TIMER_RUNNING;
2255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  check_captive_portal_timer_.Start(
2265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      FROM_HERE,
2275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      time_until_next_check,
2285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      this,
2295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      &CaptivePortalService::DetectCaptivePortalInternal);
2305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}
2315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)void CaptivePortalService::DetectCaptivePortalInternal() {
2335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  DCHECK(CalledOnValidThread());
2345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  DCHECK(state_ == STATE_TIMER_RUNNING || state_ == STATE_IDLE);
2355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  DCHECK(!TimerRunning());
2365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  state_ = STATE_CHECKING_FOR_PORTAL;
2385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // When not enabled, just claim there's an Internet connection.
2405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if (!enabled_) {
2415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // Count this as a success, so the backoff entry won't apply exponential
2425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // backoff, but will apply the standard delay.
2435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    backoff_entry_->InformOfRequest(true);
2445c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu    OnResult(captive_portal::RESULT_INTERNET_CONNECTED);
2455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    return;
2465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  }
2475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  captive_portal_detector_.DetectCaptivePortal(
2495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      test_url_, base::Bind(
2505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          &CaptivePortalService::OnPortalDetectionCompleted,
2515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          base::Unretained(this)));
2525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}
2535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)void CaptivePortalService::OnPortalDetectionCompleted(
2555c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu    const captive_portal::CaptivePortalDetector::Results& results) {
2565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  DCHECK(CalledOnValidThread());
2575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  DCHECK_EQ(STATE_CHECKING_FOR_PORTAL, state_);
2585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  DCHECK(!TimerRunning());
2595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  DCHECK(enabled_);
2605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2615c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu  CaptivePortalResult result = results.result;
2625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  const base::TimeDelta& retry_after_delta = results.retry_after_delta;
2635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  base::TimeTicks now = GetCurrentTimeTicks();
2645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // Record histograms.
2665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  UMA_HISTOGRAM_ENUMERATION("CaptivePortal.DetectResult",
267effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch                            GetHistogramEntryForDetectionResult(results),
268effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch                            DETECTION_RESULT_COUNT);
2695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // If this isn't the first captive portal result, record stats.
2715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if (!last_check_time_.is_null()) {
2725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    UMA_HISTOGRAM_LONG_TIMES("CaptivePortal.TimeBetweenChecks",
2735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                             now - last_check_time_);
2745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    if (last_detection_result_ != result) {
2765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // If the last result was different from the result of the latest test,
2775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // record histograms about the previous period over which the result was
2785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      // the same.
2795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      RecordRepeatHistograms(last_detection_result_,
2805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                             num_checks_with_same_result_,
2815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                             now - first_check_time_with_same_result_);
2825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    }
2835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  }
2845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if (last_check_time_.is_null() || result != last_detection_result_) {
2865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    first_check_time_with_same_result_ = now;
2875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    num_checks_with_same_result_ = 1;
2885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // Reset the backoff entry both to update the default time and clear
2905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // previous failures.
2915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    ResetBackoffEntry(result);
2925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    backoff_entry_->SetCustomReleaseTime(now + retry_after_delta);
2945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // The BackoffEntry is not informed of this request, so there's no delay
2955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // before the next request.  This allows for faster login when a captive
2965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // portal is first detected.  It can also help when moving between captive
2975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // portals.
2985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  } else {
2995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    DCHECK_LE(1, num_checks_with_same_result_);
3005821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    ++num_checks_with_same_result_;
3015821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // Requests that have the same Result as the last one are considered
3035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // "failures", to trigger backoff.
3045821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    backoff_entry_->SetCustomReleaseTime(now + retry_after_delta);
3055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    backoff_entry_->InformOfRequest(false);
3065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  }
3075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  last_check_time_ = now;
3095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  OnResult(result);
3115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}
3125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)void CaptivePortalService::Shutdown() {
3145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  DCHECK(CalledOnValidThread());
3155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if (enabled_) {
3165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    RecordRepeatHistograms(
3175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        last_detection_result_,
3185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        num_checks_with_same_result_,
3195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        GetCurrentTimeTicks() - first_check_time_with_same_result_);
3205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  }
3215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}
3225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3235c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liuvoid CaptivePortalService::OnResult(CaptivePortalResult result) {
3245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  DCHECK_EQ(STATE_CHECKING_FOR_PORTAL, state_);
3255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  state_ = STATE_IDLE;
3265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Results results;
3285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  results.previous_result = last_detection_result_;
3295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  results.result = result;
3305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  last_detection_result_ = result;
3315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  content::NotificationService::current()->Notify(
3335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      chrome::NOTIFICATION_CAPTIVE_PORTAL_CHECK_RESULT,
3345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      content::Source<Profile>(profile_),
3355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      content::Details<Results>(&results));
3365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}
3375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3385c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liuvoid CaptivePortalService::ResetBackoffEntry(CaptivePortalResult result) {
3395c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu  if (!enabled_ || result == captive_portal::RESULT_BEHIND_CAPTIVE_PORTAL) {
3405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // Use the shorter time when the captive portal service is not enabled, or
3415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // behind a captive portal.
3425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    recheck_policy_.backoff_policy.initial_delay_ms =
3435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        recheck_policy_.initial_backoff_portal_ms;
3445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  } else {
3455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    recheck_policy_.backoff_policy.initial_delay_ms =
3465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        recheck_policy_.initial_backoff_no_portal_ms;
3475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  }
3485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  backoff_entry_.reset(new RecheckBackoffEntry(this));
3505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}
3515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)void CaptivePortalService::UpdateEnabledState() {
3532a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)  DCHECK(CalledOnValidThread());
3545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  bool enabled_before = enabled_;
3555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  enabled_ = testing_state_ != DISABLED_FOR_TESTING &&
3565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)             resolve_errors_with_web_service_.GetValue();
3575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if (testing_state_ != SKIP_OS_CHECK_FOR_TESTING &&
359a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)      ShouldDeferToNativeCaptivePortalDetection()) {
3605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    enabled_ = false;
3615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  }
3625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if (enabled_before == enabled_)
3645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    return;
3655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  // Clear data used for histograms.
3675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  num_checks_with_same_result_ = 0;
3685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  first_check_time_with_same_result_ = base::TimeTicks();
3695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  last_check_time_ = base::TimeTicks();
3705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  ResetBackoffEntry(last_detection_result_);
3725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if (state_ == STATE_CHECKING_FOR_PORTAL || state_ == STATE_TIMER_RUNNING) {
3745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // If a captive portal check was running or pending, cancel check
3755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // and the timer.
3765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    check_captive_portal_timer_.Stop();
3775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    captive_portal_detector_.Cancel();
3785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    state_ = STATE_IDLE;
3795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // Since a captive portal request was queued or running, something may be
3815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    // expecting to receive a captive portal result.
3825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    DetectCaptivePortal();
3835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  }
3845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}
3855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)base::TimeTicks CaptivePortalService::GetCurrentTimeTicks() const {
3875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if (time_ticks_for_testing_.is_null())
3885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    return base::TimeTicks::Now();
3895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  else
3905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    return time_ticks_for_testing_;
3915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}
3925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)bool CaptivePortalService::DetectionInProgress() const {
3945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  return state_ == STATE_CHECKING_FOR_PORTAL;
3955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}
3965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)bool CaptivePortalService::TimerRunning() const {
3985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  return check_captive_portal_timer_.IsRunning();
3995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}
400