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