toolkit/components/captivedetect/captivedetect.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/components/captivedetect/captivedetect.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,448 @@
     1.4 +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.7 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.8 +
     1.9 +'use strict';
    1.10 +
    1.11 +const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
    1.12 +
    1.13 +Cu.import('resource://gre/modules/XPCOMUtils.jsm');
    1.14 +Cu.import('resource://gre/modules/Services.jsm');
    1.15 +
    1.16 +const DEBUG = false; // set to true to show debug messages
    1.17 +
    1.18 +const kCAPTIVEPORTALDETECTOR_CONTRACTID = '@mozilla.org/toolkit/captive-detector;1';
    1.19 +const kCAPTIVEPORTALDETECTOR_CID        = Components.ID('{d9cd00ba-aa4d-47b1-8792-b1fe0cd35060}');
    1.20 +
    1.21 +const kOpenCaptivePortalLoginEvent = 'captive-portal-login';
    1.22 +const kAbortCaptivePortalLoginEvent = 'captive-portal-login-abort';
    1.23 +
    1.24 +function URLFetcher(url, timeout) {
    1.25 +  let self = this;
    1.26 +  let xhr = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
    1.27 +              .createInstance(Ci.nsIXMLHttpRequest);
    1.28 +  xhr.open('GET', url, true);
    1.29 +  // Prevent the request from reading from the cache.
    1.30 +  xhr.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
    1.31 +  // Prevent the request from writing to the cache.
    1.32 +  xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
    1.33 +  // Prevent privacy leaks
    1.34 +  xhr.channel.loadFlags |= Ci.nsIRequest.LOAD_ANONYMOUS;
    1.35 +  // The Cache-Control header is only interpreted by proxies and the
    1.36 +  // final destination. It does not help if a resource is already
    1.37 +  // cached locally.
    1.38 +  xhr.setRequestHeader("Cache-Control", "no-cache");
    1.39 +  // HTTP/1.0 servers might not implement Cache-Control and
    1.40 +  // might only implement Pragma: no-cache
    1.41 +  xhr.setRequestHeader("Pragma", "no-cache");
    1.42 +
    1.43 +  xhr.timeout = timeout;
    1.44 +  xhr.ontimeout = function () { self.ontimeout(); };
    1.45 +  xhr.onerror = function () { self.onerror(); };
    1.46 +  xhr.onreadystatechange = function(oEvent) {
    1.47 +    if (xhr.readyState === 4) {
    1.48 +      if (self._isAborted) {
    1.49 +        return;
    1.50 +      }
    1.51 +      if (xhr.status === 200) {
    1.52 +        self.onsuccess(xhr.responseText);
    1.53 +      } else if (xhr.status) {
    1.54 +        self.onredirectorerror(xhr.status);
    1.55 +      }
    1.56 +    }
    1.57 +  };
    1.58 +  xhr.send();
    1.59 +  this._xhr = xhr;
    1.60 +}
    1.61 +
    1.62 +URLFetcher.prototype = {
    1.63 +  _isAborted: false,
    1.64 +  ontimeout: function() {},
    1.65 +  onerror: function() {},
    1.66 +  abort: function() {
    1.67 +    if (!this._isAborted) {
    1.68 +      this._isAborted = true;
    1.69 +      this._xhr.abort();
    1.70 +    }
    1.71 +  },
    1.72 +}
    1.73 +
    1.74 +function LoginObserver(captivePortalDetector) {
    1.75 +  const LOGIN_OBSERVER_STATE_DETACHED = 0; /* Should not monitor network activity since no ongoing login procedure */
    1.76 +  const LOGIN_OBSERVER_STATE_IDLE = 1; /* No network activity currently, waiting for a longer enough idle period */
    1.77 +  const LOGIN_OBSERVER_STATE_BURST = 2; /* Network activity is detected, probably caused by a login procedure */
    1.78 +  const LOGIN_OBSERVER_STATE_VERIFY_NEEDED = 3; /* Verifing network accessiblity is required after a long enough idle */
    1.79 +  const LOGIN_OBSERVER_STATE_VERIFYING = 4; /* LoginObserver is probing if public network is available */
    1.80 +
    1.81 +  let state = LOGIN_OBSERVER_STATE_DETACHED;
    1.82 +
    1.83 +  let timer = Cc['@mozilla.org/timer;1'].createInstance(Ci.nsITimer);
    1.84 +  let activityDistributor = Cc['@mozilla.org/network/http-activity-distributor;1']
    1.85 +                              .getService(Ci.nsIHttpActivityDistributor);
    1.86 +  let urlFetcher = null;
    1.87 +
    1.88 +  let pageCheckingDone = function pageCheckingDone() {
    1.89 +    if (state === LOGIN_OBSERVER_STATE_VERIFYING) {
    1.90 +      urlFetcher = null;
    1.91 +      // Finish polling the canonical site, switch back to idle state and
    1.92 +      // waiting for next burst
    1.93 +      state = LOGIN_OBSERVER_STATE_IDLE;
    1.94 +      timer.initWithCallback(observer,
    1.95 +                             captivePortalDetector._pollingTime,
    1.96 +                             timer.TYPE_ONE_SHOT);
    1.97 +    }
    1.98 +  };
    1.99 +
   1.100 +  let checkPageContent = function checkPageContent() {
   1.101 +    debug("checking if public network is available after the login procedure");
   1.102 +
   1.103 +    urlFetcher = new URLFetcher(captivePortalDetector._canonicalSiteURL,
   1.104 +                                captivePortalDetector._maxWaitingTime);
   1.105 +    urlFetcher.ontimeout = pageCheckingDone;
   1.106 +    urlFetcher.onerror = pageCheckingDone;
   1.107 +    urlFetcher.onsuccess = function (content) {
   1.108 +      if (captivePortalDetector.validateContent(content)) {
   1.109 +        urlFetcher = null;
   1.110 +        captivePortalDetector.executeCallback(true);
   1.111 +      } else {
   1.112 +        pageCheckingDone();
   1.113 +      }
   1.114 +    };
   1.115 +    urlFetcher.onredirectorerror = pageCheckingDone;
   1.116 +  };
   1.117 +
   1.118 +  // Public interface of LoginObserver
   1.119 +  let observer = {
   1.120 +    QueryInterface: XPCOMUtils.generateQI([Ci.nsIHttpActivityOberver,
   1.121 +                                           Ci.nsITimerCallback]),
   1.122 +
   1.123 +    attach: function attach() {
   1.124 +      if (state === LOGIN_OBSERVER_STATE_DETACHED) {
   1.125 +        activityDistributor.addObserver(this);
   1.126 +        state = LOGIN_OBSERVER_STATE_IDLE;
   1.127 +        timer.initWithCallback(this,
   1.128 +                               captivePortalDetector._pollingTime,
   1.129 +                               timer.TYPE_ONE_SHOT);
   1.130 +        debug('attach HttpObserver for login activity');
   1.131 +      }
   1.132 +    },
   1.133 +
   1.134 +    detach: function detach() {
   1.135 +      if (state !== LOGIN_OBSERVER_STATE_DETACHED) {
   1.136 +        if (urlFetcher) {
   1.137 +          urlFetcher.abort();
   1.138 +          urlFetcher = null;
   1.139 +        }
   1.140 +        activityDistributor.removeObserver(this);
   1.141 +        timer.cancel();
   1.142 +        state = LOGIN_OBSERVER_STATE_DETACHED;
   1.143 +        debug('detach HttpObserver for login activity');
   1.144 +      }
   1.145 +    },
   1.146 +
   1.147 +    /*
   1.148 +     * Treat all HTTP transactions as captive portal login activities.
   1.149 +     */
   1.150 +    observeActivity: function observeActivity(aHttpChannel, aActivityType,
   1.151 +                                              aActivitySubtype, aTimestamp,
   1.152 +                                              aExtraSizeData, aExtraStringData) {
   1.153 +      if (aActivityType === Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION
   1.154 +          && aActivitySubtype === Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE) {
   1.155 +        switch (state) {
   1.156 +          case LOGIN_OBSERVER_STATE_IDLE:
   1.157 +          case LOGIN_OBSERVER_STATE_VERIFY_NEEDED:
   1.158 +            state = LOGIN_OBSERVER_STATE_BURST;
   1.159 +            break;
   1.160 +          default:
   1.161 +            break;
   1.162 +        }
   1.163 +      }
   1.164 +    },
   1.165 +
   1.166 +    /*
   1.167 +     * Check if login activity is finished according to HTTP burst.
   1.168 +     */
   1.169 +    notify : function notify() {
   1.170 +      switch(state) {
   1.171 +        case LOGIN_OBSERVER_STATE_BURST:
   1.172 +          // Wait while network stays idle for a short period
   1.173 +          state = LOGIN_OBSERVER_STATE_VERIFY_NEEDED;
   1.174 +          // Fall though to start polling timer
   1.175 +        case LOGIN_OBSERVER_STATE_IDLE:
   1.176 +          timer.initWithCallback(this,
   1.177 +                                 captivePortalDetector._pollingTime,
   1.178 +                                 timer.TYPE_ONE_SHOT);
   1.179 +          break;
   1.180 +        case LOGIN_OBSERVER_STATE_VERIFY_NEEDED:
   1.181 +          // Polling the canonical website since network stays idle for a while
   1.182 +          state = LOGIN_OBSERVER_STATE_VERIFYING;
   1.183 +          checkPageContent();
   1.184 +          break;
   1.185 +
   1.186 +        default:
   1.187 +          break;
   1.188 +      }
   1.189 +    },
   1.190 +  };
   1.191 +
   1.192 +  return observer;
   1.193 +}
   1.194 +
   1.195 +function CaptivePortalDetector() {
   1.196 +  // Load preference
   1.197 +  this._canonicalSiteURL = null;
   1.198 +  this._canonicalSiteExpectedContent = null;
   1.199 +
   1.200 +  try {
   1.201 +    this._canonicalSiteURL =
   1.202 +      Services.prefs.getCharPref('captivedetect.canonicalURL');
   1.203 +    this._canonicalSiteExpectedContent =
   1.204 +      Services.prefs.getCharPref('captivedetect.canonicalContent');
   1.205 +  } catch(e) {
   1.206 +    debug('canonicalURL or canonicalContent not set.')
   1.207 +  }
   1.208 +
   1.209 +  this._maxWaitingTime =
   1.210 +    Services.prefs.getIntPref('captivedetect.maxWaitingTime');
   1.211 +  this._pollingTime =
   1.212 +    Services.prefs.getIntPref('captivedetect.pollingTime');
   1.213 +  this._maxRetryCount =
   1.214 +    Services.prefs.getIntPref('captivedetect.maxRetryCount');
   1.215 +  debug('Load Prefs {site=' + this._canonicalSiteURL + ',content='
   1.216 +        + this._canonicalSiteExpectedContent + ',time=' + this._maxWaitingTime
   1.217 +        + "max-retry=" + this._maxRetryCount + '}');
   1.218 +
   1.219 +  // Create HttpObserver for monitoring the login procedure
   1.220 +  this._loginObserver = LoginObserver(this);
   1.221 +
   1.222 +  this._nextRequestId = 0;
   1.223 +  this._runningRequest = null;
   1.224 +  this._requestQueue = []; // Maintain a progress table, store callbacks and the ongoing XHR
   1.225 +  this._interfaceNames = {}; // Maintain names of the requested network interfaces
   1.226 +
   1.227 +  debug('CaptiveProtalDetector initiated, waitng for network connection established');
   1.228 +}
   1.229 +
   1.230 +CaptivePortalDetector.prototype = {
   1.231 +  classID:   kCAPTIVEPORTALDETECTOR_CID,
   1.232 +  classInfo: XPCOMUtils.generateCI({classID: kCAPTIVEPORTALDETECTOR_CID,
   1.233 +                                    contractID: kCAPTIVEPORTALDETECTOR_CONTRACTID,
   1.234 +                                    classDescription: 'Captive Portal Detector',
   1.235 +                                    interfaces: [Ci.nsICaptivePortalDetector]}),
   1.236 +  QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalDetector]),
   1.237 +
   1.238 +  // nsICaptivePortalDetector
   1.239 +  checkCaptivePortal: function checkCaptivePortal(aInterfaceName, aCallback) {
   1.240 +    if (!this._canonicalSiteURL) {
   1.241 +      throw Components.Exception('No canonical URL set up.');
   1.242 +    }
   1.243 +
   1.244 +    // Prevent multiple requests on a single network interface
   1.245 +    if (this._interfaceNames[aInterfaceName]) {
   1.246 +      throw Components.Exception('Do not allow multiple request on one interface: ' + aInterface);
   1.247 +    }
   1.248 +
   1.249 +    let request = {interfaceName: aInterfaceName};
   1.250 +    if (aCallback) {
   1.251 +      let callback = aCallback.QueryInterface(Ci.nsICaptivePortalCallback);
   1.252 +      request['callback'] = callback;
   1.253 +      request['retryCount'] = 0;
   1.254 +    }
   1.255 +    this._addRequest(request);
   1.256 +  },
   1.257 +
   1.258 +  abort: function abort(aInterfaceName) {
   1.259 +    debug('abort for ' + aInterfaceName);
   1.260 +    this._removeRequest(aInterfaceName);
   1.261 +  },
   1.262 +
   1.263 +  finishPreparation: function finishPreparation(aInterfaceName) {
   1.264 +    debug('finish preparation phase for interface "' + aInterfaceName + '"');
   1.265 +    if (!this._runningRequest
   1.266 +        || this._runningRequest.interfaceName !== aInterfaceName) {
   1.267 +      debug('invalid finishPreparation for ' + aInterfaceName);
   1.268 +      throw Components.Exception('only first request is allowed to invoke |finishPreparation|');
   1.269 +      return;
   1.270 +    }
   1.271 +
   1.272 +    this._startDetection();
   1.273 +  },
   1.274 +
   1.275 +  cancelLogin: function cancelLogin(eventId) {
   1.276 +    debug('login canceled by user for request "' + eventId + '"');
   1.277 +    // Captive portal login procedure is canceled by user
   1.278 +    if (this._runningRequest && this._runningRequest.hasOwnProperty('eventId')) {
   1.279 +      let id = this._runningRequest.eventId;
   1.280 +      if (eventId === id) {
   1.281 +        this.executeCallback(false);
   1.282 +      }
   1.283 +    }
   1.284 +  },
   1.285 +
   1.286 +  _applyDetection: function _applyDetection() {
   1.287 +    debug('enter applyDetection('+ this._runningRequest.interfaceName + ')');
   1.288 +
   1.289 +    // Execute network interface preparation
   1.290 +    if (this._runningRequest.hasOwnProperty('callback')) {
   1.291 +      this._runningRequest.callback.prepare();
   1.292 +    } else {
   1.293 +      this._startDetection();
   1.294 +    }
   1.295 +  },
   1.296 +
   1.297 +  _startDetection: function _startDetection() {
   1.298 +    debug('startDetection {site=' + this._canonicalSiteURL + ',content='
   1.299 +          + this._canonicalSiteExpectedContent + ',time=' + this._maxWaitingTime + '}');
   1.300 +    let self = this;
   1.301 +
   1.302 +    let urlFetcher = new URLFetcher(this._canonicalSiteURL, this._maxWaitingTime);
   1.303 +
   1.304 +    let mayRetry = this._mayRetry.bind(this);
   1.305 +
   1.306 +    urlFetcher.ontimeout = mayRetry;
   1.307 +    urlFetcher.onerror = mayRetry;
   1.308 +    urlFetcher.onsuccess = function (content) {
   1.309 +      if (self.validateContent(content)) {
   1.310 +        self.executeCallback(true);
   1.311 +      } else {
   1.312 +        // Content of the canonical website has been overwrite
   1.313 +        self._startLogin();
   1.314 +      }
   1.315 +    };
   1.316 +    urlFetcher.onredirectorerror = function (status) {
   1.317 +      if (status >= 300 && status <= 399) {
   1.318 +        // The canonical website has been redirected to an unknown location
   1.319 +        self._startLogin();
   1.320 +      } else {
   1.321 +        mayRetry();
   1.322 +      }
   1.323 +    };
   1.324 +
   1.325 +    this._runningRequest['urlFetcher'] = urlFetcher;
   1.326 +  },
   1.327 +
   1.328 +  _startLogin: function _startLogin() {
   1.329 +    let id = this._allocateRequestId();
   1.330 +    let details = {
   1.331 +      type: kOpenCaptivePortalLoginEvent,
   1.332 +      id: id,
   1.333 +      url: this._canonicalSiteURL,
   1.334 +    };
   1.335 +    this._loginObserver.attach();
   1.336 +    this._runningRequest['eventId'] = id;
   1.337 +    this._sendEvent(kOpenCaptivePortalLoginEvent, details);
   1.338 +  },
   1.339 +
   1.340 +  _mayRetry: function _mayRetry() {
   1.341 +    if (this._runningRequest.retryCount++ < this._maxRetryCount) {
   1.342 +      debug('retry-Detection: ' + this._runningRequest.retryCount + '/' + this._maxRetryCount);
   1.343 +      this._startDetection();
   1.344 +    } else {
   1.345 +      this.executeCallback(true);
   1.346 +    }
   1.347 +  },
   1.348 +
   1.349 +  executeCallback: function executeCallback(success) {
   1.350 +    if (this._runningRequest) {
   1.351 +      debug('callback executed');
   1.352 +      if (this._runningRequest.hasOwnProperty('callback')) {
   1.353 +        this._runningRequest.callback.complete(success);
   1.354 +      }
   1.355 +
   1.356 +      // Continue the following request
   1.357 +      this._runningRequest['complete'] = true;
   1.358 +      this._removeRequest(this._runningRequest.interfaceName);
   1.359 +    }
   1.360 +  },
   1.361 +
   1.362 +  _sendEvent: function _sendEvent(topic, details) {
   1.363 +    debug('sendEvent "' + JSON.stringify(details) + '"');
   1.364 +    Services.obs.notifyObservers(this,
   1.365 +                                 topic,
   1.366 +                                 JSON.stringify(details));
   1.367 +  },
   1.368 +
   1.369 +  validateContent: function validateContent(content) {
   1.370 +    debug('received content: ' + content);
   1.371 +    return (content === this._canonicalSiteExpectedContent);
   1.372 +  },
   1.373 +
   1.374 +  _allocateRequestId: function _allocateRequestId() {
   1.375 +    let newId = this._nextRequestId++;
   1.376 +    return newId.toString();
   1.377 +  },
   1.378 +
   1.379 +  _runNextRequest: function _runNextRequest() {
   1.380 +    let nextRequest = this._requestQueue.shift();
   1.381 +    if (nextRequest) {
   1.382 +      this._runningRequest = nextRequest;
   1.383 +      this._applyDetection();
   1.384 +    }
   1.385 +  },
   1.386 +
   1.387 +  _addRequest: function _addRequest(request) {
   1.388 +    this._interfaceNames[request.interfaceName] = true;
   1.389 +    this._requestQueue.push(request);
   1.390 +    if (!this._runningRequest) {
   1.391 +      this._runNextRequest();
   1.392 +    }
   1.393 +  },
   1.394 +
   1.395 +  _removeRequest: function _removeRequest(aInterfaceName) {
   1.396 +    if (!this._interfaceNames[aInterfaceName]) {
   1.397 +      return;
   1.398 +    }
   1.399 +
   1.400 +    delete this._interfaceNames[aInterfaceName];
   1.401 +
   1.402 +    if (this._runningRequest
   1.403 +        && this._runningRequest.interfaceName === aInterfaceName) {
   1.404 +      this._loginObserver.detach();
   1.405 +
   1.406 +      if (!this._runningRequest.complete) {
   1.407 +        // Abort the user login procedure
   1.408 +        if (this._runningRequest.hasOwnProperty('eventId')) {
   1.409 +          let details = {
   1.410 +            type: kAbortCaptivePortalLoginEvent,
   1.411 +            id: this._runningRequest.eventId
   1.412 +          };
   1.413 +          this._sendEvent(kAbortCaptivePortalLoginEvent, details);
   1.414 +        }
   1.415 +
   1.416 +        // Abort the ongoing HTTP request
   1.417 +        if (this._runningRequest.hasOwnProperty('urlFetcher')) {
   1.418 +          this._runningRequest.urlFetcher.abort();
   1.419 +        }
   1.420 +      }
   1.421 +
   1.422 +      debug('remove running request');
   1.423 +      this._runningRequest = null;
   1.424 +
   1.425 +      // Continue next pending reqeust if the ongoing one has been aborted
   1.426 +      this._runNextRequest();
   1.427 +      return;
   1.428 +    }
   1.429 +
   1.430 +    // Check if a pending request has been aborted
   1.431 +    for (let i = 0; i < this._requestQueue.length; i++) {
   1.432 +      if (this._requestQueue[i].interfaceName == aInterfaceName) {
   1.433 +        this._requestQueue.splice(i, 1);
   1.434 +
   1.435 +        debug('remove pending request #' + i + ', remaining ' + this._requestQueue.length);
   1.436 +        break;
   1.437 +      }
   1.438 +    }
   1.439 +  },
   1.440 +};
   1.441 +
   1.442 +let debug;
   1.443 +if (DEBUG) {
   1.444 +  debug = function (s) {
   1.445 +    dump('-*- CaptivePortalDetector component: ' + s + '\n');
   1.446 +  };
   1.447 +} else {
   1.448 +  debug = function (s) {};
   1.449 +}
   1.450 +
   1.451 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([CaptivePortalDetector]);

mercurial