toolkit/components/captivedetect/captivedetect.js

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

mercurial