Bug 752982 - Part 1: Implement captive portal detection service. r=thinker
authorShih-Chiang Chien <schien@mozilla.com>
Sat, 15 Sep 2012 11:37:53 +0800
changeset 127097 35d48d9d80d542075bee8ecec666ce7bcb14ac8a
parent 127096 ed29092600501b9b2798c6a9c0ee643eed8281ef
child 127098 33be341bb1334ee4a6c9f69f519447ef1f6d9d41
push id3384
push userlsblakk@mozilla.com
push dateTue, 19 Feb 2013 18:42:39 +0000
treeherdermozilla-aurora@d8c97bae8521 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersthinker
bugs752982
milestone21.0a1
Bug 752982 - Part 1: Implement captive portal detection service. r=thinker
b2g/installer/package-manifest.in
browser/installer/package-manifest.in
browser/installer/removed-files.in
configure.in
mobile/android/installer/package-manifest.in
services/Makefile.in
services/captivedetect/CaptivePortalDetectComponents.manifest
services/captivedetect/Makefile.in
services/captivedetect/captivedetect.js
services/captivedetect/nsICaptivePortalDetector.idl
services/captivedetect/services-captivedetect.js
services/makefiles.sh
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -282,16 +282,19 @@
 @BINPATH@/components/rdf.xpt
 @BINPATH@/components/satchel.xpt
 @BINPATH@/components/saxparser.xpt
 @BINPATH@/components/sessionstore.xpt
 #ifdef MOZ_SERVICES_SYNC
 @BINPATH@/components/services-crypto.xpt
 #endif
 @BINPATH@/components/services-crypto-component.xpt
+#ifdef MOZ_SERVICES_CAPTIVEDETECT
+@BINPATH@/components/services-captivedetect.xpt
+#endif
 @BINPATH@/components/shellservice.xpt
 @BINPATH@/components/shistory.xpt
 @BINPATH@/components/spellchecker.xpt
 @BINPATH@/components/storage.xpt
 @BINPATH@/components/telemetry.xpt
 @BINPATH@/components/toolkitprofile.xpt
 #ifdef MOZ_ENABLE_XREMOTE
 @BINPATH@/components/toolkitremote.xpt
@@ -488,16 +491,20 @@
 @BINPATH@/components/WeaveCrypto.js
 #endif
 @BINPATH@/components/servicesComponents.manifest
 @BINPATH@/components/cryptoComponents.manifest
 #ifdef MOZ_SERVICES_HEALTHREPORT
 @BINPATH@/components/HealthReportComponents.manifest
 @BINPATH@/components/HealthReportService.js
 #endif
+#ifdef MOZ_SERVICES_CAPTIVEDETECT
+@BINPATH@/components/CaptivePortalDetectComponents.manifest
+@BINPATH@/components/captivedetect.js
+#endif
 @BINPATH@/components/TelemetryPing.js
 @BINPATH@/components/TelemetryPing.manifest
 @BINPATH@/components/Webapps.js
 @BINPATH@/components/Webapps.manifest
 @BINPATH@/components/AppsService.js
 @BINPATH@/components/AppsService.manifest
 
 @BINPATH@/components/nsDOMIdentity.js
@@ -585,16 +592,19 @@
 @BINPATH@/defaults/autoconfig/platform.js
 @BINPATH@/defaults/autoconfig/prefcalls.js
 @BINPATH@/defaults/profile/prefs.js
 
 ; Services (gre) prefs
 #ifdef MOZ_SERVICES_SYNC
 @BINPATH@/defaults/pref/services-sync.js
 #endif
+#ifdef MOZ_SERVICES_CAPTIVEDETECT
+@BINPATH@/defaults/pref/services-captivedetect.js
+#endif
 
 ; [Layout Engine Resources]
 ; Style Sheets, Graphics and other Resources used by the layout engine. 
 @BINPATH@/res/EditorOverride.css
 @BINPATH@/res/contenteditable.css
 @BINPATH@/res/designmode.css
 @BINPATH@/res/table-add-column-after-active.gif
 @BINPATH@/res/table-add-column-after-hover.gif
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -279,16 +279,19 @@
 #ifdef MOZ_ENABLE_PROFILER_SPS
 @BINPATH@/components/profiler.xpt
 #endif
 @BINPATH@/components/rdf.xpt
 @BINPATH@/components/satchel.xpt
 @BINPATH@/components/saxparser.xpt
 @BINPATH@/components/sessionstore.xpt
 @BINPATH@/components/services-crypto-component.xpt
+#ifdef MOZ_SERVICES_CAPTIVEDETECT
+@BINPATH@/components/services-captivedetect.xpt
+#endif
 @BINPATH@/components/shellservice.xpt
 @BINPATH@/components/shistory.xpt
 @BINPATH@/components/spellchecker.xpt
 @BINPATH@/components/storage.xpt
 @BINPATH@/components/toolkitprofile.xpt
 #ifdef MOZ_ENABLE_XREMOTE
 @BINPATH@/components/toolkitremote.xpt
 #endif
@@ -468,16 +471,20 @@
 #endif
 #ifdef MOZ_SERVICES_HEALTHREPORT
 @BINPATH@/components/HealthReportComponents.manifest
 #endif
 #ifdef MOZ_SERVICES_SYNC
 @BINPATH@/components/SyncComponents.manifest
 @BINPATH@/components/Weave.js
 #endif
+#ifdef MOZ_SERVICES_CAPTIVEDETECT
+@BINPATH@/components/CaptivePortalDetectComponents.manifest
+@BINPATH@/components/captivedetect.js
+#endif
 @BINPATH@/components/servicesComponents.manifest
 @BINPATH@/components/cryptoComponents.manifest
 @BINPATH@/components/TelemetryPing.js
 @BINPATH@/components/TelemetryPing.manifest
 @BINPATH@/components/messageWakeupService.js
 @BINPATH@/components/messageWakeupService.manifest
 @BINPATH@/components/SettingsManager.js
 @BINPATH@/components/SettingsManager.manifest
--- a/browser/installer/removed-files.in
+++ b/browser/installer/removed-files.in
@@ -971,16 +971,17 @@ xpicleanup@BIN_SUFFIX@
   components/Webapps.manifest
   components/WebContentConverter.js
   defaults/autoconfig/platform.js
   defaults/autoconfig/prefcalls.js
   defaults/pref/firefox-branding.js
   defaults/pref/firefox.js
   defaults/pref/firefox-l10n.js
   defaults/pref/services-sync.js
+  defaults/pref/services-captivedetect.js
   defaults/profile/bookmarks.html
   defaults/profile/chrome/userChrome-example.css
   defaults/profile/chrome/userContent-example.css
   defaults/profile/localstore.rdf
   defaults/profile/mimeTypes.rdf
   defaults/profile/prefs.js
   greprefs.js
   hyphenation/
--- a/configure.in
+++ b/configure.in
@@ -8369,16 +8369,22 @@ if test -n "$MOZ_SERVICES_NOTIFICATIONS"
 fi
 
 dnl Build Sync Services if required
 AC_SUBST(MOZ_SERVICES_SYNC)
 if test -n "$MOZ_SERVICES_SYNC"; then
   AC_DEFINE(MOZ_SERVICES_SYNC)
 fi
 
+dnl Build Captive Portal Detector if required
+AC_SUBST(MOZ_SERVICES_CAPTIVEDETECT)
+if test -n "$MOZ_SERVICES_CAPTIVEDETECT"; then
+  AC_DEFINE(MOZ_SERVICES_CAPTIVEDETECT)
+fi
+
 dnl ========================================================
 if test "$MOZ_DEBUG" -o "$NS_TRACE_MALLOC" -o "$MOZ_DMD"; then
     MOZ_COMPONENTS_VERSION_SCRIPT_LDFLAGS=
 fi
 
 if test "$MOZ_APP_COMPONENT_INCLUDE"; then
   AC_DEFINE_UNQUOTED(MOZ_APP_COMPONENT_INCLUDE, "$MOZ_APP_COMPONENT_INCLUDE")
 fi
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -205,16 +205,19 @@
 @BINPATH@/components/profiler.xpt
 #endif
 @BINPATH@/components/proxyObject.xpt
 @BINPATH@/components/rdf.xpt
 @BINPATH@/components/satchel.xpt
 @BINPATH@/components/saxparser.xpt
 @BINPATH@/components/sessionstore.xpt
 @BINPATH@/components/services-crypto-component.xpt
+#ifdef MOZ_SERVICES_CAPTIVEDETECT
+@BINPATH@/components/services-captivedetect.xpt
+#endif
 @BINPATH@/components/shellservice.xpt
 @BINPATH@/components/shistory.xpt
 @BINPATH@/components/spellchecker.xpt
 @BINPATH@/components/storage.xpt
 @BINPATH@/components/telemetry.xpt
 @BINPATH@/components/toolkitprofile.xpt
 #ifdef MOZ_ENABLE_XREMOTE
 @BINPATH@/components/toolkitremote.xpt
@@ -372,16 +375,21 @@
 @BINPATH@/components/PeerConnection.manifest
 #endif
 
 #ifdef MOZ_SERVICES_HEALTHREPORT
 @BINPATH@/components/HealthReportComponents.manifest
 @BINPATH@/components/HealthReportService.js
 #endif
 
+#ifdef MOZ_SERVICES_CAPTIVEDETECT
+@BINPATH@/components/CaptivePortalDetectComponents.manifest
+@BINPATH@/components/captivedetect.js
+#endif
+
 ; Modules
 @BINPATH@/modules/*
 
 #ifdef MOZ_SAFE_BROWSING
 ; Safe Browsing
 @BINPATH@/components/nsURLClassifier.manifest
 @BINPATH@/components/nsUrlClassifierHashCompleter.js
 @BINPATH@/components/nsUrlClassifierListManager.js
@@ -423,16 +431,20 @@
 @BINPATH@/@PREF_DIR@/mobile.js
 @BINPATH@/@PREF_DIR@/mobile-branding.js
 @BINPATH@/@PREF_DIR@/channel-prefs.js
 @BINPATH@/greprefs.js
 @BINPATH@/defaults/autoconfig/platform.js
 @BINPATH@/defaults/autoconfig/prefcalls.js
 @BINPATH@/defaults/profile/prefs.js
 
+#ifdef MOZ_SERVICES_CAPTIVEDETECT
+@BINPATH@/defaults/pref/services-captivedetect.js
+#endif
+
 ; [Layout Engine Resources]
 ; Style Sheets, Graphics and other Resources used by the layout engine. 
 @BINPATH@/res/EditorOverride.css
 @BINPATH@/res/contenteditable.css
 @BINPATH@/res/designmode.css
 @BINPATH@/res/TopLevelImageDocument.css
 @BINPATH@/res/TopLevelVideoDocument.css
 @BINPATH@/res/table-add-column-after-active.gif
--- a/services/Makefile.in
+++ b/services/Makefile.in
@@ -25,9 +25,13 @@ endif
 ifdef MOZ_SERVICES_METRICS
 PARALLEL_DIRS += metrics
 endif
 
 ifdef MOZ_SERVICES_SYNC
 PARALLEL_DIRS += sync
 endif
 
+ifdef MOZ_SERVICES_CAPTIVEDETECT
+PARALLEL_DIRS += captivedetect
+endif
+
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/services/captivedetect/CaptivePortalDetectComponents.manifest
@@ -0,0 +1,2 @@
+component {d9cd00ba-aa4d-47b1-8792-b1fe0cd35060} captivedetect.js
+contract @mozilla.org/services/captive-detector;1 {d9cd00ba-aa4d-47b1-8792-b1fe0cd35060}
new file mode 100644
--- /dev/null
+++ b/services/captivedetect/Makefile.in
@@ -0,0 +1,27 @@
+# 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/.
+
+DEPTH     = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir    = @srcdir@
+VPATH     = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MODULE         = services-captivedetect
+XPIDL_MODULE   = services-captivedetect
+
+XPIDLSRCS = \
+  nsICaptivePortalDetector.idl \
+  $(NULL)
+
+EXTRA_COMPONENTS = \
+  CaptivePortalDetectComponents.manifest \
+  captivedetect.js \
+  $(NULL)
+
+PREF_JS_EXPORTS := $(srcdir)/services-captivedetect.js
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/services/captivedetect/captivedetect.js
@@ -0,0 +1,426 @@
+/* -*- Mode: Java; 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/. */
+
+'use strict';
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+
+const DEBUG = false; // set to true to show debug messages
+
+const kCAPTIVEPORTALDETECTOR_CONTRACTID = '@mozilla.org/services/captive-detector;1';
+const kCAPTIVEPORTALDETECTOR_CID        = Components.ID('{d9cd00ba-aa4d-47b1-8792-b1fe0cd35060}');
+
+const kOpenCaptivePortalLoginEvent = 'captive-portal-login';
+const kAbortCaptivePortalLoginEvent = 'captive-portal-login-abort';
+
+function URLFetcher(url, timeout) {
+  let self = this;
+  let xhr = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
+              .createInstance(Ci.nsIXMLHttpRequest);
+  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;
+  // 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 {
+        self.onredirectorerror(xhr.status);
+      }
+    }
+  };
+  xhr.send();
+  this._xhr = xhr;
+}
+
+URLFetcher.prototype = {
+  _isAborted: false,
+  ontimeout: function() {},
+  onerror: function() {},
+  abort: function() {
+    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: XPCOMUtils.generateQI([Ci.nsIHttpActivityOberver,
+                                           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:
+          timer.initWithCallback(this,
+                                 captivePortalDetector._pollingTime,
+                                 timer.TYPE_ONE_SHOT);
+          break;
+        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 =
+    Services.prefs.getCharPref('services.captivedetect.canonicalURL');
+  this._canonicalSiteExpectedContent =
+    Services.prefs.getCharPref('services.captivedetect.canonicalContent');
+  this._maxWaitingTime =
+    Services.prefs.getIntPref('services.captivedetect.maxWaitingTime');
+  this._pollingTime =
+    Services.prefs.getIntPref('services.captivedetect.pollingTime');
+  this._maxRetryCount =
+    Services.prefs.getIntPref('services.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, waitng for network connection established');
+}
+
+CaptivePortalDetector.prototype = {
+  classID:   kCAPTIVEPORTALDETECTOR_CID,
+  classInfo: XPCOMUtils.generateCI({classID: kCAPTIVEPORTALDETECTOR_CID,
+                                    contractID: kCAPTIVEPORTALDETECTOR_CONTRACTID,
+                                    classDescription: 'Captive Portal Detector',
+                                    interfaces: [Ci.nsICaptivePortalDetector]}),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalDetector]),
+
+  // nsICaptivePortalDetector
+  checkCaptivePortal: function checkCaptivePortal(aInterfaceName, aCallback) {
+    // Prevent multiple requests on a single network interface
+    if (this._interfaceNames[aInterfaceName]) {
+      throw Components.Exception('Do not allow multiple request on one interface: ' + aInterface);
+    }
+
+    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|');
+      return;
+    }
+
+    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 requestDone = this.executeCallback.bind(this, true);
+    urlFetcher.ontimeout = requestDone;
+    urlFetcher.onerror = requestDone;
+    urlFetcher.onsuccess = function (content) {
+      if (self.validateContent(content)) {
+        requestDone();
+      } 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 if (self._runningRequest.retryCount++ < self._maxRetryCount) {
+        debug('startDetection-retry: ' + self._runningRequest.retryCount);
+        self._startDetection();
+      } else {
+        requestDone();
+      }
+    };
+
+    this._runningRequest['urlFetcher'] = urlFetcher;
+  },
+
+  _startLogin: function _startLogin() {
+    let id = this._allocateRequestId();
+    let details = {
+      type: kOpenCaptivePortalLoginEvent,
+      id: id,
+      url: this._canonicalSiteURL,
+    };
+    this._loginObserver.attach();
+    this._runningRequest['eventId'] = id;
+    this._sendEvent(kOpenCaptivePortalLoginEvent, details);
+  },
+
+  executeCallback: function executeCallback(success) {
+    if (this._runningRequest) {
+      debug('callback executed');
+      if (this._runningRequest.hasOwnProperty('callback')) {
+        this._runningRequest.callback.complete(success);
+      }
+
+      // 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);
+    return (content === this._canonicalSiteExpectedContent);
+  },
+
+  _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;
+      }
+    }
+  },
+};
+
+let debug;
+if (DEBUG) {
+  debug = function (s) {
+    dump('-*- CaptivePortalDetector component: ' + s + '\n');
+  };
+} else {
+  debug = function (s) {};
+}
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([CaptivePortalDetector]);
new file mode 100644
--- /dev/null
+++ b/services/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 wstring 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 wstring 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 wstring 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 wstring ifname);
+};
new file mode 100644
--- /dev/null
+++ b/services/captivedetect/services-captivedetect.js
@@ -0,0 +1,9 @@
+/* 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/. */
+
+pref("services.captivedetect.canonicalURL", "http://localhost/test.html");
+pref("services.captivedetect.canonicalContent", "true");
+pref("services.captivedetect.maxWaitingTime", 5000);
+pref("services.captivedetect.pollingTime", 3000);
+pref("services.captivedetect.maxRetryCount", 5);
--- a/services/makefiles.sh
+++ b/services/makefiles.sh
@@ -7,16 +7,17 @@ add_makefiles "
   services/common/Makefile
   services/crypto/Makefile
   services/crypto/component/Makefile
   services/healthreport/Makefile
   services/datareporting/Makefile
   services/metrics/Makefile
   services/sync/Makefile
   services/sync/locales/Makefile
+  services/captivedetect/Makefile
 "
 
 if [ "$ENABLE_TESTS" ]; then
   add_makefiles "
     services/common/tests/Makefile
     services/crypto/tests/Makefile
     services/healthreport/tests/Makefile
     services/datareporting/tests/Makefile