diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /toolkit/components/captivedetect | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/captivedetect')
16 files changed, 1381 insertions, 0 deletions
diff --git a/toolkit/components/captivedetect/CaptiveDetect.jsm b/toolkit/components/captivedetect/CaptiveDetect.jsm new file mode 100644 index 0000000000..3434d96982 --- /dev/null +++ b/toolkit/components/captivedetect/CaptiveDetect.jsm @@ -0,0 +1,553 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyGlobalGetters(this, ["XMLHttpRequest"]); + +const DEBUG = false; // set to true to show debug messages + +const kCAPTIVEPORTALDETECTOR_CID = Components.ID( + "{d9cd00ba-aa4d-47b1-8792-b1fe0cd35060}" +); + +const kOpenCaptivePortalLoginEvent = "captive-portal-login"; +const kAbortCaptivePortalLoginEvent = "captive-portal-login-abort"; +const kCaptivePortalLoginSuccessEvent = "captive-portal-login-success"; +const kCaptivePortalCheckComplete = "captive-portal-check-complete"; + +function URLFetcher(url, timeout) { + let self = this; + let xhr = new XMLHttpRequest(); + xhr.open("GET", url, true); + // Prevent the request from reading from the cache. + xhr.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; + // Prevent the request from writing to the cache. + xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; + // Prevent privacy leaks + xhr.channel.loadFlags |= Ci.nsIRequest.LOAD_ANONYMOUS; + // Use the system's resolver for this check + xhr.channel.setTRRMode(Ci.nsIRequest.TRR_DISABLED_MODE); + // We except this from being classified + xhr.channel.loadFlags |= Ci.nsIChannel.LOAD_BYPASS_URL_CLASSIFIER; + // Prevent HTTPS-Only Mode from upgrading the request. + xhr.channel.loadInfo.httpsOnlyStatus |= Ci.nsILoadInfo.HTTPS_ONLY_EXEMPT; + // Allow deprecated HTTP request from SystemPrincipal + xhr.channel.loadInfo.allowDeprecatedSystemRequests = true; + + // We don't want to follow _any_ redirects + xhr.channel.QueryInterface(Ci.nsIHttpChannel).redirectionLimit = 0; + + // bug 1666072 - firefox.com returns a HSTS header triggering a https upgrade + // but the upgrade triggers an internal redirect causing an incorrect locked + // portal notification. We exclude CP detection from STS. + xhr.channel.QueryInterface(Ci.nsIHttpChannel).allowSTS = false; + + // The Cache-Control header is only interpreted by proxies and the + // final destination. It does not help if a resource is already + // cached locally. + xhr.setRequestHeader("Cache-Control", "no-cache"); + // HTTP/1.0 servers might not implement Cache-Control and + // might only implement Pragma: no-cache + xhr.setRequestHeader("Pragma", "no-cache"); + + xhr.timeout = timeout; + xhr.ontimeout = function() { + self.ontimeout(); + }; + xhr.onerror = function() { + self.onerror(); + }; + xhr.onreadystatechange = function(oEvent) { + if (xhr.readyState === 4) { + if (self._isAborted) { + return; + } + if (xhr.status === 200) { + self.onsuccess(xhr.responseText); + } else if (xhr.status) { + self.onredirectorerror(xhr.status); + } else if ( + xhr.channel && + xhr.channel.status == Cr.NS_ERROR_REDIRECT_LOOP + ) { + // For some redirects we don't get a status, so we need to check it + // this way. This only works because we set the redirectionLimit to 0. + self.onredirectorerror(300); + // No need to invoke the onerror callback, we handled it here. + xhr.onerror = null; + } + } + }; + xhr.send(); + this._xhr = xhr; +} + +URLFetcher.prototype = { + _isAborted: false, + ontimeout() {}, + onerror() {}, + abort() { + if (!this._isAborted) { + this._isAborted = true; + this._xhr.abort(); + } + }, +}; + +function LoginObserver(captivePortalDetector) { + const LOGIN_OBSERVER_STATE_DETACHED = 0; /* Should not monitor network activity since no ongoing login procedure */ + const LOGIN_OBSERVER_STATE_IDLE = 1; /* No network activity currently, waiting for a longer enough idle period */ + const LOGIN_OBSERVER_STATE_BURST = 2; /* Network activity is detected, probably caused by a login procedure */ + const LOGIN_OBSERVER_STATE_VERIFY_NEEDED = 3; /* Verifing network accessiblity is required after a long enough idle */ + const LOGIN_OBSERVER_STATE_VERIFYING = 4; /* LoginObserver is probing if public network is available */ + + let state = LOGIN_OBSERVER_STATE_DETACHED; + + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + let activityDistributor = Cc[ + "@mozilla.org/network/http-activity-distributor;1" + ].getService(Ci.nsIHttpActivityDistributor); + let urlFetcher = null; + + let pageCheckingDone = function pageCheckingDone() { + if (state === LOGIN_OBSERVER_STATE_VERIFYING) { + urlFetcher = null; + // Finish polling the canonical site, switch back to idle state and + // waiting for next burst + state = LOGIN_OBSERVER_STATE_IDLE; + timer.initWithCallback( + observer, + captivePortalDetector._pollingTime, + timer.TYPE_ONE_SHOT + ); + } + }; + + let checkPageContent = function checkPageContent() { + debug("checking if public network is available after the login procedure"); + + urlFetcher = new URLFetcher( + captivePortalDetector._canonicalSiteURL, + captivePortalDetector._maxWaitingTime + ); + urlFetcher.ontimeout = pageCheckingDone; + urlFetcher.onerror = pageCheckingDone; + urlFetcher.onsuccess = function(content) { + if (captivePortalDetector.validateContent(content)) { + urlFetcher = null; + captivePortalDetector.executeCallback(true); + } else { + pageCheckingDone(); + } + }; + urlFetcher.onredirectorerror = pageCheckingDone; + }; + + // Public interface of LoginObserver + let observer = { + QueryInterface: ChromeUtils.generateQI([ + "nsIHttpActivityObserver", + "nsITimerCallback", + ]), + + attach: function attach() { + if (state === LOGIN_OBSERVER_STATE_DETACHED) { + activityDistributor.addObserver(this); + state = LOGIN_OBSERVER_STATE_IDLE; + timer.initWithCallback( + this, + captivePortalDetector._pollingTime, + timer.TYPE_ONE_SHOT + ); + debug("attach HttpObserver for login activity"); + } + }, + + detach: function detach() { + if (state !== LOGIN_OBSERVER_STATE_DETACHED) { + if (urlFetcher) { + urlFetcher.abort(); + urlFetcher = null; + } + activityDistributor.removeObserver(this); + timer.cancel(); + state = LOGIN_OBSERVER_STATE_DETACHED; + debug("detach HttpObserver for login activity"); + } + }, + + /* + * Treat all HTTP transactions as captive portal login activities. + */ + observeActivity: function observeActivity( + aHttpChannel, + aActivityType, + aActivitySubtype, + aTimestamp, + aExtraSizeData, + aExtraStringData + ) { + if ( + aActivityType === + Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION && + aActivitySubtype === + Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE + ) { + switch (state) { + case LOGIN_OBSERVER_STATE_IDLE: + case LOGIN_OBSERVER_STATE_VERIFY_NEEDED: + state = LOGIN_OBSERVER_STATE_BURST; + break; + default: + break; + } + } + }, + + /* + * Check if login activity is finished according to HTTP burst. + */ + notify: function notify() { + switch (state) { + case LOGIN_OBSERVER_STATE_BURST: + // Wait while network stays idle for a short period + state = LOGIN_OBSERVER_STATE_VERIFY_NEEDED; + // Fall through to start polling timer + case LOGIN_OBSERVER_STATE_IDLE: + // Just fall through to perform a captive portal check. + case LOGIN_OBSERVER_STATE_VERIFY_NEEDED: + // Polling the canonical website since network stays idle for a while + state = LOGIN_OBSERVER_STATE_VERIFYING; + checkPageContent(); + break; + + default: + break; + } + }, + }; + + return observer; +} + +function CaptivePortalDetector() { + // Load preference + this._canonicalSiteURL = null; + this._canonicalSiteExpectedContent = null; + + try { + this._canonicalSiteURL = Services.prefs.getCharPref( + "captivedetect.canonicalURL" + ); + this._canonicalSiteExpectedContent = Services.prefs.getCharPref( + "captivedetect.canonicalContent" + ); + } catch (e) { + debug("canonicalURL or canonicalContent not set."); + } + + this._maxWaitingTime = Services.prefs.getIntPref( + "captivedetect.maxWaitingTime" + ); + this._pollingTime = Services.prefs.getIntPref("captivedetect.pollingTime"); + this._maxRetryCount = Services.prefs.getIntPref( + "captivedetect.maxRetryCount" + ); + debug( + "Load Prefs {site=" + + this._canonicalSiteURL + + ",content=" + + this._canonicalSiteExpectedContent + + ",time=" + + this._maxWaitingTime + + "max-retry=" + + this._maxRetryCount + + "}" + ); + + // Create HttpObserver for monitoring the login procedure + this._loginObserver = LoginObserver(this); + + this._nextRequestId = 0; + this._runningRequest = null; + this._requestQueue = []; // Maintain a progress table, store callbacks and the ongoing XHR + this._interfaceNames = {}; // Maintain names of the requested network interfaces + + debug( + "CaptiveProtalDetector initiated, waiting for network connection established" + ); +} + +CaptivePortalDetector.prototype = { + classID: kCAPTIVEPORTALDETECTOR_CID, + QueryInterface: ChromeUtils.generateQI(["nsICaptivePortalDetector"]), + + // nsICaptivePortalDetector + checkCaptivePortal: function checkCaptivePortal(aInterfaceName, aCallback) { + if (!this._canonicalSiteURL) { + throw Components.Exception("No canonical URL set up."); + } + + // Prevent multiple requests on a single network interface + if (this._interfaceNames[aInterfaceName]) { + throw Components.Exception( + "Do not allow multiple request on one interface: " + aInterfaceName + ); + } + + let request = { interfaceName: aInterfaceName }; + if (aCallback) { + let callback = aCallback.QueryInterface(Ci.nsICaptivePortalCallback); + request.callback = callback; + request.retryCount = 0; + } + this._addRequest(request); + }, + + abort: function abort(aInterfaceName) { + debug("abort for " + aInterfaceName); + this._removeRequest(aInterfaceName); + }, + + finishPreparation: function finishPreparation(aInterfaceName) { + debug('finish preparation phase for interface "' + aInterfaceName + '"'); + if ( + !this._runningRequest || + this._runningRequest.interfaceName !== aInterfaceName + ) { + debug("invalid finishPreparation for " + aInterfaceName); + throw Components.Exception( + "only first request is allowed to invoke |finishPreparation|" + ); + } + + this._startDetection(); + }, + + cancelLogin: function cancelLogin(eventId) { + debug('login canceled by user for request "' + eventId + '"'); + // Captive portal login procedure is canceled by user + if ( + this._runningRequest && + this._runningRequest.hasOwnProperty("eventId") + ) { + let id = this._runningRequest.eventId; + if (eventId === id) { + this.executeCallback(false); + } + } + }, + + _applyDetection: function _applyDetection() { + debug("enter applyDetection(" + this._runningRequest.interfaceName + ")"); + + // Execute network interface preparation + if (this._runningRequest.hasOwnProperty("callback")) { + this._runningRequest.callback.prepare(); + } else { + this._startDetection(); + } + }, + + _startDetection: function _startDetection() { + debug( + "startDetection {site=" + + this._canonicalSiteURL + + ",content=" + + this._canonicalSiteExpectedContent + + ",time=" + + this._maxWaitingTime + + "}" + ); + let self = this; + + let urlFetcher = new URLFetcher( + this._canonicalSiteURL, + this._maxWaitingTime + ); + + let mayRetry = this._mayRetry.bind(this); + + urlFetcher.ontimeout = mayRetry; + urlFetcher.onerror = mayRetry; + urlFetcher.onsuccess = function(content) { + if (self.validateContent(content)) { + self.executeCallback(true); + } else { + // Content of the canonical website has been overwrite + self._startLogin(); + } + }; + urlFetcher.onredirectorerror = function(status) { + if (status >= 300 && status <= 399) { + // The canonical website has been redirected to an unknown location + self._startLogin(); + } else { + mayRetry(); + } + }; + + this._runningRequest.urlFetcher = urlFetcher; + }, + + _startLogin: function _startLogin() { + let id = this._allocateRequestId(); + let details = { + type: kOpenCaptivePortalLoginEvent, + id, + url: this._canonicalSiteURL, + }; + this._loginObserver.attach(); + this._runningRequest.eventId = id; + this._sendEvent(kOpenCaptivePortalLoginEvent, details); + }, + + _mayRetry: function _mayRetry() { + if ( + this._runningRequest && + this._runningRequest.retryCount++ < this._maxRetryCount + ) { + debug( + "retry-Detection: " + + this._runningRequest.retryCount + + "/" + + this._maxRetryCount + ); + this._startDetection(); + } else { + this.executeCallback(false); + } + }, + + executeCallback: function executeCallback(success) { + if (this._runningRequest) { + debug("callback executed"); + if (this._runningRequest.hasOwnProperty("callback")) { + this._runningRequest.callback.complete(success); + } + + // Only when the request has a event id and |success| is true + // do we need to notify the login-success event. + if (this._runningRequest.hasOwnProperty("eventId") && success) { + let details = { + type: kCaptivePortalLoginSuccessEvent, + id: this._runningRequest.eventId, + }; + this._sendEvent(kCaptivePortalLoginSuccessEvent, details); + } + + // Continue the following request + this._runningRequest.complete = true; + this._removeRequest(this._runningRequest.interfaceName); + } + }, + + _sendEvent: function _sendEvent(topic, details) { + debug('sendEvent "' + JSON.stringify(details) + '"'); + Services.obs.notifyObservers(this, topic, JSON.stringify(details)); + }, + + validateContent: function validateContent(content) { + debug("received content: " + content); + let valid = content === this._canonicalSiteExpectedContent; + // We need a way to indicate that a check has been performed, and if we are + // still in a captive portal. + this._sendEvent(kCaptivePortalCheckComplete, !valid); + return valid; + }, + + _allocateRequestId: function _allocateRequestId() { + let newId = this._nextRequestId++; + return newId.toString(); + }, + + _runNextRequest: function _runNextRequest() { + let nextRequest = this._requestQueue.shift(); + if (nextRequest) { + this._runningRequest = nextRequest; + this._applyDetection(); + } + }, + + _addRequest: function _addRequest(request) { + this._interfaceNames[request.interfaceName] = true; + this._requestQueue.push(request); + if (!this._runningRequest) { + this._runNextRequest(); + } + }, + + _removeRequest: function _removeRequest(aInterfaceName) { + if (!this._interfaceNames[aInterfaceName]) { + return; + } + + delete this._interfaceNames[aInterfaceName]; + + if ( + this._runningRequest && + this._runningRequest.interfaceName === aInterfaceName + ) { + this._loginObserver.detach(); + + if (!this._runningRequest.complete) { + // Abort the user login procedure + if (this._runningRequest.hasOwnProperty("eventId")) { + let details = { + type: kAbortCaptivePortalLoginEvent, + id: this._runningRequest.eventId, + }; + this._sendEvent(kAbortCaptivePortalLoginEvent, details); + } + + // Abort the ongoing HTTP request + if (this._runningRequest.hasOwnProperty("urlFetcher")) { + this._runningRequest.urlFetcher.abort(); + } + } + + debug("remove running request"); + this._runningRequest = null; + + // Continue next pending reqeust if the ongoing one has been aborted + this._runNextRequest(); + return; + } + + // Check if a pending request has been aborted + for (let i = 0; i < this._requestQueue.length; i++) { + if (this._requestQueue[i].interfaceName == aInterfaceName) { + this._requestQueue.splice(i, 1); + + debug( + "remove pending request #" + + i + + ", remaining " + + this._requestQueue.length + ); + break; + } + } + }, +}; + +var debug; +if (DEBUG) { + // eslint-disable-next-line no-global-assign + debug = function(s) { + dump("-*- CaptivePortalDetector component: " + s + "\n"); + }; +} else { + // eslint-disable-next-line no-global-assign + debug = function(s) {}; +} + +var EXPORTED_SYMBOLS = ["CaptivePortalDetector"]; diff --git a/toolkit/components/captivedetect/components.conf b/toolkit/components/captivedetect/components.conf new file mode 100644 index 0000000000..d247f1aa7f --- /dev/null +++ b/toolkit/components/captivedetect/components.conf @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Classes = [ + { + 'cid': '{d9cd00ba-aa4d-47b1-8792-b1fe0cd35060}', + 'contract_ids': ['@mozilla.org/toolkit/captive-detector;1'], + 'jsm': 'resource://gre/modules/CaptiveDetect.jsm', + 'constructor': 'CaptivePortalDetector', + }, +] diff --git a/toolkit/components/captivedetect/moz.build b/toolkit/components/captivedetect/moz.build new file mode 100644 index 0000000000..d18f851183 --- /dev/null +++ b/toolkit/components/captivedetect/moz.build @@ -0,0 +1,24 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Device Interfaces") + +XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"] + +XPIDL_SOURCES += [ + "nsICaptivePortalDetector.idl", +] + +XPIDL_MODULE = "captivedetect" + +EXTRA_JS_MODULES += [ + "CaptiveDetect.jsm", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] diff --git a/toolkit/components/captivedetect/nsICaptivePortalDetector.idl b/toolkit/components/captivedetect/nsICaptivePortalDetector.idl new file mode 100644 index 0000000000..ba8fff416b --- /dev/null +++ b/toolkit/components/captivedetect/nsICaptivePortalDetector.idl @@ -0,0 +1,53 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[scriptable, uuid(593fdeec-6284-4de8-b416-8e63cbdc695e)] +interface nsICaptivePortalCallback : nsISupports +{ + /** + * Preparation for network interface before captive portal detection started. + */ + void prepare(); + + /** + * Invoke callbacks after captive portal detection finished. + */ + void complete(in bool success); +}; + +[scriptable, uuid(2f827c5a-f551-477f-af09-71adbfbd854a)] +interface nsICaptivePortalDetector : nsISupports +{ + /** + * Perform captive portal detection on specific network interface. + * @param ifname The name of network interface, exception will be thrwon + * if the same interface has unfinished request. + * @param callback Callbacks when detection procedure starts and finishes. + */ + void checkCaptivePortal(in AString ifname, + in nsICaptivePortalCallback callback); + + /** + * Abort captive portal detection for specific network interface + * due to system failure, callback will not be invoked. + * @param ifname The name of network interface. + */ + void abort(in AString ifname); + + /** + * Cancel captive portal login procedure by user, callback will be invoked. + * @param eventId Login event id provided in |captive-portal-login| event. + */ + void cancelLogin(in AString eventId); + + /** + * Notify prepare phase is finished, routing and dns must be ready for sending + * out XMLHttpRequest. this is callback for CaptivePortalDetector API user. + * @param ifname The name of network interface, must be unique. + */ + void finishPreparation(in AString ifname); +}; diff --git a/toolkit/components/captivedetect/test/unit/head_setprefs.js b/toolkit/components/captivedetect/test/unit/head_setprefs.js new file mode 100644 index 0000000000..d7b822a456 --- /dev/null +++ b/toolkit/components/captivedetect/test/unit/head_setprefs.js @@ -0,0 +1,77 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +var { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var { + HTTP_400, + HTTP_401, + HTTP_402, + HTTP_403, + HTTP_404, + HTTP_405, + HTTP_406, + HTTP_407, + HTTP_408, + HTTP_409, + HTTP_410, + HTTP_411, + HTTP_412, + HTTP_413, + HTTP_414, + HTTP_415, + HTTP_417, + HTTP_500, + HTTP_501, + HTTP_502, + HTTP_503, + HTTP_504, + HTTP_505, + HttpError, + HttpServer, +} = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyServiceGetter( + this, + "gCaptivePortalDetector", + "@mozilla.org/toolkit/captive-detector;1", + "nsICaptivePortalDetector" +); + +const kCanonicalSitePath = "/canonicalSite.html"; +const kCanonicalSiteContent = "true"; +const kPrefsCanonicalURL = "captivedetect.canonicalURL"; +const kPrefsCanonicalContent = "captivedetect.canonicalContent"; +const kPrefsMaxWaitingTime = "captivedetect.maxWaitingTime"; +const kPrefsPollingTime = "captivedetect.pollingTime"; + +var gServer; +var gServerURL; + +function setupPrefs() { + Services.prefs.setCharPref( + kPrefsCanonicalURL, + gServerURL + kCanonicalSitePath + ); + Services.prefs.setCharPref(kPrefsCanonicalContent, kCanonicalSiteContent); + Services.prefs.setIntPref(kPrefsMaxWaitingTime, 0); + Services.prefs.setIntPref(kPrefsPollingTime, 1); +} + +function run_captivedetect_test(xhr_handler, fakeUIResponse, testfun) { + gServer = new HttpServer(); + gServer.registerPathHandler(kCanonicalSitePath, xhr_handler); + gServer.start(-1); + gServerURL = "http://localhost:" + gServer.identity.primaryPort; + + setupPrefs(); + + fakeUIResponse(); + + testfun(); +} diff --git a/toolkit/components/captivedetect/test/unit/test_abort.js b/toolkit/components/captivedetect/test/unit/test_abort.js new file mode 100644 index 0000000000..5a2451d5b8 --- /dev/null +++ b/toolkit/components/captivedetect/test/unit/test_abort.js @@ -0,0 +1,53 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const kInterfaceName = "wifi"; + +var server; +var step = 0; +var loginFinished = false; + +function xhr_handler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/plain", false); + if (loginFinished) { + response.write("true"); + } else { + response.write("false"); + } +} + +function fakeUIResponse() { + Services.obs.addObserver(function observe(subject, topic, data) { + if (topic === "captive-portal-login") { + do_throw("should not receive captive-portal-login event"); + } + }, "captive-portal-login"); +} + +function test_abort() { + do_test_pending(); + + let callback = { + QueryInterface: ChromeUtils.generateQI(["nsICaptivePortalCallback"]), + prepare: function prepare() { + Assert.equal(++step, 1); + gCaptivePortalDetector.finishPreparation(kInterfaceName); + }, + complete: function complete(success) { + do_throw("should not execute |complete| callback"); + }, + }; + + gCaptivePortalDetector.checkCaptivePortal(kInterfaceName, callback); + gCaptivePortalDetector.abort(kInterfaceName); + gServer.stop(do_test_finished); +} + +function run_test() { + run_captivedetect_test(xhr_handler, fakeUIResponse, test_abort); +} diff --git a/toolkit/components/captivedetect/test/unit/test_abort_during_user_login.js b/toolkit/components/captivedetect/test/unit/test_abort_during_user_login.js new file mode 100644 index 0000000000..f77e11888f --- /dev/null +++ b/toolkit/components/captivedetect/test/unit/test_abort_during_user_login.js @@ -0,0 +1,65 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const kInterfaceName = "wifi"; + +var server; +var step = 0; +var loginFinished = false; + +function xhr_handler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/plain", false); + if (loginFinished) { + response.write("true"); + } else { + response.write("false"); + } +} + +function fakeUIResponse() { + let requestId; + Services.obs.addObserver(function observe(subject, topic, data) { + if (topic === "captive-portal-login") { + let xhr = new XMLHttpRequest(); + xhr.open("GET", gServerURL + kCanonicalSitePath, true); + xhr.send(); + loginFinished = true; + Assert.equal(++step, 2); + requestId = JSON.parse(data).id; + gCaptivePortalDetector.abort(kInterfaceName); + } + }, "captive-portal-login"); + Services.obs.addObserver(function observe(subject, topic, data) { + if (topic === "captive-portal-login-abort") { + Assert.equal(++step, 3); + Assert.equal(JSON.parse(data).id, requestId); + gServer.stop(do_test_finished); + } + }, "captive-portal-login-abort"); +} + +function test_abort() { + do_test_pending(); + + let callback = { + QueryInterface: ChromeUtils.generateQI(["nsICaptivePortalCallback"]), + prepare: function prepare() { + Assert.equal(++step, 1); + gCaptivePortalDetector.finishPreparation(kInterfaceName); + }, + complete: function complete(success) { + do_throw("should not execute |complete| callback"); + }, + }; + + gCaptivePortalDetector.checkCaptivePortal(kInterfaceName, callback); +} + +function run_test() { + run_captivedetect_test(xhr_handler, fakeUIResponse, test_abort); +} diff --git a/toolkit/components/captivedetect/test/unit/test_abort_ongoing_request.js b/toolkit/components/captivedetect/test/unit/test_abort_ongoing_request.js new file mode 100644 index 0000000000..b297079841 --- /dev/null +++ b/toolkit/components/captivedetect/test/unit/test_abort_ongoing_request.js @@ -0,0 +1,75 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const kInterfaceName = "wifi"; +const kOtherInterfaceName = "ril"; + +var server; +var step = 0; +var loginFinished = false; + +function xhr_handler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/plain", false); + if (loginFinished) { + response.write("true"); + } else { + response.write("false"); + } +} + +function fakeUIResponse() { + Services.obs.addObserver(function observe(subject, topic, data) { + if (topic === "captive-portal-login") { + let xhr = new XMLHttpRequest(); + xhr.open("GET", gServerURL + kCanonicalSitePath, true); + xhr.send(); + loginFinished = true; + Assert.equal(++step, 3); + } + }, "captive-portal-login"); +} + +function test_multiple_requests_abort() { + do_test_pending(); + + let callback = { + QueryInterface: ChromeUtils.generateQI(["nsICaptivePortalCallback"]), + prepare: function prepare() { + Assert.equal(++step, 1); + gCaptivePortalDetector.finishPreparation(kInterfaceName); + }, + complete: function complete(success) { + do_throw("should not execute |complete| callback for " + kInterfaceName); + }, + }; + + let otherCallback = { + QueryInterface: ChromeUtils.generateQI(["nsICaptivePortalCallback"]), + prepare: function prepare() { + Assert.equal(++step, 2); + gCaptivePortalDetector.finishPreparation(kOtherInterfaceName); + }, + complete: function complete(success) { + Assert.equal(++step, 4); + Assert.ok(success); + gServer.stop(do_test_finished); + }, + }; + + gCaptivePortalDetector.checkCaptivePortal(kInterfaceName, callback); + gCaptivePortalDetector.checkCaptivePortal(kOtherInterfaceName, otherCallback); + gCaptivePortalDetector.abort(kInterfaceName); +} + +function run_test() { + run_captivedetect_test( + xhr_handler, + fakeUIResponse, + test_multiple_requests_abort + ); +} diff --git a/toolkit/components/captivedetect/test/unit/test_abort_pending_request.js b/toolkit/components/captivedetect/test/unit/test_abort_pending_request.js new file mode 100644 index 0000000000..2373332f6c --- /dev/null +++ b/toolkit/components/captivedetect/test/unit/test_abort_pending_request.js @@ -0,0 +1,72 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const kInterfaceName = "wifi"; +const kOtherInterfaceName = "ril"; + +var server; +var step = 0; +var loginFinished = false; + +function xhr_handler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/plain", false); + if (loginFinished) { + response.write("true"); + } else { + response.write("false"); + } +} + +function fakeUIResponse() { + Services.obs.addObserver(function observe(subject, topic, data) { + if (topic === "captive-portal-login") { + let xhr = new XMLHttpRequest(); + xhr.open("GET", gServerURL + kCanonicalSitePath, true); + xhr.send(); + loginFinished = true; + Assert.equal(++step, 2); + } + }, "captive-portal-login"); +} + +function test_abort() { + do_test_pending(); + + let callback = { + QueryInterface: ChromeUtils.generateQI(["nsICaptivePortalCallback"]), + prepare: function prepare() { + Assert.equal(++step, 1); + gCaptivePortalDetector.finishPreparation(kInterfaceName); + }, + complete: function complete(success) { + Assert.equal(++step, 3); + Assert.ok(success); + gServer.stop(do_test_finished); + }, + }; + + let otherCallback = { + QueryInterface: ChromeUtils.generateQI(["nsICaptivePortalCallback"]), + prepare: function prepare() { + do_throw( + "should not execute |prepare| callback for " + kOtherInterfaceName + ); + }, + complete: function complete(success) { + do_throw("should not execute |complete| callback for " + kInterfaceName); + }, + }; + + gCaptivePortalDetector.checkCaptivePortal(kInterfaceName, callback); + gCaptivePortalDetector.checkCaptivePortal(kOtherInterfaceName, otherCallback); + gCaptivePortalDetector.abort(kOtherInterfaceName); +} + +function run_test() { + run_captivedetect_test(xhr_handler, fakeUIResponse, test_abort); +} diff --git a/toolkit/components/captivedetect/test/unit/test_captive_portal_found.js b/toolkit/components/captivedetect/test/unit/test_captive_portal_found.js new file mode 100644 index 0000000000..cf789fa4e6 --- /dev/null +++ b/toolkit/components/captivedetect/test/unit/test_captive_portal_found.js @@ -0,0 +1,66 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const kInterfaceName = "wifi"; + +var server; +var step = 0; +var loginFinished = false; + +function xhr_handler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/plain", false); + if (loginFinished) { + response.write("true"); + } else { + response.write("false"); + } +} + +function fakeUIResponse() { + Services.obs.addObserver(function observe(subject, topic, data) { + if (topic === "captive-portal-login") { + let xhr = new XMLHttpRequest(); + xhr.open("GET", gServerURL + kCanonicalSitePath, true); + xhr.send(); + loginFinished = true; + Assert.equal(++step, 2); + } + }, "captive-portal-login"); + + Services.obs.addObserver(function observe(subject, topic, data) { + if (topic === "captive-portal-login-success") { + Assert.equal(++step, 4); + gServer.stop(do_test_finished); + } + }, "captive-portal-login-success"); +} + +function test_portal_found() { + do_test_pending(); + + let callback = { + QueryInterface: ChromeUtils.generateQI(["nsICaptivePortalCallback"]), + prepare: function prepare() { + Assert.equal(++step, 1); + gCaptivePortalDetector.finishPreparation(kInterfaceName); + }, + complete: function complete(success) { + // Since this is a synchronous callback, it must happen before + // 'captive-portal-login-success' is received. + // (Check captivedetect.js::executeCallback + Assert.equal(++step, 3); + Assert.ok(success); + }, + }; + + gCaptivePortalDetector.checkCaptivePortal(kInterfaceName, callback); +} + +function run_test() { + run_captivedetect_test(xhr_handler, fakeUIResponse, test_portal_found); +} diff --git a/toolkit/components/captivedetect/test/unit/test_captive_portal_found_303.js b/toolkit/components/captivedetect/test/unit/test_captive_portal_found_303.js new file mode 100644 index 0000000000..d69094b5a3 --- /dev/null +++ b/toolkit/components/captivedetect/test/unit/test_captive_portal_found_303.js @@ -0,0 +1,74 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const kInterfaceName = "wifi"; + +var step = 0; +var loginFinished = false; + +var gRedirectServer; +var gRedirectServerURL; + +function xhr_handler(metadata, response) { + if (loginFinished) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/plain", false); + response.write("true"); + } else { + response.setStatusLine(metadata.httpVersion, 303, "See Other"); + response.setHeader("Location", gRedirectServerURL, false); + response.setHeader("Content-Type", "text/html", false); + } +} + +function fakeUIResponse() { + Services.obs.addObserver(function observe(subject, topic, data) { + if (topic === "captive-portal-login") { + let xhr = new XMLHttpRequest(); + xhr.open("GET", gServerURL + kCanonicalSitePath, true); + xhr.send(); + loginFinished = true; + Assert.equal(++step, 2); + } + }, "captive-portal-login"); + + Services.obs.addObserver(function observe(subject, topic, data) { + if (topic === "captive-portal-login-success") { + Assert.equal(++step, 4); + gServer.stop(function() { + gRedirectServer.stop(do_test_finished); + }); + } + }, "captive-portal-login-success"); +} + +function test_portal_found() { + do_test_pending(); + + let callback = { + QueryInterface: ChromeUtils.generateQI(["nsICaptivePortalCallback"]), + prepare: function prepare() { + Assert.equal(++step, 1); + gCaptivePortalDetector.finishPreparation(kInterfaceName); + }, + complete: function complete(success) { + Assert.equal(++step, 3); + Assert.ok(success); + }, + }; + + gCaptivePortalDetector.checkCaptivePortal(kInterfaceName, callback); +} + +function run_test() { + gRedirectServer = new HttpServer(); + gRedirectServer.start(-1); + gRedirectServerURL = + "http://localhost:" + gRedirectServer.identity.primaryPort; + + run_captivedetect_test(xhr_handler, fakeUIResponse, test_portal_found); +} diff --git a/toolkit/components/captivedetect/test/unit/test_captive_portal_not_found.js b/toolkit/components/captivedetect/test/unit/test_captive_portal_not_found.js new file mode 100644 index 0000000000..e601277018 --- /dev/null +++ b/toolkit/components/captivedetect/test/unit/test_captive_portal_not_found.js @@ -0,0 +1,55 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const kInterfaceName = "wifi"; + +var server; +var step = 0; +var attempt = 0; + +function xhr_handler(metadata, response) { + dump("HTTP activity\n"); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/plain", false); + response.write("true"); + attempt++; +} + +function fakeUIResponse() { + Services.obs.addObserver(function observe(subject, topic, data) { + if (topic == "captive-portal-login") { + do_throw("should not receive captive-portal-login event"); + } + }, "captive-portal-login"); +} + +function test_portal_not_found() { + do_test_pending(); + + let callback = { + QueryInterface: ChromeUtils.generateQI(["nsICaptivePortalCallback"]), + prepare: function prepare() { + Assert.equal(++step, 1); + gCaptivePortalDetector.finishPreparation(kInterfaceName); + }, + complete: function complete(success) { + Assert.equal(++step, 2); + Assert.ok(success); + Assert.equal(attempt, 1); + gServer.stop(function() { + dump("server stop\n"); + do_test_finished(); + }); + }, + }; + + gCaptivePortalDetector.checkCaptivePortal(kInterfaceName, callback); +} + +function run_test() { + run_captivedetect_test(xhr_handler, fakeUIResponse, test_portal_not_found); +} diff --git a/toolkit/components/captivedetect/test/unit/test_captive_portal_not_found_404.js b/toolkit/components/captivedetect/test/unit/test_captive_portal_not_found_404.js new file mode 100644 index 0000000000..b189acabb3 --- /dev/null +++ b/toolkit/components/captivedetect/test/unit/test_captive_portal_not_found_404.js @@ -0,0 +1,49 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const kInterfaceName = "wifi"; + +var server; +var step = 0; +var loginFinished = false; +var attempt = 0; + +function xhr_handler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 404, "Page not Found"); + attempt++; +} + +function fakeUIResponse() { + Services.obs.addObserver(function observe(subject, topic, data) { + if (topic === "captive-portal-login") { + do_throw("should not receive captive-portal-login event"); + } + }, "captive-portal-login"); +} + +function test_portal_not_found() { + do_test_pending(); + + let callback = { + QueryInterface: ChromeUtils.generateQI(["nsICaptivePortalCallback"]), + prepare: function prepare() { + Assert.equal(++step, 1); + gCaptivePortalDetector.finishPreparation(kInterfaceName); + }, + complete: function complete(success) { + Assert.equal(++step, 2); + Assert.ok(!success); + Assert.equal(attempt, 6); + gServer.stop(do_test_finished); + }, + }; + + gCaptivePortalDetector.checkCaptivePortal(kInterfaceName, callback); +} + +function run_test() { + run_captivedetect_test(xhr_handler, fakeUIResponse, test_portal_not_found); +} diff --git a/toolkit/components/captivedetect/test/unit/test_multiple_requests.js b/toolkit/components/captivedetect/test/unit/test_multiple_requests.js new file mode 100644 index 0000000000..5b5856acef --- /dev/null +++ b/toolkit/components/captivedetect/test/unit/test_multiple_requests.js @@ -0,0 +1,84 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const kInterfaceName = "wifi"; +const kOtherInterfaceName = "ril"; + +var server; +var step = 0; +var loginFinished = false; +var loginSuccessCount = 0; + +function xhr_handler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/plain", false); + if (loginFinished) { + response.write("true"); + } else { + response.write("false"); + } +} + +function fakeUIResponse() { + Services.obs.addObserver(function observe(subject, topic, data) { + if (topic === "captive-portal-login") { + let xhr = new XMLHttpRequest(); + xhr.open("GET", gServerURL + kCanonicalSitePath, true); + xhr.send(); + loginFinished = true; + Assert.equal(++step, 2); + } + }, "captive-portal-login"); + + Services.obs.addObserver(function observe(subject, topic, data) { + if (topic === "captive-portal-login-success") { + loginSuccessCount++; + if (loginSuccessCount > 1) { + throw new Error( + "We should only receive 'captive-portal-login-success' once" + ); + } + Assert.equal(++step, 4); + } + }, "captive-portal-login-success"); +} + +function test_multiple_requests() { + do_test_pending(); + + let callback = { + QueryInterface: ChromeUtils.generateQI(["nsICaptivePortalCallback"]), + prepare: function prepare() { + Assert.equal(++step, 1); + gCaptivePortalDetector.finishPreparation(kInterfaceName); + }, + complete: function complete(success) { + Assert.equal(++step, 3); + Assert.ok(success); + }, + }; + + let otherCallback = { + QueryInterface: ChromeUtils.generateQI(["nsICaptivePortalCallback"]), + prepare: function prepare() { + Assert.equal(++step, 5); + gCaptivePortalDetector.finishPreparation(kOtherInterfaceName); + }, + complete: function complete(success) { + Assert.equal(++step, 6); + Assert.ok(success); + gServer.stop(do_test_finished); + }, + }; + + gCaptivePortalDetector.checkCaptivePortal(kInterfaceName, callback); + gCaptivePortalDetector.checkCaptivePortal(kOtherInterfaceName, otherCallback); +} + +function run_test() { + run_captivedetect_test(xhr_handler, fakeUIResponse, test_multiple_requests); +} diff --git a/toolkit/components/captivedetect/test/unit/test_user_cancel.js b/toolkit/components/captivedetect/test/unit/test_user_cancel.js new file mode 100644 index 0000000000..fbe073a999 --- /dev/null +++ b/toolkit/components/captivedetect/test/unit/test_user_cancel.js @@ -0,0 +1,53 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const kInterfaceName = "wifi"; + +var server; +var step = 0; + +function xhr_handler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/plain", false); + response.write("false"); +} + +function fakeUIResponse() { + Services.obs.addObserver(function observe(subject, topic, data) { + if (topic === "captive-portal-login") { + let xhr = new XMLHttpRequest(); + xhr.open("GET", gServerURL + kCanonicalSitePath, true); + xhr.send(); + Assert.equal(++step, 2); + let details = JSON.parse(data); + gCaptivePortalDetector.cancelLogin(details.id); + } + }, "captive-portal-login"); +} + +function test_cancel() { + do_test_pending(); + + let callback = { + QueryInterface: ChromeUtils.generateQI(["nsICaptivePortalCallback"]), + prepare: function prepare() { + Assert.equal(++step, 1); + gCaptivePortalDetector.finishPreparation(kInterfaceName); + }, + complete: function complete(success) { + Assert.equal(++step, 3); + Assert.ok(!success); + gServer.stop(do_test_finished); + }, + }; + + gCaptivePortalDetector.checkCaptivePortal(kInterfaceName, callback); +} + +function run_test() { + run_captivedetect_test(xhr_handler, fakeUIResponse, test_cancel); +} diff --git a/toolkit/components/captivedetect/test/unit/xpcshell.ini b/toolkit/components/captivedetect/test/unit/xpcshell.ini new file mode 100644 index 0000000000..693a31872d --- /dev/null +++ b/toolkit/components/captivedetect/test/unit/xpcshell.ini @@ -0,0 +1,14 @@ +[DEFAULT] +head = head_setprefs.js + +[test_captive_portal_not_found.js] +[test_captive_portal_not_found_404.js] +[test_captive_portal_found.js] +[test_captive_portal_found_303.js] +[test_abort.js] +[test_abort_during_user_login.js] +[test_user_cancel.js] +[test_multiple_requests.js] +[test_abort_ongoing_request.js] +[test_abort_pending_request.js] + |