toolkit/components/captivedetect/captivedetect.js

Fri, 16 Jan 2015 18:13:44 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Fri, 16 Jan 2015 18:13:44 +0100
branch
TOR_BUG_9701
changeset 14
925c144e1f1f
permissions
-rw-r--r--

Integrate suggestion from review to improve consistency with existing code.

michael@0 1 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
michael@0 2 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 4 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 5
michael@0 6 'use strict';
michael@0 7
michael@0 8 const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
michael@0 9
michael@0 10 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
michael@0 11 Cu.import('resource://gre/modules/Services.jsm');
michael@0 12
michael@0 13 const DEBUG = false; // set to true to show debug messages
michael@0 14
michael@0 15 const kCAPTIVEPORTALDETECTOR_CONTRACTID = '@mozilla.org/toolkit/captive-detector;1';
michael@0 16 const kCAPTIVEPORTALDETECTOR_CID = Components.ID('{d9cd00ba-aa4d-47b1-8792-b1fe0cd35060}');
michael@0 17
michael@0 18 const kOpenCaptivePortalLoginEvent = 'captive-portal-login';
michael@0 19 const kAbortCaptivePortalLoginEvent = 'captive-portal-login-abort';
michael@0 20
michael@0 21 function URLFetcher(url, timeout) {
michael@0 22 let self = this;
michael@0 23 let xhr = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
michael@0 24 .createInstance(Ci.nsIXMLHttpRequest);
michael@0 25 xhr.open('GET', url, true);
michael@0 26 // Prevent the request from reading from the cache.
michael@0 27 xhr.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
michael@0 28 // Prevent the request from writing to the cache.
michael@0 29 xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
michael@0 30 // Prevent privacy leaks
michael@0 31 xhr.channel.loadFlags |= Ci.nsIRequest.LOAD_ANONYMOUS;
michael@0 32 // The Cache-Control header is only interpreted by proxies and the
michael@0 33 // final destination. It does not help if a resource is already
michael@0 34 // cached locally.
michael@0 35 xhr.setRequestHeader("Cache-Control", "no-cache");
michael@0 36 // HTTP/1.0 servers might not implement Cache-Control and
michael@0 37 // might only implement Pragma: no-cache
michael@0 38 xhr.setRequestHeader("Pragma", "no-cache");
michael@0 39
michael@0 40 xhr.timeout = timeout;
michael@0 41 xhr.ontimeout = function () { self.ontimeout(); };
michael@0 42 xhr.onerror = function () { self.onerror(); };
michael@0 43 xhr.onreadystatechange = function(oEvent) {
michael@0 44 if (xhr.readyState === 4) {
michael@0 45 if (self._isAborted) {
michael@0 46 return;
michael@0 47 }
michael@0 48 if (xhr.status === 200) {
michael@0 49 self.onsuccess(xhr.responseText);
michael@0 50 } else if (xhr.status) {
michael@0 51 self.onredirectorerror(xhr.status);
michael@0 52 }
michael@0 53 }
michael@0 54 };
michael@0 55 xhr.send();
michael@0 56 this._xhr = xhr;
michael@0 57 }
michael@0 58
michael@0 59 URLFetcher.prototype = {
michael@0 60 _isAborted: false,
michael@0 61 ontimeout: function() {},
michael@0 62 onerror: function() {},
michael@0 63 abort: function() {
michael@0 64 if (!this._isAborted) {
michael@0 65 this._isAborted = true;
michael@0 66 this._xhr.abort();
michael@0 67 }
michael@0 68 },
michael@0 69 }
michael@0 70
michael@0 71 function LoginObserver(captivePortalDetector) {
michael@0 72 const LOGIN_OBSERVER_STATE_DETACHED = 0; /* Should not monitor network activity since no ongoing login procedure */
michael@0 73 const LOGIN_OBSERVER_STATE_IDLE = 1; /* No network activity currently, waiting for a longer enough idle period */
michael@0 74 const LOGIN_OBSERVER_STATE_BURST = 2; /* Network activity is detected, probably caused by a login procedure */
michael@0 75 const LOGIN_OBSERVER_STATE_VERIFY_NEEDED = 3; /* Verifing network accessiblity is required after a long enough idle */
michael@0 76 const LOGIN_OBSERVER_STATE_VERIFYING = 4; /* LoginObserver is probing if public network is available */
michael@0 77
michael@0 78 let state = LOGIN_OBSERVER_STATE_DETACHED;
michael@0 79
michael@0 80 let timer = Cc['@mozilla.org/timer;1'].createInstance(Ci.nsITimer);
michael@0 81 let activityDistributor = Cc['@mozilla.org/network/http-activity-distributor;1']
michael@0 82 .getService(Ci.nsIHttpActivityDistributor);
michael@0 83 let urlFetcher = null;
michael@0 84
michael@0 85 let pageCheckingDone = function pageCheckingDone() {
michael@0 86 if (state === LOGIN_OBSERVER_STATE_VERIFYING) {
michael@0 87 urlFetcher = null;
michael@0 88 // Finish polling the canonical site, switch back to idle state and
michael@0 89 // waiting for next burst
michael@0 90 state = LOGIN_OBSERVER_STATE_IDLE;
michael@0 91 timer.initWithCallback(observer,
michael@0 92 captivePortalDetector._pollingTime,
michael@0 93 timer.TYPE_ONE_SHOT);
michael@0 94 }
michael@0 95 };
michael@0 96
michael@0 97 let checkPageContent = function checkPageContent() {
michael@0 98 debug("checking if public network is available after the login procedure");
michael@0 99
michael@0 100 urlFetcher = new URLFetcher(captivePortalDetector._canonicalSiteURL,
michael@0 101 captivePortalDetector._maxWaitingTime);
michael@0 102 urlFetcher.ontimeout = pageCheckingDone;
michael@0 103 urlFetcher.onerror = pageCheckingDone;
michael@0 104 urlFetcher.onsuccess = function (content) {
michael@0 105 if (captivePortalDetector.validateContent(content)) {
michael@0 106 urlFetcher = null;
michael@0 107 captivePortalDetector.executeCallback(true);
michael@0 108 } else {
michael@0 109 pageCheckingDone();
michael@0 110 }
michael@0 111 };
michael@0 112 urlFetcher.onredirectorerror = pageCheckingDone;
michael@0 113 };
michael@0 114
michael@0 115 // Public interface of LoginObserver
michael@0 116 let observer = {
michael@0 117 QueryInterface: XPCOMUtils.generateQI([Ci.nsIHttpActivityOberver,
michael@0 118 Ci.nsITimerCallback]),
michael@0 119
michael@0 120 attach: function attach() {
michael@0 121 if (state === LOGIN_OBSERVER_STATE_DETACHED) {
michael@0 122 activityDistributor.addObserver(this);
michael@0 123 state = LOGIN_OBSERVER_STATE_IDLE;
michael@0 124 timer.initWithCallback(this,
michael@0 125 captivePortalDetector._pollingTime,
michael@0 126 timer.TYPE_ONE_SHOT);
michael@0 127 debug('attach HttpObserver for login activity');
michael@0 128 }
michael@0 129 },
michael@0 130
michael@0 131 detach: function detach() {
michael@0 132 if (state !== LOGIN_OBSERVER_STATE_DETACHED) {
michael@0 133 if (urlFetcher) {
michael@0 134 urlFetcher.abort();
michael@0 135 urlFetcher = null;
michael@0 136 }
michael@0 137 activityDistributor.removeObserver(this);
michael@0 138 timer.cancel();
michael@0 139 state = LOGIN_OBSERVER_STATE_DETACHED;
michael@0 140 debug('detach HttpObserver for login activity');
michael@0 141 }
michael@0 142 },
michael@0 143
michael@0 144 /*
michael@0 145 * Treat all HTTP transactions as captive portal login activities.
michael@0 146 */
michael@0 147 observeActivity: function observeActivity(aHttpChannel, aActivityType,
michael@0 148 aActivitySubtype, aTimestamp,
michael@0 149 aExtraSizeData, aExtraStringData) {
michael@0 150 if (aActivityType === Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION
michael@0 151 && aActivitySubtype === Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE) {
michael@0 152 switch (state) {
michael@0 153 case LOGIN_OBSERVER_STATE_IDLE:
michael@0 154 case LOGIN_OBSERVER_STATE_VERIFY_NEEDED:
michael@0 155 state = LOGIN_OBSERVER_STATE_BURST;
michael@0 156 break;
michael@0 157 default:
michael@0 158 break;
michael@0 159 }
michael@0 160 }
michael@0 161 },
michael@0 162
michael@0 163 /*
michael@0 164 * Check if login activity is finished according to HTTP burst.
michael@0 165 */
michael@0 166 notify : function notify() {
michael@0 167 switch(state) {
michael@0 168 case LOGIN_OBSERVER_STATE_BURST:
michael@0 169 // Wait while network stays idle for a short period
michael@0 170 state = LOGIN_OBSERVER_STATE_VERIFY_NEEDED;
michael@0 171 // Fall though to start polling timer
michael@0 172 case LOGIN_OBSERVER_STATE_IDLE:
michael@0 173 timer.initWithCallback(this,
michael@0 174 captivePortalDetector._pollingTime,
michael@0 175 timer.TYPE_ONE_SHOT);
michael@0 176 break;
michael@0 177 case LOGIN_OBSERVER_STATE_VERIFY_NEEDED:
michael@0 178 // Polling the canonical website since network stays idle for a while
michael@0 179 state = LOGIN_OBSERVER_STATE_VERIFYING;
michael@0 180 checkPageContent();
michael@0 181 break;
michael@0 182
michael@0 183 default:
michael@0 184 break;
michael@0 185 }
michael@0 186 },
michael@0 187 };
michael@0 188
michael@0 189 return observer;
michael@0 190 }
michael@0 191
michael@0 192 function CaptivePortalDetector() {
michael@0 193 // Load preference
michael@0 194 this._canonicalSiteURL = null;
michael@0 195 this._canonicalSiteExpectedContent = null;
michael@0 196
michael@0 197 try {
michael@0 198 this._canonicalSiteURL =
michael@0 199 Services.prefs.getCharPref('captivedetect.canonicalURL');
michael@0 200 this._canonicalSiteExpectedContent =
michael@0 201 Services.prefs.getCharPref('captivedetect.canonicalContent');
michael@0 202 } catch(e) {
michael@0 203 debug('canonicalURL or canonicalContent not set.')
michael@0 204 }
michael@0 205
michael@0 206 this._maxWaitingTime =
michael@0 207 Services.prefs.getIntPref('captivedetect.maxWaitingTime');
michael@0 208 this._pollingTime =
michael@0 209 Services.prefs.getIntPref('captivedetect.pollingTime');
michael@0 210 this._maxRetryCount =
michael@0 211 Services.prefs.getIntPref('captivedetect.maxRetryCount');
michael@0 212 debug('Load Prefs {site=' + this._canonicalSiteURL + ',content='
michael@0 213 + this._canonicalSiteExpectedContent + ',time=' + this._maxWaitingTime
michael@0 214 + "max-retry=" + this._maxRetryCount + '}');
michael@0 215
michael@0 216 // Create HttpObserver for monitoring the login procedure
michael@0 217 this._loginObserver = LoginObserver(this);
michael@0 218
michael@0 219 this._nextRequestId = 0;
michael@0 220 this._runningRequest = null;
michael@0 221 this._requestQueue = []; // Maintain a progress table, store callbacks and the ongoing XHR
michael@0 222 this._interfaceNames = {}; // Maintain names of the requested network interfaces
michael@0 223
michael@0 224 debug('CaptiveProtalDetector initiated, waitng for network connection established');
michael@0 225 }
michael@0 226
michael@0 227 CaptivePortalDetector.prototype = {
michael@0 228 classID: kCAPTIVEPORTALDETECTOR_CID,
michael@0 229 classInfo: XPCOMUtils.generateCI({classID: kCAPTIVEPORTALDETECTOR_CID,
michael@0 230 contractID: kCAPTIVEPORTALDETECTOR_CONTRACTID,
michael@0 231 classDescription: 'Captive Portal Detector',
michael@0 232 interfaces: [Ci.nsICaptivePortalDetector]}),
michael@0 233 QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalDetector]),
michael@0 234
michael@0 235 // nsICaptivePortalDetector
michael@0 236 checkCaptivePortal: function checkCaptivePortal(aInterfaceName, aCallback) {
michael@0 237 if (!this._canonicalSiteURL) {
michael@0 238 throw Components.Exception('No canonical URL set up.');
michael@0 239 }
michael@0 240
michael@0 241 // Prevent multiple requests on a single network interface
michael@0 242 if (this._interfaceNames[aInterfaceName]) {
michael@0 243 throw Components.Exception('Do not allow multiple request on one interface: ' + aInterface);
michael@0 244 }
michael@0 245
michael@0 246 let request = {interfaceName: aInterfaceName};
michael@0 247 if (aCallback) {
michael@0 248 let callback = aCallback.QueryInterface(Ci.nsICaptivePortalCallback);
michael@0 249 request['callback'] = callback;
michael@0 250 request['retryCount'] = 0;
michael@0 251 }
michael@0 252 this._addRequest(request);
michael@0 253 },
michael@0 254
michael@0 255 abort: function abort(aInterfaceName) {
michael@0 256 debug('abort for ' + aInterfaceName);
michael@0 257 this._removeRequest(aInterfaceName);
michael@0 258 },
michael@0 259
michael@0 260 finishPreparation: function finishPreparation(aInterfaceName) {
michael@0 261 debug('finish preparation phase for interface "' + aInterfaceName + '"');
michael@0 262 if (!this._runningRequest
michael@0 263 || this._runningRequest.interfaceName !== aInterfaceName) {
michael@0 264 debug('invalid finishPreparation for ' + aInterfaceName);
michael@0 265 throw Components.Exception('only first request is allowed to invoke |finishPreparation|');
michael@0 266 return;
michael@0 267 }
michael@0 268
michael@0 269 this._startDetection();
michael@0 270 },
michael@0 271
michael@0 272 cancelLogin: function cancelLogin(eventId) {
michael@0 273 debug('login canceled by user for request "' + eventId + '"');
michael@0 274 // Captive portal login procedure is canceled by user
michael@0 275 if (this._runningRequest && this._runningRequest.hasOwnProperty('eventId')) {
michael@0 276 let id = this._runningRequest.eventId;
michael@0 277 if (eventId === id) {
michael@0 278 this.executeCallback(false);
michael@0 279 }
michael@0 280 }
michael@0 281 },
michael@0 282
michael@0 283 _applyDetection: function _applyDetection() {
michael@0 284 debug('enter applyDetection('+ this._runningRequest.interfaceName + ')');
michael@0 285
michael@0 286 // Execute network interface preparation
michael@0 287 if (this._runningRequest.hasOwnProperty('callback')) {
michael@0 288 this._runningRequest.callback.prepare();
michael@0 289 } else {
michael@0 290 this._startDetection();
michael@0 291 }
michael@0 292 },
michael@0 293
michael@0 294 _startDetection: function _startDetection() {
michael@0 295 debug('startDetection {site=' + this._canonicalSiteURL + ',content='
michael@0 296 + this._canonicalSiteExpectedContent + ',time=' + this._maxWaitingTime + '}');
michael@0 297 let self = this;
michael@0 298
michael@0 299 let urlFetcher = new URLFetcher(this._canonicalSiteURL, this._maxWaitingTime);
michael@0 300
michael@0 301 let mayRetry = this._mayRetry.bind(this);
michael@0 302
michael@0 303 urlFetcher.ontimeout = mayRetry;
michael@0 304 urlFetcher.onerror = mayRetry;
michael@0 305 urlFetcher.onsuccess = function (content) {
michael@0 306 if (self.validateContent(content)) {
michael@0 307 self.executeCallback(true);
michael@0 308 } else {
michael@0 309 // Content of the canonical website has been overwrite
michael@0 310 self._startLogin();
michael@0 311 }
michael@0 312 };
michael@0 313 urlFetcher.onredirectorerror = function (status) {
michael@0 314 if (status >= 300 && status <= 399) {
michael@0 315 // The canonical website has been redirected to an unknown location
michael@0 316 self._startLogin();
michael@0 317 } else {
michael@0 318 mayRetry();
michael@0 319 }
michael@0 320 };
michael@0 321
michael@0 322 this._runningRequest['urlFetcher'] = urlFetcher;
michael@0 323 },
michael@0 324
michael@0 325 _startLogin: function _startLogin() {
michael@0 326 let id = this._allocateRequestId();
michael@0 327 let details = {
michael@0 328 type: kOpenCaptivePortalLoginEvent,
michael@0 329 id: id,
michael@0 330 url: this._canonicalSiteURL,
michael@0 331 };
michael@0 332 this._loginObserver.attach();
michael@0 333 this._runningRequest['eventId'] = id;
michael@0 334 this._sendEvent(kOpenCaptivePortalLoginEvent, details);
michael@0 335 },
michael@0 336
michael@0 337 _mayRetry: function _mayRetry() {
michael@0 338 if (this._runningRequest.retryCount++ < this._maxRetryCount) {
michael@0 339 debug('retry-Detection: ' + this._runningRequest.retryCount + '/' + this._maxRetryCount);
michael@0 340 this._startDetection();
michael@0 341 } else {
michael@0 342 this.executeCallback(true);
michael@0 343 }
michael@0 344 },
michael@0 345
michael@0 346 executeCallback: function executeCallback(success) {
michael@0 347 if (this._runningRequest) {
michael@0 348 debug('callback executed');
michael@0 349 if (this._runningRequest.hasOwnProperty('callback')) {
michael@0 350 this._runningRequest.callback.complete(success);
michael@0 351 }
michael@0 352
michael@0 353 // Continue the following request
michael@0 354 this._runningRequest['complete'] = true;
michael@0 355 this._removeRequest(this._runningRequest.interfaceName);
michael@0 356 }
michael@0 357 },
michael@0 358
michael@0 359 _sendEvent: function _sendEvent(topic, details) {
michael@0 360 debug('sendEvent "' + JSON.stringify(details) + '"');
michael@0 361 Services.obs.notifyObservers(this,
michael@0 362 topic,
michael@0 363 JSON.stringify(details));
michael@0 364 },
michael@0 365
michael@0 366 validateContent: function validateContent(content) {
michael@0 367 debug('received content: ' + content);
michael@0 368 return (content === this._canonicalSiteExpectedContent);
michael@0 369 },
michael@0 370
michael@0 371 _allocateRequestId: function _allocateRequestId() {
michael@0 372 let newId = this._nextRequestId++;
michael@0 373 return newId.toString();
michael@0 374 },
michael@0 375
michael@0 376 _runNextRequest: function _runNextRequest() {
michael@0 377 let nextRequest = this._requestQueue.shift();
michael@0 378 if (nextRequest) {
michael@0 379 this._runningRequest = nextRequest;
michael@0 380 this._applyDetection();
michael@0 381 }
michael@0 382 },
michael@0 383
michael@0 384 _addRequest: function _addRequest(request) {
michael@0 385 this._interfaceNames[request.interfaceName] = true;
michael@0 386 this._requestQueue.push(request);
michael@0 387 if (!this._runningRequest) {
michael@0 388 this._runNextRequest();
michael@0 389 }
michael@0 390 },
michael@0 391
michael@0 392 _removeRequest: function _removeRequest(aInterfaceName) {
michael@0 393 if (!this._interfaceNames[aInterfaceName]) {
michael@0 394 return;
michael@0 395 }
michael@0 396
michael@0 397 delete this._interfaceNames[aInterfaceName];
michael@0 398
michael@0 399 if (this._runningRequest
michael@0 400 && this._runningRequest.interfaceName === aInterfaceName) {
michael@0 401 this._loginObserver.detach();
michael@0 402
michael@0 403 if (!this._runningRequest.complete) {
michael@0 404 // Abort the user login procedure
michael@0 405 if (this._runningRequest.hasOwnProperty('eventId')) {
michael@0 406 let details = {
michael@0 407 type: kAbortCaptivePortalLoginEvent,
michael@0 408 id: this._runningRequest.eventId
michael@0 409 };
michael@0 410 this._sendEvent(kAbortCaptivePortalLoginEvent, details);
michael@0 411 }
michael@0 412
michael@0 413 // Abort the ongoing HTTP request
michael@0 414 if (this._runningRequest.hasOwnProperty('urlFetcher')) {
michael@0 415 this._runningRequest.urlFetcher.abort();
michael@0 416 }
michael@0 417 }
michael@0 418
michael@0 419 debug('remove running request');
michael@0 420 this._runningRequest = null;
michael@0 421
michael@0 422 // Continue next pending reqeust if the ongoing one has been aborted
michael@0 423 this._runNextRequest();
michael@0 424 return;
michael@0 425 }
michael@0 426
michael@0 427 // Check if a pending request has been aborted
michael@0 428 for (let i = 0; i < this._requestQueue.length; i++) {
michael@0 429 if (this._requestQueue[i].interfaceName == aInterfaceName) {
michael@0 430 this._requestQueue.splice(i, 1);
michael@0 431
michael@0 432 debug('remove pending request #' + i + ', remaining ' + this._requestQueue.length);
michael@0 433 break;
michael@0 434 }
michael@0 435 }
michael@0 436 },
michael@0 437 };
michael@0 438
michael@0 439 let debug;
michael@0 440 if (DEBUG) {
michael@0 441 debug = function (s) {
michael@0 442 dump('-*- CaptivePortalDetector component: ' + s + '\n');
michael@0 443 };
michael@0 444 } else {
michael@0 445 debug = function (s) {};
michael@0 446 }
michael@0 447
michael@0 448 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([CaptivePortalDetector]);

mercurial