Home | History | Annotate | Download | only in captive_portal
      1 // Copyright (c) 2012 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/captive_portal/captive_portal_tab_reloader.h"
      6 
      7 #include "base/bind.h"
      8 #include "base/callback.h"
      9 #include "base/message_loop/message_loop.h"
     10 #include "chrome/browser/captive_portal/captive_portal_service.h"
     11 #include "chrome/browser/captive_portal/captive_portal_service_factory.h"
     12 #include "content/public/browser/interstitial_page.h"
     13 #include "content/public/browser/navigation_controller.h"
     14 #include "content/public/browser/navigation_entry.h"
     15 #include "content/public/browser/web_contents.h"
     16 #include "net/base/net_errors.h"
     17 
     18 namespace captive_portal {
     19 
     20 namespace {
     21 
     22 // The time to wait for a slow loading SSL page before triggering a captive
     23 // portal check.
     24 const int kDefaultSlowSSLTimeSeconds = 30;
     25 
     26 // Returns true if an SSL request resulting in |error| may indicate a captive
     27 // portal.
     28 bool SslNetErrorMayImplyCaptivePortal(int error) {
     29   // May be returned when a captive portal silently blocks an SSL request.
     30   if (error == net::ERR_CONNECTION_TIMED_OUT)
     31     return true;
     32 
     33   // May be returned when a captive portal lets SSL requests connect, but
     34   // disconnects Chrome after Chrome starts SSL negotiation, or sends an
     35   // HTTP response.
     36   if (error == net::ERR_SSL_PROTOCOL_ERROR)
     37     return true;
     38 
     39   return false;
     40 }
     41 
     42 }  // namespace
     43 
     44 CaptivePortalTabReloader::CaptivePortalTabReloader(
     45     Profile* profile,
     46     content::WebContents* web_contents,
     47     const OpenLoginTabCallback& open_login_tab_callback)
     48     : profile_(profile),
     49       web_contents_(web_contents),
     50       state_(STATE_NONE),
     51       provisional_main_frame_load_(false),
     52       ssl_url_in_redirect_chain_(false),
     53       slow_ssl_load_time_(
     54           base::TimeDelta::FromSeconds(kDefaultSlowSSLTimeSeconds)),
     55       open_login_tab_callback_(open_login_tab_callback),
     56       weak_factory_(this) {
     57 }
     58 
     59 CaptivePortalTabReloader::~CaptivePortalTabReloader() {
     60 }
     61 
     62 void CaptivePortalTabReloader::OnLoadStart(bool is_ssl) {
     63   provisional_main_frame_load_ = true;
     64   ssl_url_in_redirect_chain_ = is_ssl;
     65 
     66   SetState(STATE_NONE);
     67 
     68   // Start the slow load timer for SSL pages.
     69   // TODO(mmenke):  Should this look at the port instead?  The reason the
     70   //                request never connects is because of the port, not the
     71   //                protocol.
     72   if (is_ssl)
     73     SetState(STATE_TIMER_RUNNING);
     74 }
     75 
     76 void CaptivePortalTabReloader::OnLoadCommitted(int net_error) {
     77   provisional_main_frame_load_ = false;
     78   ssl_url_in_redirect_chain_ = false;
     79 
     80   if (state_ == STATE_NONE)
     81     return;
     82 
     83   // If |net_error| is not an error code that could indicate there's a captive
     84   // portal, reset the state.
     85   if (!SslNetErrorMayImplyCaptivePortal(net_error)) {
     86     // TODO(mmenke):  If the new URL is the same as the old broken URL, and the
     87     //                request succeeds, should probably trigger another
     88     //                captive portal check.
     89     SetState(STATE_NONE);
     90     return;
     91   }
     92 
     93   // The page returned an error out before the timer triggered.  Go ahead and
     94   // try to detect a portal now, rather than waiting for the timer.
     95   if (state_ == STATE_TIMER_RUNNING) {
     96     OnSlowSSLConnect();
     97     return;
     98   }
     99 
    100   // If the tab needs to reload, do so asynchronously, to avoid reentrancy
    101   // issues.
    102   if (state_ == STATE_NEEDS_RELOAD) {
    103     base::MessageLoop::current()->PostTask(
    104         FROM_HERE,
    105         base::Bind(&CaptivePortalTabReloader::ReloadTabIfNeeded,
    106                    weak_factory_.GetWeakPtr()));
    107   }
    108 }
    109 
    110 void CaptivePortalTabReloader::OnAbort() {
    111   provisional_main_frame_load_ = false;
    112   ssl_url_in_redirect_chain_ = false;
    113 
    114   SetState(STATE_NONE);
    115 }
    116 
    117 void CaptivePortalTabReloader::OnRedirect(bool is_ssl) {
    118   SetState(STATE_NONE);
    119   if (!is_ssl)
    120     return;
    121   // Only start the SSL timer running if no SSL URL has been seen in the current
    122   // redirect chain.  If we've already successfully downloaded one SSL URL,
    123   // assume we're not behind a captive portal.
    124   if (!ssl_url_in_redirect_chain_)
    125     SetState(STATE_TIMER_RUNNING);
    126   ssl_url_in_redirect_chain_ = true;
    127 }
    128 
    129 void CaptivePortalTabReloader::OnCaptivePortalResults(
    130     Result previous_result,
    131     Result result) {
    132   if (result == RESULT_BEHIND_CAPTIVE_PORTAL) {
    133     if (state_ == STATE_MAYBE_BROKEN_BY_PORTAL) {
    134       SetState(STATE_BROKEN_BY_PORTAL);
    135       MaybeOpenCaptivePortalLoginTab();
    136     }
    137     return;
    138   }
    139 
    140   switch (state_) {
    141     case STATE_MAYBE_BROKEN_BY_PORTAL:
    142     case STATE_TIMER_RUNNING:
    143       // If the previous result was BEHIND_CAPTIVE_PORTAL, and the state is
    144       // either STATE_MAYBE_BROKEN_BY_PORTAL or STATE_TIMER_RUNNING, reload the
    145       // tab.  In the latter case, the tab has yet to commit, but is an SSL
    146       // page, so if the page ends up at an error caused by a captive portal, it
    147       // will be reloaded.  If not, the state will just be reset.  The helps in
    148       // the case that a user tries to reload a tab, and then quickly logs in.
    149       if (previous_result == RESULT_BEHIND_CAPTIVE_PORTAL) {
    150         SetState(STATE_NEEDS_RELOAD);
    151         return;
    152       }
    153       SetState(STATE_NONE);
    154       return;
    155 
    156     case STATE_BROKEN_BY_PORTAL:
    157       // Either reload the tab now, if an error page has already been committed
    158       // or an interstitial is being displayed, or reload it if and when a
    159       // timeout commits.
    160       SetState(STATE_NEEDS_RELOAD);
    161       return;
    162 
    163     case STATE_NEEDS_RELOAD:
    164     case STATE_NONE:
    165       // If the tab needs to reload or is in STATE_NONE, do nothing.  The reload
    166       // case shouldn't be very common, since it only lasts until a tab times
    167       // out, but it's still possible.
    168       return;
    169 
    170     default:
    171       NOTREACHED();
    172   }
    173 }
    174 
    175 void CaptivePortalTabReloader::OnSSLCertError(const net::SSLInfo& ssl_info) {
    176   // TODO(mmenke):  Figure out if any cert errors should be ignored.  The
    177   // most common errors when behind captive portals are likely
    178   // ERR_CERT_COMMON_NAME_INVALID and ERR_CERT_AUTHORITY_INVALID.  It's unclear
    179   // if captive portals cause any others.
    180   if (state_ == STATE_TIMER_RUNNING)
    181     SetState(STATE_MAYBE_BROKEN_BY_PORTAL);
    182 }
    183 
    184 void CaptivePortalTabReloader::OnSlowSSLConnect() {
    185   SetState(STATE_MAYBE_BROKEN_BY_PORTAL);
    186 }
    187 
    188 void CaptivePortalTabReloader::SetState(State new_state) {
    189   // Stop the timer even when old and new states are the same.
    190   if (state_ == STATE_TIMER_RUNNING) {
    191     slow_ssl_load_timer_.Stop();
    192   } else {
    193     DCHECK(!slow_ssl_load_timer_.IsRunning());
    194   }
    195 
    196   // Check for unexpected state transitions.
    197   switch (state_) {
    198     case STATE_NONE:
    199       DCHECK(new_state == STATE_NONE ||
    200              new_state == STATE_TIMER_RUNNING);
    201       break;
    202     case STATE_TIMER_RUNNING:
    203       DCHECK(new_state == STATE_NONE ||
    204              new_state == STATE_MAYBE_BROKEN_BY_PORTAL ||
    205              new_state == STATE_NEEDS_RELOAD);
    206       break;
    207     case STATE_MAYBE_BROKEN_BY_PORTAL:
    208       DCHECK(new_state == STATE_NONE ||
    209              new_state == STATE_BROKEN_BY_PORTAL ||
    210              new_state == STATE_NEEDS_RELOAD);
    211       break;
    212     case STATE_BROKEN_BY_PORTAL:
    213       DCHECK(new_state == STATE_NONE ||
    214              new_state == STATE_NEEDS_RELOAD);
    215       break;
    216     case STATE_NEEDS_RELOAD:
    217       DCHECK_EQ(STATE_NONE, new_state);
    218       break;
    219     default:
    220       NOTREACHED();
    221       break;
    222   };
    223 
    224   state_ = new_state;
    225 
    226   switch (state_) {
    227     case STATE_TIMER_RUNNING:
    228       slow_ssl_load_timer_.Start(
    229           FROM_HERE,
    230           slow_ssl_load_time_,
    231           this,
    232           &CaptivePortalTabReloader::OnSlowSSLConnect);
    233       break;
    234 
    235     case STATE_MAYBE_BROKEN_BY_PORTAL:
    236       CheckForCaptivePortal();
    237       break;
    238 
    239     case STATE_NEEDS_RELOAD:
    240       // Try to reload the tab now.
    241       ReloadTabIfNeeded();
    242       break;
    243 
    244     default:
    245       break;
    246   }
    247 }
    248 
    249 void CaptivePortalTabReloader::ReloadTabIfNeeded() {
    250   // If the page no longer needs to be reloaded, do nothing.
    251   if (state_ != STATE_NEEDS_RELOAD)
    252     return;
    253 
    254   // If there's still a provisional load going, do nothing unless there's an
    255   // interstitial page.  Reloading when displaying an interstitial page will
    256   // reload the underlying page, even if it hasn't yet committed.
    257   if (provisional_main_frame_load_ &&
    258       !content::InterstitialPage::GetInterstitialPage(web_contents_)) {
    259     return;
    260   }
    261 
    262   SetState(STATE_NONE);
    263   ReloadTab();
    264 }
    265 
    266 void CaptivePortalTabReloader::ReloadTab() {
    267   content::NavigationController* controller = &web_contents_->GetController();
    268   if (!controller->GetActiveEntry()->GetHasPostData())
    269     controller->Reload(true);
    270 }
    271 
    272 void CaptivePortalTabReloader::MaybeOpenCaptivePortalLoginTab() {
    273   open_login_tab_callback_.Run();
    274 }
    275 
    276 void CaptivePortalTabReloader::CheckForCaptivePortal() {
    277   CaptivePortalService* service =
    278       CaptivePortalServiceFactory::GetForProfile(profile_);
    279   if (service)
    280     service->DetectCaptivePortal();
    281 }
    282 
    283 }  // namespace captive_portal
    284