зеркало из https://github.com/mozilla/gecko-dev.git
466 строки
16 KiB
JavaScript
466 строки
16 KiB
JavaScript
/* -*- 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.loadFlags |= Ci.nsIRequest.LOAD_DISABLE_TRR;
|
|
|
|
// We don't want to follow _any_ redirects
|
|
xhr.channel.QueryInterface(Ci.nsIHttpChannel).redirectionLimit = 0;
|
|
|
|
// 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.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);
|
|
}
|
|
}
|
|
};
|
|
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([Ci.nsIHttpActivityObserver,
|
|
Ci.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 though 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([Ci.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;
|
|
}
|
|
}
|
|
},
|
|
};
|
|
|
|
/* globals debug: true */
|
|
var debug;
|
|
if (DEBUG) {
|
|
debug = function(s) {
|
|
dump("-*- CaptivePortalDetector component: " + s + "\n");
|
|
};
|
|
} else {
|
|
debug = function(s) {};
|
|
}
|
|
|
|
var EXPORTED_SYMBOLS = ["CaptivePortalDetector"];
|