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]);