michael@0: /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: 'use strict'; michael@0: michael@0: const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; michael@0: michael@0: Cu.import('resource://gre/modules/XPCOMUtils.jsm'); michael@0: Cu.import('resource://gre/modules/Services.jsm'); michael@0: michael@0: const DEBUG = false; // set to true to show debug messages michael@0: michael@0: const kCAPTIVEPORTALDETECTOR_CONTRACTID = '@mozilla.org/toolkit/captive-detector;1'; michael@0: const kCAPTIVEPORTALDETECTOR_CID = Components.ID('{d9cd00ba-aa4d-47b1-8792-b1fe0cd35060}'); michael@0: michael@0: const kOpenCaptivePortalLoginEvent = 'captive-portal-login'; michael@0: const kAbortCaptivePortalLoginEvent = 'captive-portal-login-abort'; michael@0: michael@0: function URLFetcher(url, timeout) { michael@0: let self = this; michael@0: let xhr = Cc['@mozilla.org/xmlextras/xmlhttprequest;1'] michael@0: .createInstance(Ci.nsIXMLHttpRequest); michael@0: xhr.open('GET', url, true); michael@0: // Prevent the request from reading from the cache. michael@0: xhr.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; michael@0: // Prevent the request from writing to the cache. michael@0: xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; michael@0: // Prevent privacy leaks michael@0: xhr.channel.loadFlags |= Ci.nsIRequest.LOAD_ANONYMOUS; michael@0: // The Cache-Control header is only interpreted by proxies and the michael@0: // final destination. It does not help if a resource is already michael@0: // cached locally. michael@0: xhr.setRequestHeader("Cache-Control", "no-cache"); michael@0: // HTTP/1.0 servers might not implement Cache-Control and michael@0: // might only implement Pragma: no-cache michael@0: xhr.setRequestHeader("Pragma", "no-cache"); michael@0: michael@0: xhr.timeout = timeout; michael@0: xhr.ontimeout = function () { self.ontimeout(); }; michael@0: xhr.onerror = function () { self.onerror(); }; michael@0: xhr.onreadystatechange = function(oEvent) { michael@0: if (xhr.readyState === 4) { michael@0: if (self._isAborted) { michael@0: return; michael@0: } michael@0: if (xhr.status === 200) { michael@0: self.onsuccess(xhr.responseText); michael@0: } else if (xhr.status) { michael@0: self.onredirectorerror(xhr.status); michael@0: } michael@0: } michael@0: }; michael@0: xhr.send(); michael@0: this._xhr = xhr; michael@0: } michael@0: michael@0: URLFetcher.prototype = { michael@0: _isAborted: false, michael@0: ontimeout: function() {}, michael@0: onerror: function() {}, michael@0: abort: function() { michael@0: if (!this._isAborted) { michael@0: this._isAborted = true; michael@0: this._xhr.abort(); michael@0: } michael@0: }, michael@0: } michael@0: michael@0: function LoginObserver(captivePortalDetector) { michael@0: const LOGIN_OBSERVER_STATE_DETACHED = 0; /* Should not monitor network activity since no ongoing login procedure */ michael@0: const LOGIN_OBSERVER_STATE_IDLE = 1; /* No network activity currently, waiting for a longer enough idle period */ michael@0: const LOGIN_OBSERVER_STATE_BURST = 2; /* Network activity is detected, probably caused by a login procedure */ michael@0: const LOGIN_OBSERVER_STATE_VERIFY_NEEDED = 3; /* Verifing network accessiblity is required after a long enough idle */ michael@0: const LOGIN_OBSERVER_STATE_VERIFYING = 4; /* LoginObserver is probing if public network is available */ michael@0: michael@0: let state = LOGIN_OBSERVER_STATE_DETACHED; michael@0: michael@0: let timer = Cc['@mozilla.org/timer;1'].createInstance(Ci.nsITimer); michael@0: let activityDistributor = Cc['@mozilla.org/network/http-activity-distributor;1'] michael@0: .getService(Ci.nsIHttpActivityDistributor); michael@0: let urlFetcher = null; michael@0: michael@0: let pageCheckingDone = function pageCheckingDone() { michael@0: if (state === LOGIN_OBSERVER_STATE_VERIFYING) { michael@0: urlFetcher = null; michael@0: // Finish polling the canonical site, switch back to idle state and michael@0: // waiting for next burst michael@0: state = LOGIN_OBSERVER_STATE_IDLE; michael@0: timer.initWithCallback(observer, michael@0: captivePortalDetector._pollingTime, michael@0: timer.TYPE_ONE_SHOT); michael@0: } michael@0: }; michael@0: michael@0: let checkPageContent = function checkPageContent() { michael@0: debug("checking if public network is available after the login procedure"); michael@0: michael@0: urlFetcher = new URLFetcher(captivePortalDetector._canonicalSiteURL, michael@0: captivePortalDetector._maxWaitingTime); michael@0: urlFetcher.ontimeout = pageCheckingDone; michael@0: urlFetcher.onerror = pageCheckingDone; michael@0: urlFetcher.onsuccess = function (content) { michael@0: if (captivePortalDetector.validateContent(content)) { michael@0: urlFetcher = null; michael@0: captivePortalDetector.executeCallback(true); michael@0: } else { michael@0: pageCheckingDone(); michael@0: } michael@0: }; michael@0: urlFetcher.onredirectorerror = pageCheckingDone; michael@0: }; michael@0: michael@0: // Public interface of LoginObserver michael@0: let observer = { michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIHttpActivityOberver, michael@0: Ci.nsITimerCallback]), michael@0: michael@0: attach: function attach() { michael@0: if (state === LOGIN_OBSERVER_STATE_DETACHED) { michael@0: activityDistributor.addObserver(this); michael@0: state = LOGIN_OBSERVER_STATE_IDLE; michael@0: timer.initWithCallback(this, michael@0: captivePortalDetector._pollingTime, michael@0: timer.TYPE_ONE_SHOT); michael@0: debug('attach HttpObserver for login activity'); michael@0: } michael@0: }, michael@0: michael@0: detach: function detach() { michael@0: if (state !== LOGIN_OBSERVER_STATE_DETACHED) { michael@0: if (urlFetcher) { michael@0: urlFetcher.abort(); michael@0: urlFetcher = null; michael@0: } michael@0: activityDistributor.removeObserver(this); michael@0: timer.cancel(); michael@0: state = LOGIN_OBSERVER_STATE_DETACHED; michael@0: debug('detach HttpObserver for login activity'); michael@0: } michael@0: }, michael@0: michael@0: /* michael@0: * Treat all HTTP transactions as captive portal login activities. michael@0: */ michael@0: observeActivity: function observeActivity(aHttpChannel, aActivityType, michael@0: aActivitySubtype, aTimestamp, michael@0: aExtraSizeData, aExtraStringData) { michael@0: if (aActivityType === Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION michael@0: && aActivitySubtype === Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE) { michael@0: switch (state) { michael@0: case LOGIN_OBSERVER_STATE_IDLE: michael@0: case LOGIN_OBSERVER_STATE_VERIFY_NEEDED: michael@0: state = LOGIN_OBSERVER_STATE_BURST; michael@0: break; michael@0: default: michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /* michael@0: * Check if login activity is finished according to HTTP burst. michael@0: */ michael@0: notify : function notify() { michael@0: switch(state) { michael@0: case LOGIN_OBSERVER_STATE_BURST: michael@0: // Wait while network stays idle for a short period michael@0: state = LOGIN_OBSERVER_STATE_VERIFY_NEEDED; michael@0: // Fall though to start polling timer michael@0: case LOGIN_OBSERVER_STATE_IDLE: michael@0: timer.initWithCallback(this, michael@0: captivePortalDetector._pollingTime, michael@0: timer.TYPE_ONE_SHOT); michael@0: break; michael@0: case LOGIN_OBSERVER_STATE_VERIFY_NEEDED: michael@0: // Polling the canonical website since network stays idle for a while michael@0: state = LOGIN_OBSERVER_STATE_VERIFYING; michael@0: checkPageContent(); michael@0: break; michael@0: michael@0: default: michael@0: break; michael@0: } michael@0: }, michael@0: }; michael@0: michael@0: return observer; michael@0: } michael@0: michael@0: function CaptivePortalDetector() { michael@0: // Load preference michael@0: this._canonicalSiteURL = null; michael@0: this._canonicalSiteExpectedContent = null; michael@0: michael@0: try { michael@0: this._canonicalSiteURL = michael@0: Services.prefs.getCharPref('captivedetect.canonicalURL'); michael@0: this._canonicalSiteExpectedContent = michael@0: Services.prefs.getCharPref('captivedetect.canonicalContent'); michael@0: } catch(e) { michael@0: debug('canonicalURL or canonicalContent not set.') michael@0: } michael@0: michael@0: this._maxWaitingTime = michael@0: Services.prefs.getIntPref('captivedetect.maxWaitingTime'); michael@0: this._pollingTime = michael@0: Services.prefs.getIntPref('captivedetect.pollingTime'); michael@0: this._maxRetryCount = michael@0: Services.prefs.getIntPref('captivedetect.maxRetryCount'); michael@0: debug('Load Prefs {site=' + this._canonicalSiteURL + ',content=' michael@0: + this._canonicalSiteExpectedContent + ',time=' + this._maxWaitingTime michael@0: + "max-retry=" + this._maxRetryCount + '}'); michael@0: michael@0: // Create HttpObserver for monitoring the login procedure michael@0: this._loginObserver = LoginObserver(this); michael@0: michael@0: this._nextRequestId = 0; michael@0: this._runningRequest = null; michael@0: this._requestQueue = []; // Maintain a progress table, store callbacks and the ongoing XHR michael@0: this._interfaceNames = {}; // Maintain names of the requested network interfaces michael@0: michael@0: debug('CaptiveProtalDetector initiated, waitng for network connection established'); michael@0: } michael@0: michael@0: CaptivePortalDetector.prototype = { michael@0: classID: kCAPTIVEPORTALDETECTOR_CID, michael@0: classInfo: XPCOMUtils.generateCI({classID: kCAPTIVEPORTALDETECTOR_CID, michael@0: contractID: kCAPTIVEPORTALDETECTOR_CONTRACTID, michael@0: classDescription: 'Captive Portal Detector', michael@0: interfaces: [Ci.nsICaptivePortalDetector]}), michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalDetector]), michael@0: michael@0: // nsICaptivePortalDetector michael@0: checkCaptivePortal: function checkCaptivePortal(aInterfaceName, aCallback) { michael@0: if (!this._canonicalSiteURL) { michael@0: throw Components.Exception('No canonical URL set up.'); michael@0: } michael@0: michael@0: // Prevent multiple requests on a single network interface michael@0: if (this._interfaceNames[aInterfaceName]) { michael@0: throw Components.Exception('Do not allow multiple request on one interface: ' + aInterface); michael@0: } michael@0: michael@0: let request = {interfaceName: aInterfaceName}; michael@0: if (aCallback) { michael@0: let callback = aCallback.QueryInterface(Ci.nsICaptivePortalCallback); michael@0: request['callback'] = callback; michael@0: request['retryCount'] = 0; michael@0: } michael@0: this._addRequest(request); michael@0: }, michael@0: michael@0: abort: function abort(aInterfaceName) { michael@0: debug('abort for ' + aInterfaceName); michael@0: this._removeRequest(aInterfaceName); michael@0: }, michael@0: michael@0: finishPreparation: function finishPreparation(aInterfaceName) { michael@0: debug('finish preparation phase for interface "' + aInterfaceName + '"'); michael@0: if (!this._runningRequest michael@0: || this._runningRequest.interfaceName !== aInterfaceName) { michael@0: debug('invalid finishPreparation for ' + aInterfaceName); michael@0: throw Components.Exception('only first request is allowed to invoke |finishPreparation|'); michael@0: return; michael@0: } michael@0: michael@0: this._startDetection(); michael@0: }, michael@0: michael@0: cancelLogin: function cancelLogin(eventId) { michael@0: debug('login canceled by user for request "' + eventId + '"'); michael@0: // Captive portal login procedure is canceled by user michael@0: if (this._runningRequest && this._runningRequest.hasOwnProperty('eventId')) { michael@0: let id = this._runningRequest.eventId; michael@0: if (eventId === id) { michael@0: this.executeCallback(false); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: _applyDetection: function _applyDetection() { michael@0: debug('enter applyDetection('+ this._runningRequest.interfaceName + ')'); michael@0: michael@0: // Execute network interface preparation michael@0: if (this._runningRequest.hasOwnProperty('callback')) { michael@0: this._runningRequest.callback.prepare(); michael@0: } else { michael@0: this._startDetection(); michael@0: } michael@0: }, michael@0: michael@0: _startDetection: function _startDetection() { michael@0: debug('startDetection {site=' + this._canonicalSiteURL + ',content=' michael@0: + this._canonicalSiteExpectedContent + ',time=' + this._maxWaitingTime + '}'); michael@0: let self = this; michael@0: michael@0: let urlFetcher = new URLFetcher(this._canonicalSiteURL, this._maxWaitingTime); michael@0: michael@0: let mayRetry = this._mayRetry.bind(this); michael@0: michael@0: urlFetcher.ontimeout = mayRetry; michael@0: urlFetcher.onerror = mayRetry; michael@0: urlFetcher.onsuccess = function (content) { michael@0: if (self.validateContent(content)) { michael@0: self.executeCallback(true); michael@0: } else { michael@0: // Content of the canonical website has been overwrite michael@0: self._startLogin(); michael@0: } michael@0: }; michael@0: urlFetcher.onredirectorerror = function (status) { michael@0: if (status >= 300 && status <= 399) { michael@0: // The canonical website has been redirected to an unknown location michael@0: self._startLogin(); michael@0: } else { michael@0: mayRetry(); michael@0: } michael@0: }; michael@0: michael@0: this._runningRequest['urlFetcher'] = urlFetcher; michael@0: }, michael@0: michael@0: _startLogin: function _startLogin() { michael@0: let id = this._allocateRequestId(); michael@0: let details = { michael@0: type: kOpenCaptivePortalLoginEvent, michael@0: id: id, michael@0: url: this._canonicalSiteURL, michael@0: }; michael@0: this._loginObserver.attach(); michael@0: this._runningRequest['eventId'] = id; michael@0: this._sendEvent(kOpenCaptivePortalLoginEvent, details); michael@0: }, michael@0: michael@0: _mayRetry: function _mayRetry() { michael@0: if (this._runningRequest.retryCount++ < this._maxRetryCount) { michael@0: debug('retry-Detection: ' + this._runningRequest.retryCount + '/' + this._maxRetryCount); michael@0: this._startDetection(); michael@0: } else { michael@0: this.executeCallback(true); michael@0: } michael@0: }, michael@0: michael@0: executeCallback: function executeCallback(success) { michael@0: if (this._runningRequest) { michael@0: debug('callback executed'); michael@0: if (this._runningRequest.hasOwnProperty('callback')) { michael@0: this._runningRequest.callback.complete(success); michael@0: } michael@0: michael@0: // Continue the following request michael@0: this._runningRequest['complete'] = true; michael@0: this._removeRequest(this._runningRequest.interfaceName); michael@0: } michael@0: }, michael@0: michael@0: _sendEvent: function _sendEvent(topic, details) { michael@0: debug('sendEvent "' + JSON.stringify(details) + '"'); michael@0: Services.obs.notifyObservers(this, michael@0: topic, michael@0: JSON.stringify(details)); michael@0: }, michael@0: michael@0: validateContent: function validateContent(content) { michael@0: debug('received content: ' + content); michael@0: return (content === this._canonicalSiteExpectedContent); michael@0: }, michael@0: michael@0: _allocateRequestId: function _allocateRequestId() { michael@0: let newId = this._nextRequestId++; michael@0: return newId.toString(); michael@0: }, michael@0: michael@0: _runNextRequest: function _runNextRequest() { michael@0: let nextRequest = this._requestQueue.shift(); michael@0: if (nextRequest) { michael@0: this._runningRequest = nextRequest; michael@0: this._applyDetection(); michael@0: } michael@0: }, michael@0: michael@0: _addRequest: function _addRequest(request) { michael@0: this._interfaceNames[request.interfaceName] = true; michael@0: this._requestQueue.push(request); michael@0: if (!this._runningRequest) { michael@0: this._runNextRequest(); michael@0: } michael@0: }, michael@0: michael@0: _removeRequest: function _removeRequest(aInterfaceName) { michael@0: if (!this._interfaceNames[aInterfaceName]) { michael@0: return; michael@0: } michael@0: michael@0: delete this._interfaceNames[aInterfaceName]; michael@0: michael@0: if (this._runningRequest michael@0: && this._runningRequest.interfaceName === aInterfaceName) { michael@0: this._loginObserver.detach(); michael@0: michael@0: if (!this._runningRequest.complete) { michael@0: // Abort the user login procedure michael@0: if (this._runningRequest.hasOwnProperty('eventId')) { michael@0: let details = { michael@0: type: kAbortCaptivePortalLoginEvent, michael@0: id: this._runningRequest.eventId michael@0: }; michael@0: this._sendEvent(kAbortCaptivePortalLoginEvent, details); michael@0: } michael@0: michael@0: // Abort the ongoing HTTP request michael@0: if (this._runningRequest.hasOwnProperty('urlFetcher')) { michael@0: this._runningRequest.urlFetcher.abort(); michael@0: } michael@0: } michael@0: michael@0: debug('remove running request'); michael@0: this._runningRequest = null; michael@0: michael@0: // Continue next pending reqeust if the ongoing one has been aborted michael@0: this._runNextRequest(); michael@0: return; michael@0: } michael@0: michael@0: // Check if a pending request has been aborted michael@0: for (let i = 0; i < this._requestQueue.length; i++) { michael@0: if (this._requestQueue[i].interfaceName == aInterfaceName) { michael@0: this._requestQueue.splice(i, 1); michael@0: michael@0: debug('remove pending request #' + i + ', remaining ' + this._requestQueue.length); michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: }; michael@0: michael@0: let debug; michael@0: if (DEBUG) { michael@0: debug = function (s) { michael@0: dump('-*- CaptivePortalDetector component: ' + s + '\n'); michael@0: }; michael@0: } else { michael@0: debug = function (s) {}; michael@0: } michael@0: michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory([CaptivePortalDetector]);