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.

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

mercurial