Bug 1059001 - Part 3: Add encryption socket option. r=past draft
authorJ. Ryan Stinnett <jryans@gmail.com>
Wed, 03 Dec 2014 02:06:51 -0800
changeset 230563 0ae7c0e713b4eed921b542fe626b780800da8db6
parent 230562 3a66c96e8bef68dc597788245b34aea1c8a314f3
child 230564 22f3d20ba4a3e121642150b06cbbcf7b63d69fa9
push id166
push userjryans@gmail.com
push dateWed, 03 Dec 2014 10:07:04 +0000
reviewerspast
bugs1059001
milestone37.0a1
Bug 1059001 - Part 3: Add encryption socket option. r=past
b2g/chrome/content/devtools/debugger.js
browser/devtools/framework/connect/connect.js
browser/devtools/framework/toolbox-process-window.js
modules/libpref/init/all.js
toolkit/devtools/apps/tests/debugger-protocol-helper.js
toolkit/devtools/client/connection-manager.js
toolkit/devtools/client/dbg-client.jsm
toolkit/devtools/gcli/source/lib/gcli/connectors/rdp.js
toolkit/devtools/security/cert.js
toolkit/devtools/security/moz.build
toolkit/devtools/security/socket.js
toolkit/devtools/security/tests/unit/head_dbg.js
toolkit/devtools/security/tests/unit/test_cert.js
toolkit/devtools/security/tests/unit/test_encryption.js
toolkit/devtools/security/tests/unit/testactors.js
toolkit/devtools/security/tests/unit/xpcshell.ini
toolkit/devtools/transport/tests/unit/head_dbg.js
toolkit/devtools/transport/tests/unit/test_bulk_error.js
toolkit/devtools/transport/tests/unit/test_client_server_bulk.js
toolkit/devtools/transport/tests/unit/test_dbgsocket.js
toolkit/devtools/transport/tests/unit/test_dbgsocket_connection_drop.js
toolkit/devtools/transport/tests/unit/test_no_bulk.js
toolkit/devtools/transport/tests/unit/test_queue.js
toolkit/devtools/transport/tests/unit/test_transport_bulk.js
--- a/b2g/chrome/content/devtools/debugger.js
+++ b/b2g/chrome/content/devtools/debugger.js
@@ -178,16 +178,17 @@ let WiFiRemoteDebugger = {
     RemoteDebugger.initServer();
 
     try {
       debug("Starting WiFi debugger");
       this._listener = DebuggerServer.createListener();
       this._listener.portOrPath = -1 /* any available port */;
       this._listener.allowConnection = RemoteDebugger.prompt;
       this._listener.discoverable = true;
+      this._listener.encryption = true;
       this._listener.open();
       let port = this._listener.port;
       debug("Started WiFi debugger on " + port);
     } catch (e) {
       debug("Unable to start WiFi debugger server: " + e);
     }
   },
 
--- a/browser/devtools/framework/connect/connect.js
+++ b/browser/devtools/framework/connect/connect.js
@@ -36,47 +36,51 @@ window.addEventListener("DOMContentLoade
   }
 
   if (port) {
     document.getElementById("port").value = port;
   }
 
   let form = document.querySelector("#connection-form form");
   form.addEventListener("submit", function() {
-    window.submit();
+    window.submit().catch(e => {
+      Cu.reportError(e);
+      // Bug 921850: catch rare exception from DebuggerClient.socketConnect
+      showError("unexpected");
+    });
   });
 }, true);
 
 /**
  * Called when the "connect" button is clicked.
  */
-function submit() {
+let submit = Task.async(function*() {
   // Show the "connecting" screen
   document.body.classList.add("connecting");
 
   // Save the host/port values
   let host = document.getElementById("host").value;
   Services.prefs.setCharPref("devtools.debugger.remote-host", host);
 
   let port = document.getElementById("port").value;
   Services.prefs.setIntPref("devtools.debugger.remote-port", port);
 
   // Initiate the connection
-  let transport;
-  try {
-    transport = DebuggerClient.socketConnect(host, port);
-  } catch(e) {
-    // Bug 921850: catch rare exception from DebuggerClient.socketConnect
-    showError("unexpected");
-    return;
-  }
+  let transport = yield DebuggerClient.socketConnect({ host, port });
   gClient = new DebuggerClient(transport);
   let delay = Services.prefs.getIntPref("devtools.debugger.remote-timeout");
   gConnectionTimeout = setTimeout(handleConnectionTimeout, delay);
-  gClient.connect(onConnectionReady);
+  let response = yield clientConnect();
+  yield onConnectionReady(...response);
+});
+
+function clientConnect() {
+  let deferred = promise.defer();
+  gClient.connect((...args) => deferred.resolve(args));
+  return deferred.promise;
 }
 
 /**
  * Connection is ready. List actors and build buttons.
  */
 let onConnectionReady = Task.async(function*(aType, aTraits) {
   clearTimeout(gConnectionTimeout);
 
--- a/browser/devtools/framework/toolbox-process-window.js
+++ b/browser/devtools/framework/toolbox-process-window.js
@@ -7,61 +7,62 @@ const { classes: Cc, interfaces: Ci, uti
 
 let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
 let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
 let { DebuggerClient } =
   Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {});
 let { ViewHelpers } =
   Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {});
+let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 
 /**
  * Shortcuts for accessing various debugger preferences.
  */
 let Prefs = new ViewHelpers.Prefs("devtools.debugger", {
   chromeDebuggingHost: ["Char", "chrome-debugging-host"],
   chromeDebuggingPort: ["Int", "chrome-debugging-port"]
 });
 
 let gToolbox, gClient;
 
-function connect() {
+let connect = Task.async(function*() {
   window.removeEventListener("load", connect);
   // Initiate the connection
-  let transport = DebuggerClient.socketConnect(
-    Prefs.chromeDebuggingHost,
-    Prefs.chromeDebuggingPort
-  );
+  let transport = yield DebuggerClient.socketConnect({
+    host: Prefs.chromeDebuggingHost,
+    port: Prefs.chromeDebuggingPort
+  });
   gClient = new DebuggerClient(transport);
   gClient.connect(() => {
     let addonID = getParameterByName("addonID");
 
     if (addonID) {
       gClient.listAddons(({addons}) => {
         let addonActor = addons.filter(addon => addon.id === addonID).pop();
         openToolbox(addonActor);
       });
     } else {
       gClient.listTabs(openToolbox);
     }
   });
-}
+});
 
 // Certain options should be toggled since we can assume chrome debugging here
 function setPrefDefaults() {
   Services.prefs.setBoolPref("devtools.inspector.showUserAgentStyles", true);
   Services.prefs.setBoolPref("devtools.profiler.ui.show-platform-data", true);
   Services.prefs.setBoolPref("browser.devedition.theme.showCustomizeButton", false);
 }
 
 window.addEventListener("load", function() {
   let cmdClose = document.getElementById("toolbox-cmd-close");
   cmdClose.addEventListener("command", onCloseCommand);
   setPrefDefaults();
-  connect();
+  connect().catch(Cu.reportError);
 });
 
 function onCloseCommand(event) {
   window.close();
 }
 
 function openToolbox(form) {
   let options = {
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -790,16 +790,18 @@ pref("devtools.dump.emit", false);
 // Disable device discovery logging
 pref("devtools.discovery.log", false);
 // Disable scanning for DevTools devices via WiFi
 pref("devtools.remote.wifi.scan", false);
 // Hide UI options for controlling device visibility over WiFi
 // N.B.: This does not set whether the device can be discovered via WiFi, only
 // whether the UI control to make such a choice is shown to the user
 pref("devtools.remote.wifi.visible", false);
+// Client must complete TLS handshake within this window (ms)
+pref("devtools.remote.tls-handshake-timeout", 10000);
 
 // view source
 pref("view_source.syntax_highlight", true);
 pref("view_source.wrap_long_lines", false);
 pref("view_source.editor.external", false);
 pref("view_source.editor.path", "");
 // allows to add further arguments to the editor; use the %LINE% placeholder
 // for jumping to a specific line (e.g. "/line:%LINE%" or "--goto %LINE%")
--- a/toolkit/devtools/apps/tests/debugger-protocol-helper.js
+++ b/toolkit/devtools/apps/tests/debugger-protocol-helper.js
@@ -23,18 +23,22 @@ function connect(onDone) {
     let settingsService = Cc["@mozilla.org/settingsService;1"].getService(Ci.nsISettingsService);
     settingsService.createLock().set("devtools.debugger.remote-enabled", true, null);
     // We can't use `set` callback as it is fired before shell.js code listening for this setting
     // is actually called. Same thing applies to mozsettings-changed obs notification.
     // So listen to a custom event until bug 942756 lands
     let observer = {
       observe: function (subject, topic, data) {
         Services.obs.removeObserver(observer, "debugger-server-started");
-        let transport = DebuggerClient.socketConnect("127.0.0.1", 6000);
-        startClient(transport, onDone);
+        DebuggerClient.socketConnect({
+          host: "127.0.0.1",
+          port: 6000
+        }).then(transport => {
+          startClient(transport, onDone);
+        }, e => dump("Connection failed: " + e + "\n"));
       }
     };
     Services.obs.addObserver(observer, "debugger-server-started", false);
   } else {
     // Initialize a loopback remote protocol connection
     DebuggerServer.init();
     // We need to register browser actors to have `listTabs` working
     // and also have a root actor
--- a/toolkit/devtools/client/connection-manager.js
+++ b/toolkit/devtools/client/connection-manager.js
@@ -4,20 +4,23 @@
  * 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 {Cc, Ci, Cu} = require("chrome");
 const {setTimeout, clearTimeout} = require('sdk/timers');
 const EventEmitter = require("devtools/toolkit/event-emitter");
+const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
 Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
+DevToolsUtils.defineLazyModuleGetter(this, "Task",
+  "resource://gre/modules/Task.jsm");
 
 /**
  * Connection Manager.
  *
  * To use this module:
  * const {ConnectionManager} = require("devtools/client/connection-manager");
  *
  * # ConnectionManager
@@ -43,17 +46,18 @@ Cu.import("resource://gre/modules/devtoo
  *  . connect(transport)    Connect via transport. Expect a "connecting" event.
  *  . disconnect()          Disconnect if connected. Expect a "disconnecting" event
  *
  * Properties:
  *  . host                  IP address or hostname
  *  . port                  Port
  *  . logs                  Current logs. "newlog" event notifies new available logs
  *  . store                 Reference to a local data store (see below)
- *  . keepConnecting        Should the connection keep trying connecting
+ *  . keepConnecting        Should the connection keep trying to connect?
+ *  . encryption            Should the connection be encrypted?
  *  . status                Connection status:
  *                            Connection.Status.CONNECTED
  *                            Connection.Status.DISCONNECTED
  *                            Connection.Status.CONNECTING
  *                            Connection.Status.DISCONNECTING
  *                            Connection.Status.DESTROYED
  *
  * Events (as in event-emitter.js):
@@ -108,16 +112,17 @@ function Connection(host, port) {
   this.uid = ++lastID;
   this.host = host;
   this.port = port;
   this._setStatus(Connection.Status.DISCONNECTED);
   this._onDisconnected = this._onDisconnected.bind(this);
   this._onConnected = this._onConnected.bind(this);
   this._onTimeout = this._onTimeout.bind(this);
   this.keepConnecting = false;
+  this.encryption = false;
 }
 
 Connection.Status = {
   CONNECTED: "connected",
   DISCONNECTED: "disconnected",
   CONNECTING: "connecting",
   DISCONNECTING: "disconnecting",
   DESTROYED: "destroyed",
@@ -217,40 +222,48 @@ Connection.prototype = {
     this.keepConnecting = false;
     if (this._client) {
       this._client.close();
       this._client = null;
     }
     this._setStatus(Connection.Status.DESTROYED);
   },
 
-  _clientConnect: function () {
-    let transport;
+  _getTransport: Task.async(function*() {
     if (this._customTransport) {
-      transport = this._customTransport;
-    } else {
-      if (!this.host) {
-        transport = DebuggerServer.connectPipe();
-      } else {
-        try {
-          transport = DebuggerClient.socketConnect(this.host, this.port);
-        } catch (e) {
-          // In some cases, especially on Mac, the openOutputStream call in
-          // DebuggerClient.socketConnect may throw NS_ERROR_NOT_INITIALIZED.
-          // It occurs when we connect agressively to the simulator,
-          // and keep trying to open a socket to the server being started in
-          // the simulator.
-          this._onDisconnected();
-          return;
-        }
+      return this._customTransport;
+    }
+    if (!this.host) {
+      return DebuggerServer.connectPipe();
+    }
+    let transport = yield DebuggerClient.socketConnect({
+      host: this.host,
+      port: this.port,
+      encryption: this.encryption
+    });
+    return transport;
+  }),
+
+  _clientConnect: function () {
+    this._getTransport().then(transport => {
+      if (!transport) {
+        return;
       }
-    }
-    this._client = new DebuggerClient(transport);
-    this._client.addOneTimeListener("closed", this._onDisconnected);
-    this._client.connect(this._onConnected);
+      this._client = new DebuggerClient(transport);
+      this._client.addOneTimeListener("closed", this._onDisconnected);
+      this._client.connect(this._onConnected);
+    }, e => {
+      console.error(e);
+      // In some cases, especially on Mac, the openOutputStream call in
+      // DebuggerClient.socketConnect may throw NS_ERROR_NOT_INITIALIZED.
+      // It occurs when we connect agressively to the simulator,
+      // and keep trying to open a socket to the server being started in
+      // the simulator.
+      this._onDisconnected();
+    });
   },
 
   get status() {
     return this._status
   },
 
   _setStatus: function(value) {
     if (this._status && this._status == value)
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -367,19 +367,19 @@ DebuggerClient.Argument = function (aPos
 DebuggerClient.Argument.prototype.getArgument = function (aParams) {
   if (!(this.position in aParams)) {
     throw new Error("Bad index into params: " + this.position);
   }
   return aParams[this.position];
 };
 
 // Expose this to save callers the trouble of importing DebuggerSocket
-DebuggerClient.socketConnect = function(host, port) {
+DebuggerClient.socketConnect = function(options) {
   // Defined here instead of just copying the function to allow lazy-load
-  return DebuggerSocket.connect(host, port);
+  return DebuggerSocket.connect(options);
 };
 
 DebuggerClient.prototype = {
   /**
    * Connect to the server and start exchanging protocol messages.
    *
    * @param aOnConnected function
    *        If specified, will be called when the greeting packet is
--- a/toolkit/devtools/gcli/source/lib/gcli/connectors/rdp.js
+++ b/toolkit/devtools/gcli/source/lib/gcli/connectors/rdp.js
@@ -14,16 +14,17 @@
  * limitations under the License.
  */
 
 'use strict';
 
 var Cu = require('chrome').Cu;
 
 var DebuggerClient = Cu.import('resource://gre/modules/devtools/dbg-client.jsm', {}).DebuggerClient;
+var { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 
 var Promise = require('../util/promise').Promise;
 var Connection = require('./connectors').Connection;
 
 /**
  * What port should we use by default?
  */
 Object.defineProperty(exports, 'defaultPort', {
@@ -56,36 +57,40 @@ exports.items = [
  */
 function RdpConnection(url) {
   throw new Error('Use RdpConnection.create');
 }
 
 /**
  * Asynchronous construction
  */
-RdpConnection.create = function(url) {
+RdpConnection.create = Task.async(function*(url) {
   this.host = url;
   this.port = undefined; // TODO: Split out the port number
 
   this.requests = {};
   this.nextRequestId = 0;
 
   this._emit = this._emit.bind(this);
 
+  let transport = yield DebuggerClient.socketConnect({
+    host: this.host,
+    port: this.port
+  });
+
   return new Promise(function(resolve, reject) {
-    this.transport = DebuggerClient.socketConnect(this.host, this.port);
-    this.client = new DebuggerClient(this.transport);
+    this.client = new DebuggerClient(transport);
     this.client.connect(function() {
       this.client.listTabs(function(response) {
         this.actor = response.gcliActor;
         resolve();
       }.bind(this));
     }.bind(this));
   }.bind(this));
-};
+});
 
 RdpConnection.prototype = Object.create(Connection.prototype);
 
 RdpConnection.prototype.call = function(command, data) {
   return new Promise(function(resolve, reject) {
     var request = { to: this.actor, type: command, data: data };
 
     this.client.request(request, function(response) {
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/security/cert.js
@@ -0,0 +1,64 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+let { Ci, Cc } = require("chrome");
+let promise = require("promise");
+let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
+DevToolsUtils.defineLazyGetter(this, "localCertService", () => {
+  return Cc["@mozilla.org/security/local-cert-service;1"]
+         .getService(Ci.nsILocalCertService);
+});
+
+const localCertName = "devtools";
+
+exports.local = {
+
+  /**
+   * Get or create a new self-signed X.509 cert to represent this device for
+   * DevTools purposes over a secure transport, like TLS.
+   *
+   * The cert is stored permanently in the profile's key store after first use,
+   * and is valid for 1 year.  If an expired or otherwise invalid cert is found,
+   * it is removed and a new one is made.
+   *
+   * @return promise
+   */
+  getOrCreate() {
+    let deferred = promise.defer();
+    localCertService.getOrCreateCert(localCertName, {
+      handleCert: function(cert, rv) {
+        if (rv) {
+          deferred.reject(rv);
+          return;
+        }
+        deferred.resolve(cert);
+      }
+    });
+    return deferred.promise;
+  },
+
+  /**
+   * Remove the DevTools self-signed X.509 cert for this device.
+   *
+   * @return promise
+   */
+  remove() {
+    let deferred = promise.defer();
+    localCertService.removeCert(localCertName, {
+      handleCert: function(rv) {
+        if (rv) {
+          deferred.reject(rv);
+          return;
+        }
+        deferred.resolve();
+      }
+    });
+    return deferred.promise;
+  }
+
+};
--- a/toolkit/devtools/security/moz.build
+++ b/toolkit/devtools/security/moz.build
@@ -16,10 +16,11 @@ UNIFIED_SOURCES += [
     'LocalCertService.cpp',
 ]
 
 FAIL_ON_WARNINGS = True
 
 FINAL_LIBRARY = 'xul'
 
 EXTRA_JS_MODULES.devtools.security += [
+    'cert.js',
     'socket.js',
 ]
--- a/toolkit/devtools/security/socket.js
+++ b/toolkit/devtools/security/socket.js
@@ -2,76 +2,181 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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";
 
 let { Ci, Cc, CC, Cr } = require("chrome");
+
+// Ensure PSM is initialized to support TLS sockets
+Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+
 let Services = require("Services");
+let promise = require("promise");
 let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
-let { dumpn } = DevToolsUtils;
+let { dumpn, dumpv } = DevToolsUtils;
 loader.lazyRequireGetter(this, "DebuggerTransport",
   "devtools/toolkit/transport/transport", true);
 loader.lazyRequireGetter(this, "DebuggerServer",
   "devtools/server/main", true);
 loader.lazyRequireGetter(this, "discovery",
   "devtools/toolkit/discovery/discovery");
-
-DevToolsUtils.defineLazyGetter(this, "ServerSocket", () => {
-  return CC("@mozilla.org/network/server-socket;1",
-            "nsIServerSocket",
-            "initSpecialConnection");
-});
-
-DevToolsUtils.defineLazyGetter(this, "UnixDomainServerSocket", () => {
-  return CC("@mozilla.org/network/server-socket;1",
-            "nsIServerSocket",
-            "initWithFilename");
-});
+loader.lazyRequireGetter(this, "cert",
+  "devtools/toolkit/security/cert");
+loader.lazyRequireGetter(this, "setTimeout", "Timer", true);
+loader.lazyRequireGetter(this, "clearTimeout", "Timer", true);
 
 DevToolsUtils.defineLazyGetter(this, "nsFile", () => {
   return CC("@mozilla.org/file/local;1", "nsIFile", "initWithPath");
 });
 
 DevToolsUtils.defineLazyGetter(this, "socketTransportService", () => {
   return Cc["@mozilla.org/network/socket-transport-service;1"]
          .getService(Ci.nsISocketTransportService);
 });
 
+DevToolsUtils.defineLazyGetter(this, "certOverrideService", () => {
+  return Cc["@mozilla.org/security/certoverride;1"]
+         .getService(Ci.nsICertOverrideService);
+});
+
+DevToolsUtils.defineLazyGetter(this, "nssErrorsService", () => {
+  return Cc["@mozilla.org/nss_errors_service;1"]
+         .getService(Ci.nsINSSErrorsService);
+});
+
+DevToolsUtils.defineLazyModuleGetter(this, "Task",
+  "resource://gre/modules/Task.jsm");
+
 const DBG_STRINGS_URI = "chrome://global/locale/devtools/debugger.properties";
 
+let DebuggerSocket = {};
+
 /**
- * Connects to a debugger server socket and returns a DebuggerTransport.
+ * Connects to a debugger server socket.
  *
  * @param host string
  *        The host name or IP address of the debugger server.
  * @param port number
  *        The port number of the debugger server.
+ * @param encryption boolean (optional)
+ *        Whether the server requires encryption.  Defaults to false.
+ * @return promise
+ *         Resolved to a DebuggerTransport instance.
  */
-function socketConnect(host, port) {
-  let s = socketTransportService.createTransport(null, 0, host, port, null);
+DebuggerSocket.connect = Task.async(function*({ host, port, encryption }) {
+  {
+    // _attemptConnect only opens the streams.  Any failures at that stage
+    // aborts the connection process immedidately.
+    let { s, input, output } = _attemptConnect({ host, port, encryption });
+
+    // Check if the input stream is alive.  If encryption is enabled, we need to
+    // watch out for cert errors by testing the input stream.
+    let { alive, certError } = yield _isInputAlive(input);
+    dumpv("Server cert accepted? " + !certError);
+    if (alive) {
+      return new DebuggerTransport(input, output);
+    }
+
+    // If the server cert failed validation, store a temporary override and make
+    // a second attempt.
+    if (encryption && certError) {
+      _storeCertOverride(s, host, port);
+    } else {
+      throw new Error("Connection failed");
+    }
+  }
+
+  {
+    let { input, output } = _attemptConnect({ host, port, encryption });
+    let { alive, certError } = yield _isInputAlive(input);
+    dumpv("Server cert accepted? " + !certError);
+    if (alive) {
+      return new DebuggerTransport(input, output);
+    }
+    throw new Error("Connection failed even after cert override");
+  }
+});
+
+/**
+ * Try to connect to a remote server socket.
+ * If successsful, the socket transport and its opened streams are returned.
+ */
+function _attemptConnect({ host, port, encryption }) {
+  let s;
+  if (encryption) {
+    s = socketTransportService.createTransport(["ssl"], 1, host, port, null);
+  } else {
+    s = socketTransportService.createTransport(null, 0, host, port, null);
+  }
   // By default the CONNECT socket timeout is very long, 65535 seconds,
   // so that if we race to be in CONNECT state while the server socket is still
   // initializing, the connection is stuck in connecting state for 18.20 hours!
   s.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 2);
 
   // openOutputStream may throw NS_ERROR_NOT_INITIALIZED if we hit some race
   // where the nsISocketTransport gets shutdown in between its instantiation and
   // the call to this method.
-  let transport;
+  let input;
+  let output;
   try {
-    transport = new DebuggerTransport(s.openInputStream(0, 0, 0),
-                                      s.openOutputStream(0, 0, 0));
+    input = s.openInputStream(0, 0, 0);
+    output = s.openOutputStream(0, 0, 0);
   } catch(e) {
     DevToolsUtils.reportException("socketConnect", e);
     throw e;
   }
-  return transport;
+
+  return { s, input, output };
+}
+
+/**
+ * Check if the input stream is alive.  For an encrypted connection, it may not
+ * be if the client refuses the server's cert.  A cert error is expected on
+ * first connection to a new host because the cert is self-signed.
+ */
+function _isInputAlive(input) {
+  let deferred = promise.defer();
+  input.asyncWait({
+    onInputStreamReady(stream) {
+      try {
+        stream.available();
+        deferred.resolve({ alive: true });
+      } catch (e) {
+        try {
+          // getErrorClass may throw if you pass a non-NSS error
+          let errorClass = nssErrorsService.getErrorClass(e.result);
+          if (errorClass === Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT) {
+            deferred.resolve({ certError: true });
+          } else {
+            deferred.reject(e);
+          }
+        } catch (nssErr) {
+          deferred.reject(e);
+        }
+      }
+    }
+  }, 0, 0, Services.tm.currentThread);
+  return deferred.promise;
+}
+
+/**
+ * To allow the connection to proceed with self-signed cert, we store a cert
+ * override.  This implies that we take on the burden of authentication for
+ * these connections.
+ */
+function _storeCertOverride(s, host, port) {
+  let cert = s.securityInfo.QueryInterface(Ci.nsISSLStatusProvider)
+              .SSLStatus.QueryInterface(Ci.nsISSLStatus).serverCert;
+  let overrideBits = Ci.nsICertOverrideService.ERROR_UNTRUSTED |
+                     Ci.nsICertOverrideService.ERROR_MISMATCH;
+  certOverrideService.rememberValidityOverride(host, port, cert, overrideBits,
+                                               true /* temporary */);
 }
 
 /**
  * Creates a new socket listener for remote connections to the DebuggerServer.
  * This helps contain and organize the parts of the server that may differ or
  * are particular to one given listener mechanism vs. another.
  */
 function SocketListener() {}
@@ -129,16 +234,21 @@ SocketListener.prototype = {
 
   /**
    * Controls whether this listener is announced via the service discovery
    * mechanism.
    */
   discoverable: false,
 
   /**
+   * Controls whether this listener's transport uses encryption.
+   */
+  encryption: false,
+
+  /**
    * Validate that all options have been set to a supported configuration.
    */
   _validateOptions: function() {
     if (this.portOrPath === null) {
       throw new Error("Must set a port / path to listen on.");
     }
     if (this.discoverable && !Number(this.portOrPath)) {
       throw new Error("Discovery only supported for TCP sockets.");
@@ -153,41 +263,63 @@ SocketListener.prototype = {
     DebuggerServer._addListener(this);
 
     let flags = Ci.nsIServerSocket.KeepWhenOffline;
     // A preference setting can force binding on the loopback interface.
     if (Services.prefs.getBoolPref("devtools.debugger.force-local")) {
       flags |= Ci.nsIServerSocket.LoopbackOnly;
     }
 
-    try {
+    let self = this;
+    return Task.spawn(function*() {
       let backlog = 4;
-      let port = Number(this.portOrPath);
+      let port = Number(self.portOrPath);
+      self._socket = self._createSocketInstance();
       if (port) {
-        this._socket = new ServerSocket(port, flags, backlog);
+        self._socket.initSpecialConnection(port, flags, backlog);
       } else {
-        let file = nsFile(this.portOrPath);
-        if (file.exists())
+        let file = nsFile(self.portOrPath);
+        if (file.exists()) {
           file.remove(false);
-        this._socket = new UnixDomainServerSocket(file, parseInt("666", 8),
-                                                  backlog);
+        }
+        self._socket.initWithFilename(file, parseInt("666", 8), backlog);
       }
-      this._socket.asyncListen(this);
-    } catch (e) {
+      yield self._setAdditionalSocketOptions();
+      self._socket.asyncListen(self);
+      dumpn("Socket listening on port: " + self.port || self.portOrPath);
+    }).then(() => {
+      if (this.discoverable && this.port) {
+        discovery.addService("devtools", { port: this.port });
+      }
+    }, e => {
       dumpn("Could not start debugging listener on '" + this.portOrPath +
             "': " + e);
       this.close();
-      throw Cr.NS_ERROR_NOT_AVAILABLE;
-    }
+    });
+  },
 
-    if (this.discoverable && this.port) {
-      discovery.addService("devtools", { port: this.port });
+  _createSocketInstance: function() {
+    if (this.encryption) {
+      return Cc["@mozilla.org/network/tls-server-socket;1"]
+             .createInstance(Ci.nsITLSServerSocket);
     }
+    return Cc["@mozilla.org/network/server-socket;1"]
+           .createInstance(Ci.nsIServerSocket);
   },
 
+  _setAdditionalSocketOptions: Task.async(function*() {
+    if (this.encryption) {
+      this._socket.serverCert = yield cert.local.getOrCreate();
+      this._socket.setSessionCache(false);
+      this._socket.setSessionTickets(false);
+      let requestCert = Ci.nsITLSServerSocket.REQUEST_NEVER;
+      this._socket.setRequestClientCertificate(requestCert);
+    }
+  }),
+
   /**
    * Closes the SocketListener.  Notifies the server to remove the listener from
    * the set of active SocketListeners.
    */
   close: function() {
     if (this.discoverable && this.port) {
       discovery.removeService("devtools");
     }
@@ -208,16 +340,19 @@ SocketListener.prototype = {
     }
     return this._socket.port;
   },
 
   // nsIServerSocketListener implementation
 
   onSocketAccepted:
   DevToolsUtils.makeInfallible(function(socket, socketTransport) {
+    if (this.encryption) {
+      new SecurityObserver(socketTransport);
+    }
     if (Services.prefs.getBoolPref("devtools.debugger.prompt-connection") &&
         !this.allowConnection()) {
       return;
     }
     dumpn("New debugging connection on " +
           socketTransport.host + ":" + socketTransport.port);
 
     let input = socketTransport.openInputStream(0, 0, 0);
@@ -227,18 +362,59 @@ SocketListener.prototype = {
   }, "SocketListener.onSocketAccepted"),
 
   onStopListening: function(socket, status) {
     dumpn("onStopListening, status: " + status);
   }
 
 };
 
-// TODO: These high-level entry points will branch based on TLS vs. bare TCP as
-// part of bug 1059001.
-exports.DebuggerSocket = {
-  createListener() {
-    return new SocketListener();
+// Client must complete TLS handshake within this window (ms)
+loader.lazyGetter(this, "HANDSHAKE_TIMEOUT", () => {
+  return Services.prefs.getIntPref("devtools.remote.tls-handshake-timeout");
+});
+
+function SecurityObserver(socketTransport) {
+  this.socketTransport = socketTransport;
+  let connectionInfo = socketTransport.securityInfo
+                       .QueryInterface(Ci.nsITLSServerConnectionInfo);
+  connectionInfo.setSecurityObserver(this);
+  this._handshakeTimeout = setTimeout(this._onHandshakeTimeout.bind(this),
+                                      HANDSHAKE_TIMEOUT);
+}
+
+SecurityObserver.prototype = {
+
+  _onHandshakeTimeout() {
+    dumpv("Client failed to complete handshake");
+    this.socketTransport.close(Cr.NS_ERROR_NET_TIMEOUT);
   },
-  connect(host, port) {
-    return socketConnect(host, port);
+
+  // nsITLSServerSecurityObserver implementation
+  onHandshakeDone(socket, clientStatus) {
+    clearTimeout(this._handshakeTimeout);
+    dumpv("TLS version:    " + clientStatus.tlsVersionUsed.toString(16));
+    dumpv("TLS cipher:     " + clientStatus.cipherName);
+    dumpv("TLS key length: " + clientStatus.keyLength);
+    dumpv("TLS MAC length: " + clientStatus.macLength);
+    /*
+     * TODO: These rules should be really be set on the TLS socket directly, but
+     * this would need more platform work to expose it via XPCOM.
+     *
+     * Server *will* send hello packet when any rules below are not met, but the
+     * socket then closes after that.
+     *
+     * Enforcing cipher suites here would be a bad idea, as we want TLS
+     * cipher negotiation to work correctly.  The server already allows only
+     * Gecko's normal set of cipher suites.
+     */
+    if (clientStatus.tlsVersionUsed < Ci.nsITLSClientStatus.TLS_VERSION_1_2) {
+      this.socketTransport.close(Cr.NS_ERROR_CONNECTION_REFUSED);
+    }
   }
+
 };
+
+DebuggerSocket.createListener = function() {
+  return new SocketListener();
+};
+
+exports.DebuggerSocket = DebuggerSocket;
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/security/tests/unit/head_dbg.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+const CC = Components.Constructor;
+
+const { devtools } =
+  Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+const { Promise: promise } =
+  Cu.import("resource://gre/modules/Promise.jsm", {});
+const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
+
+const Services = devtools.require("Services");
+const DevToolsUtils = devtools.require("devtools/toolkit/DevToolsUtils.js");
+
+// We do not want to log packets by default, because in some tests,
+// we can be sending large amounts of data. The test harness has
+// trouble dealing with logging all the data, and we end up with
+// intermittent time outs (e.g. bug 775924).
+// Services.prefs.setBoolPref("devtools.debugger.log", true);
+// Services.prefs.setBoolPref("devtools.debugger.log.verbose", true);
+// Enable remote debugging for the relevant tests.
+Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
+// Fast timeout for TLS tests
+Services.prefs.setIntPref("devtools.remote.tls-handshake-timeout", 1000);
+
+function tryImport(url) {
+  try {
+    Cu.import(url);
+  } catch (e) {
+    dump("Error importing " + url + "\n");
+    dump(DevToolsUtils.safeErrorString(e) + "\n");
+    throw e;
+  }
+}
+
+tryImport("resource://gre/modules/devtools/dbg-server.jsm");
+tryImport("resource://gre/modules/devtools/dbg-client.jsm");
+
+// Convert an nsIScriptError 'aFlags' value into an appropriate string.
+function scriptErrorFlagsToKind(aFlags) {
+  var kind;
+  if (aFlags & Ci.nsIScriptError.warningFlag)
+    kind = "warning";
+  if (aFlags & Ci.nsIScriptError.exceptionFlag)
+    kind = "exception";
+  else
+    kind = "error";
+
+  if (aFlags & Ci.nsIScriptError.strictFlag)
+    kind = "strict " + kind;
+
+  return kind;
+}
+
+// Register a console listener, so console messages don't just disappear
+// into the ether.
+let errorCount = 0;
+let listener = {
+  observe: function (aMessage) {
+    errorCount++;
+    try {
+      // If we've been given an nsIScriptError, then we can print out
+      // something nicely formatted, for tools like Emacs to pick up.
+      var scriptError = aMessage.QueryInterface(Ci.nsIScriptError);
+      dump(aMessage.sourceName + ":" + aMessage.lineNumber + ": " +
+           scriptErrorFlagsToKind(aMessage.flags) + ": " +
+           aMessage.errorMessage + "\n");
+      var string = aMessage.errorMessage;
+    } catch (x) {
+      // Be a little paranoid with message, as the whole goal here is to lose
+      // no information.
+      try {
+        var string = "" + aMessage.message;
+      } catch (x) {
+        var string = "<error converting error message to string>";
+      }
+    }
+
+    // Make sure we exit all nested event loops so that the test can finish.
+    while (DebuggerServer.xpcInspector.eventLoopNestLevel > 0) {
+      DebuggerServer.xpcInspector.exitNestedEventLoop();
+    }
+
+    // Throw in most cases, but ignore the "strict" messages
+    if (!(aMessage.flags & Ci.nsIScriptError.strictFlag)) {
+      do_throw("head_dbg.js got console message: " + string + "\n");
+    }
+  }
+};
+
+let consoleService = Cc["@mozilla.org/consoleservice;1"]
+                     .getService(Ci.nsIConsoleService);
+consoleService.registerListener(listener);
+
+/**
+ * Initialize the testing debugger server.
+ */
+function initTestDebuggerServer() {
+  DebuggerServer.registerModule("xpcshell-test/testactors");
+  DebuggerServer.init();
+}
--- a/toolkit/devtools/security/tests/unit/test_cert.js
+++ b/toolkit/devtools/security/tests/unit/test_cert.js
@@ -1,17 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-const { utils: Cu, classes: Cc, interfaces: Ci } = Components;
-
-const { Promise: promise } =
-  Cu.import("resource://gre/modules/Promise.jsm", {});
 const certService = Cc["@mozilla.org/security/local-cert-service;1"]
                     .getService(Ci.nsILocalCertService);
 
 const gNickname = "devtools";
 
 function run_test() {
   // Need profile dir to store the key / cert
   do_get_profile();
@@ -44,17 +40,17 @@ function removeCert(nickname) {
       }
       deferred.resolve();
     }
   });
   return deferred.promise;
 }
 
 add_task(function*() {
-  // No master password, so prompt required here
+  // No master password, so no prompt required here
   ok(!certService.loginPromptRequired);
 
   let certA = yield getOrCreateCert(gNickname);
   equal(certA.nickname, gNickname);
 
   // Getting again should give the same cert
   let certB = yield getOrCreateCert(gNickname);
   equal(certB.nickname, gNickname);
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/security/tests/unit/test_encryption.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function run_test() {
+  // Need profile dir to store the key / cert
+  do_get_profile();
+  // Ensure PSM is initialized
+  Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+  run_next_test();
+}
+
+function connectClient(client) {
+  let deferred = promise.defer();
+  client.connect(() => {
+    client.listTabs(deferred.resolve);
+  });
+  return deferred.promise;
+}
+
+add_task(function*() {
+  initTestDebuggerServer();
+});
+
+// Client w/ encryption connects successfully to server w/ encryption
+add_task(function*() {
+  equal(DebuggerServer.listeningSockets, 0, "0 listening sockets");
+
+  let listener = DebuggerServer.createListener();
+  ok(listener, "Socket listener created");
+  listener.portOrPath = -1 /* any available port */;
+  listener.allowConnection = () => true;
+  listener.encryption = true;
+  yield listener.open();
+  equal(DebuggerServer.listeningSockets, 1, "1 listening socket");
+
+  let transport = yield DebuggerClient.socketConnect({
+    host: "127.0.0.1",
+    port: listener.port,
+    encryption: true
+  });
+  ok(transport, "Client transport created");
+
+  let client = new DebuggerClient(transport);
+  yield connectClient(client);
+
+  // Send a message the server will echo back
+  let message = "secrets";
+  let reply = yield client.request({
+    to: "root",
+    type: "echo",
+    message
+  });
+  equal(reply.message, message, "Encrypted echo matches");
+
+  listener.close();
+  equal(DebuggerServer.listeningSockets, 0, "0 listening sockets");
+});
+
+// Client w/o encryption fails to connect to server w/ encryption
+add_task(function*() {
+  equal(DebuggerServer.listeningSockets, 0, "0 listening sockets");
+
+  let listener = DebuggerServer.createListener();
+  ok(listener, "Socket listener created");
+  listener.portOrPath = -1 /* any available port */;
+  listener.allowConnection = () => true;
+  listener.encryption = true;
+  yield listener.open();
+  equal(DebuggerServer.listeningSockets, 1, "1 listening socket");
+
+  try {
+    yield DebuggerClient.socketConnect({
+      host: "127.0.0.1",
+      port: listener.port
+      // encryption: false is the default
+    });
+  } catch(e) {
+    ok(true, "Client failed to connect as expected");
+    return;
+  }
+
+  do_throw("Connection unexpectedly succeeded");
+});
+
+add_task(function*() {
+  DebuggerServer.destroy();
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/security/tests/unit/testactors.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { ActorPool, appendExtraActors, createExtraActors } =
+  require("devtools/server/actors/common");
+const { RootActor } = require("devtools/server/actors/root");
+const { ThreadActor } = require("devtools/server/actors/script");
+const { DebuggerServer } = require("devtools/server/main");
+const promise = require("promise");
+
+var gTestGlobals = [];
+DebuggerServer.addTestGlobal = function(aGlobal) {
+  gTestGlobals.push(aGlobal);
+};
+
+// A mock tab list, for use by tests. This simply presents each global in
+// gTestGlobals as a tab, and the list is fixed: it never calls its
+// onListChanged handler.
+//
+// As implemented now, we consult gTestGlobals when we're constructed, not
+// when we're iterated over, so tests have to add their globals before the
+// root actor is created.
+function TestTabList(aConnection) {
+  this.conn = aConnection;
+
+  // An array of actors for each global added with
+  // DebuggerServer.addTestGlobal.
+  this._tabActors = [];
+
+  // A pool mapping those actors' names to the actors.
+  this._tabActorPool = new ActorPool(aConnection);
+
+  for (let global of gTestGlobals) {
+    let actor = new TestTabActor(aConnection, global);
+    actor.selected = false;
+    this._tabActors.push(actor);
+    this._tabActorPool.addActor(actor);
+  }
+  if (this._tabActors.length > 0) {
+    this._tabActors[0].selected = true;
+  }
+
+  aConnection.addActorPool(this._tabActorPool);
+}
+
+TestTabList.prototype = {
+  constructor: TestTabList,
+  getList: function () {
+    return promise.resolve([tabActor for (tabActor of this._tabActors)]);
+  }
+};
+
+function createRootActor(aConnection) {
+  let root = new RootActor(aConnection, {
+    tabList: new TestTabList(aConnection),
+    globalActorFactories: DebuggerServer.globalActorFactories
+  });
+  root.applicationType = "xpcshell-tests";
+  return root;
+}
+
+function TestTabActor(aConnection, aGlobal) {
+  this.conn = aConnection;
+  this._global = aGlobal;
+  this._threadActor = new ThreadActor(this, this._global);
+  this.conn.addActor(this._threadActor);
+  this._attached = false;
+  this._extraActors = {};
+}
+
+TestTabActor.prototype = {
+  constructor: TestTabActor,
+  actorPrefix: "TestTabActor",
+
+  get window() {
+    return { wrappedJSObject: this._global };
+  },
+
+  get url() {
+    return this._global.__name;
+  },
+
+  form: function() {
+    let response = { actor: this.actorID, title: this._global.__name };
+
+    // Walk over tab actors added by extensions and add them to a new ActorPool.
+    let actorPool = new ActorPool(this.conn);
+    this._createExtraActors(DebuggerServer.tabActorFactories, actorPool);
+    if (!actorPool.isEmpty()) {
+      this._tabActorPool = actorPool;
+      this.conn.addActorPool(this._tabActorPool);
+    }
+
+    this._appendExtraActors(response);
+
+    return response;
+  },
+
+  onAttach: function(aRequest) {
+    this._attached = true;
+
+    let response = { type: "tabAttached", threadActor: this._threadActor.actorID };
+    this._appendExtraActors(response);
+
+    return response;
+  },
+
+  onDetach: function(aRequest) {
+    if (!this._attached) {
+      return { "error":"wrongState" };
+    }
+    return { type: "detached" };
+  },
+
+  /* Support for DebuggerServer.addTabActor. */
+  _createExtraActors: createExtraActors,
+  _appendExtraActors: appendExtraActors
+};
+
+TestTabActor.prototype.requestTypes = {
+  "attach": TestTabActor.prototype.onAttach,
+  "detach": TestTabActor.prototype.onDetach
+};
+
+exports.register = function(handle) {
+  handle.setRootActor(createRootActor);
+};
+
+exports.unregister = function(handle) {
+  handle.setRootActor(null);
+};
--- a/toolkit/devtools/security/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/security/tests/unit/xpcshell.ini
@@ -1,6 +1,10 @@
 [DEFAULT]
-head =
+head = head_dbg.js
 tail =
 skip-if = toolkit == 'android'
 
+support-files=
+  testactors.js
+
 [test_cert.js]
+[test_encryption.js]
--- a/toolkit/devtools/transport/tests/unit/head_dbg.js
+++ b/toolkit/devtools/transport/tests/unit/head_dbg.js
@@ -7,16 +7,17 @@ const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 const CC = Components.Constructor;
 
 const { devtools } =
   Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 const { Promise: promise } =
   Cu.import("resource://gre/modules/Promise.jsm", {});
+const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 
 const Services = devtools.require("Services");
 const DevToolsUtils = devtools.require("devtools/toolkit/DevToolsUtils.js");
 
 // We do not want to log packets by default, because in some tests,
 // we can be sending large amounts of data. The test harness has
 // trouble dealing with logging all the data, and we end up with
 // intermittent time outs (e.g. bug 775924).
@@ -253,30 +254,30 @@ function writeTestTempFile(aFileName, aC
     } while (aContent.length > 0);
   } finally {
     stream.close();
   }
 }
 
 /*** Transport Factories ***/
 
-function socket_transport() {
+let socket_transport = Task.async(function*() {
   if (!DebuggerServer.listeningSockets) {
     let listener = DebuggerServer.createListener();
     listener.portOrPath = -1 /* any available port */;
     listener.allowConnection = () => true;
-    listener.open();
+    yield listener.open();
   }
   let port = DebuggerServer._listeners[0].port;
   do_print("Debugger server port is " + port);
-  return DebuggerClient.socketConnect("127.0.0.1", port);
-}
+  return DebuggerClient.socketConnect({ host: "127.0.0.1", port });
+});
 
 function local_transport() {
-  return DebuggerServer.connectPipe();
+  return promise.resolve(DebuggerServer.connectPipe());
 }
 
 /*** Sample Data ***/
 
 let gReallyLong;
 function really_long() {
   if (gReallyLong) {
     return gReallyLong;
--- a/toolkit/devtools/transport/tests/unit/test_bulk_error.js
+++ b/toolkit/devtools/transport/tests/unit/test_bulk_error.js
@@ -46,33 +46,33 @@ TestBulkActor.prototype.requestTypes = {
 };
 
 function add_test_bulk_actor() {
   DebuggerServer.addGlobalActor(TestBulkActor);
 }
 
 /*** Tests ***/
 
-function test_string_error(transportFactory, onReady) {
+let test_string_error = Task.async(function*(transportFactory, onReady) {
   let deferred = promise.defer();
-  let transport = transportFactory();
+  let transport = yield transportFactory();
 
   let client = new DebuggerClient(transport);
   client.connect((app, traits) => {
     do_check_eq(traits.bulk, true);
     client.listTabs(response => {
       deferred.resolve(onReady(client, response).then(() => {
         client.close();
         transport.close();
       }));
     });
   });
 
   return deferred.promise;
-}
+});
 
 /*** Reply Types ***/
 
 function json_reply(client, response) {
   let reallyLong = really_long();
 
   let request = client.startBulkRequest({
     actor: response.testBulk,
--- a/toolkit/devtools/transport/tests/unit/test_client_server_bulk.js
+++ b/toolkit/devtools/transport/tests/unit/test_client_server_bulk.js
@@ -129,26 +129,26 @@ let replyHandlers = {
     });
     return replyDeferred.promise;
   }
 
 };
 
 /*** Tests ***/
 
-function test_bulk_request_cs(transportFactory, actorType, replyType) {
+let test_bulk_request_cs = Task.async(function*(transportFactory, actorType, replyType) {
   // Ensure test files are not present from a failed run
   cleanup_files();
   writeTestTempFile("bulk-input", really_long());
 
   let clientDeferred = promise.defer();
   let serverDeferred = promise.defer();
   let bulkCopyDeferred = promise.defer();
 
-  let transport = transportFactory();
+  let transport = yield transportFactory();
 
   let client = new DebuggerClient(transport);
   client.connect((app, traits) => {
     do_check_eq(traits.bulk, true);
     client.listTabs(clientDeferred.resolve);
   });
 
   clientDeferred.promise.then(response => {
@@ -181,27 +181,27 @@ function test_bulk_request_cs(transportF
     }
   });
 
   return promise.all([
     clientDeferred.promise,
     bulkCopyDeferred.promise,
     serverDeferred.promise
   ]);
-}
+});
 
-function test_json_request_cs(transportFactory, actorType, replyType) {
+let test_json_request_cs = Task.async(function*(transportFactory, actorType, replyType) {
   // Ensure test files are not present from a failed run
   cleanup_files();
   writeTestTempFile("bulk-input", really_long());
 
   let clientDeferred = promise.defer();
   let serverDeferred = promise.defer();
 
-  let transport = transportFactory();
+  let transport = yield transportFactory();
 
   let client = new DebuggerClient(transport);
   client.connect((app, traits) => {
     do_check_eq(traits.bulk, true);
     client.listTabs(clientDeferred.resolve);
   });
 
   clientDeferred.promise.then(response => {
@@ -222,17 +222,17 @@ function test_json_request_cs(transportF
       serverDeferred.resolve();
     }
   });
 
   return promise.all([
     clientDeferred.promise,
     serverDeferred.promise
   ]);
-}
+});
 
 /*** Test Utils ***/
 
 function verify_files() {
   let reallyLong = really_long();
 
   let inputFile = getTestTempFile("bulk-input");
   let outputFile = getTestTempFile("bulk-output");
--- a/toolkit/devtools/transport/tests/unit/test_dbgsocket.js
+++ b/toolkit/devtools/transport/tests/unit/test_dbgsocket.js
@@ -7,24 +7,24 @@ Cu.import("resource://gre/modules/devtoo
 let gPort;
 let gExtraListener;
 
 function run_test()
 {
   do_print("Starting test at " + new Date().toTimeString());
   initTestDebuggerServer();
 
-  add_test(test_socket_conn);
-  add_test(test_socket_shutdown);
+  add_task(test_socket_conn);
+  add_task(test_socket_shutdown);
   add_test(test_pipe_conn);
 
   run_next_test();
 }
 
-function test_socket_conn()
+function* test_socket_conn()
 {
   do_check_eq(DebuggerServer.listeningSockets, 0);
   let listener = DebuggerServer.createListener();
   do_check_true(listener);
   listener.portOrPath = -1 /* any available port */;
   listener.allowConnection = () => true;
   listener.open();
   do_check_eq(DebuggerServer.listeningSockets, 1);
@@ -34,69 +34,69 @@ function test_socket_conn()
   gExtraListener = DebuggerServer.createListener();
   gExtraListener.portOrPath = -1;
   gExtraListener.allowConnection = () => true;
   gExtraListener.open();
   do_check_eq(DebuggerServer.listeningSockets, 2);
 
   do_print("Starting long and unicode tests at " + new Date().toTimeString());
   let unicodeString = "(╯°□°)╯︵ ┻━┻";
-  let transport = DebuggerClient.socketConnect("127.0.0.1", gPort);
+  let transport = yield DebuggerClient.socketConnect({
+    host: "127.0.0.1",
+    port: gPort
+  });
+  let closedDeferred = promise.defer();
   transport.hooks = {
     onPacket: function(aPacket) {
       this.onPacket = function(aPacket) {
         do_check_eq(aPacket.unicode, unicodeString);
         transport.close();
       }
       // Verify that things work correctly when bigger than the output
       // transport buffers and when transporting unicode...
       transport.send({to: "root",
                       type: "echo",
                       reallylong: really_long(),
                       unicode: unicodeString});
       do_check_eq(aPacket.from, "root");
     },
     onClosed: function(aStatus) {
-      run_next_test();
+      closedDeferred.resolve();
     },
   };
   transport.ready();
+  return closedDeferred.promise;
 }
 
 function test_socket_shutdown()
 {
   do_check_eq(DebuggerServer.listeningSockets, 2);
   gExtraListener.close();
   do_check_eq(DebuggerServer.listeningSockets, 1);
   do_check_true(DebuggerServer.closeAllListeners());
   do_check_eq(DebuggerServer.listeningSockets, 0);
   // Make sure closing the listener twice does nothing.
   do_check_false(DebuggerServer.closeAllListeners());
   do_check_eq(DebuggerServer.listeningSockets, 0);
 
   do_print("Connecting to a server socket at " + new Date().toTimeString());
-  let transport = DebuggerClient.socketConnect("127.0.0.1", gPort);
-  transport.hooks = {
-    onPacket: function(aPacket) {
-      // Shouldn't reach this, should never connect.
-      do_check_true(false);
-    },
+  try {
+    let transport = yield DebuggerClient.socketConnect({
+      host: "127.0.0.1",
+      port: gPort
+    });
+  } catch(e if e.result == Cr.NS_ERROR_CONNECTION_REFUSED ||
+               e.result == Cr.NS_ERROR_NET_TIMEOUT) {
+    // Expected failure
+    do_check_true(true);
+    return;
+  }
 
-    onClosed: function(aStatus) {
-      do_print("test_socket_shutdown onClosed called at " + new Date().toTimeString());
-      // The connection should be refused here, but on slow or overloaded
-      // machines it may just time out.
-      let expected = [ Cr.NS_ERROR_CONNECTION_REFUSED, Cr.NS_ERROR_NET_TIMEOUT ];
-      do_check_neq(expected.indexOf(aStatus), -1);
-      run_next_test();
-    }
-  };
-
-  do_print("Initializing input stream at " + new Date().toTimeString());
-  transport.ready();
+  // Shouldn't reach this, should never connect.
+  do_check_true(false);
 }
 
 function test_pipe_conn()
 {
   let transport = DebuggerServer.connectPipe();
   transport.hooks = {
     onPacket: function(aPacket) {
       do_check_eq(aPacket.from, "root");
--- a/toolkit/devtools/transport/tests/unit/test_dbgsocket_connection_drop.js
+++ b/toolkit/devtools/transport/tests/unit/test_dbgsocket_connection_drop.js
@@ -12,20 +12,20 @@ Cu.import("resource://gre/modules/devtoo
 Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
 
 const { RawPacket } = devtools.require("devtools/toolkit/transport/packets");
 
 function run_test() {
   do_print("Starting test at " + new Date().toTimeString());
   initTestDebuggerServer();
 
-  add_test(test_socket_conn_drops_after_invalid_header);
-  add_test(test_socket_conn_drops_after_invalid_header_2);
-  add_test(test_socket_conn_drops_after_too_large_length);
-  add_test(test_socket_conn_drops_after_too_long_header);
+  add_task(test_socket_conn_drops_after_invalid_header);
+  add_task(test_socket_conn_drops_after_invalid_header_2);
+  add_task(test_socket_conn_drops_after_too_large_length);
+  add_task(test_socket_conn_drops_after_too_long_header);
   run_next_test();
 }
 
 function test_socket_conn_drops_after_invalid_header() {
   return test_helper('fluff30:27:{"to":"root","type":"echo"}');
 }
 
 function test_socket_conn_drops_after_invalid_header_2() {
@@ -41,33 +41,38 @@ function test_socket_conn_drops_after_to
   // The packet header is currently limited to no more than 200 bytes
   let rawPacket = '4305724038957487634549823475894325';
   for (let i = 0; i < 8; i++) {
     rawPacket += rawPacket;
   }
   return test_helper(rawPacket + ':');
 }
 
-function test_helper(payload) {
+let test_helper = Task.async(function*(payload) {
   let listener = DebuggerServer.createListener();
   listener.portOrPath = -1;
   listener.allowConnection = () => true;
   listener.open();
 
-  let transport = DebuggerClient.socketConnect("127.0.0.1", listener.port);
+  let transport = yield DebuggerClient.socketConnect({
+    host: "127.0.0.1",
+    port: listener.port
+  });
+  let closedDeferred = promise.defer();
   transport.hooks = {
     onPacket: function(aPacket) {
       this.onPacket = function(aPacket) {
         do_throw(new Error("This connection should be dropped."));
         transport.close();
       };
 
       // Inject the payload directly into the stream.
       transport._outgoing.push(new RawPacket(transport, payload));
       transport._flushOutgoing();
     },
     onClosed: function(aStatus) {
       do_check_true(true);
-      run_next_test();
+      closedDeferred.resolve();
     },
   };
   transport.ready();
-}
+  return closedDeferred.promise;
+});
--- a/toolkit/devtools/transport/tests/unit/test_no_bulk.js
+++ b/toolkit/devtools/transport/tests/unit/test_no_bulk.js
@@ -21,28 +21,28 @@ function run_test() {
     DebuggerServer.destroy();
   });
 
   run_next_test();
 }
 
 /*** Tests ***/
 
-function test_bulk_send_error(transportFactory) {
+let test_bulk_send_error = Task.async(function*(transportFactory) {
   let deferred = promise.defer();
-  let transport = transportFactory();
+  let transport = yield transportFactory();
 
   let client = new DebuggerClient(transport);
   client.connect((app, traits) => {
     do_check_false(traits.bulk);
 
     try {
       client.startBulkRequest();
       do_throw(new Error("Can't use bulk since server doesn't support it"));
     } catch(e) {
       do_check_true(true);
     }
 
     deferred.resolve();
   });
 
   return deferred.promise;
-}
+});
--- a/toolkit/devtools/transport/tests/unit/test_queue.js
+++ b/toolkit/devtools/transport/tests/unit/test_queue.js
@@ -20,28 +20,28 @@ function run_test() {
     DebuggerServer.destroy();
   });
 
   run_next_test();
 }
 
 /*** Tests ***/
 
-function test_transport(transportFactory) {
+let test_transport = Task.async(function*(transportFactory) {
   let clientDeferred = promise.defer();
   let serverDeferred = promise.defer();
 
   // Ensure test files are not present from a failed run
   cleanup_files();
   let reallyLong = really_long();
   writeTestTempFile("bulk-input", reallyLong);
 
   do_check_eq(Object.keys(DebuggerServer._connections).length, 0);
 
-  let transport = transportFactory();
+  let transport = yield transportFactory();
 
   // Sending from client to server
   function write_data({copyFrom}) {
     NetUtil.asyncFetch(getTestTempFile("bulk-input"), function(input, status) {
       copyFrom(input).then(() => {
         input.close();
       });
     });
@@ -128,17 +128,17 @@ function test_transport(transportFactory
     onClosed: function() {
       do_throw("Transport closed before we expected");
     }
   };
 
   transport.ready();
 
   return promise.all([clientDeferred.promise, serverDeferred.promise]);
-}
+});
 
 /*** Test Utils ***/
 
 function verify() {
   let reallyLong = really_long();
 
   let inputFile = getTestTempFile("bulk-input");
   let outputFile = getTestTempFile("bulk-output");
--- a/toolkit/devtools/transport/tests/unit/test_transport_bulk.js
+++ b/toolkit/devtools/transport/tests/unit/test_transport_bulk.js
@@ -18,30 +18,30 @@ function run_test() {
   run_next_test();
 }
 
 /*** Tests ***/
 
 /**
  * This tests a one-way bulk transfer at the transport layer.
  */
-function test_bulk_transfer_transport(transportFactory) {
+let test_bulk_transfer_transport = Task.async(function*(transportFactory) {
   do_print("Starting bulk transfer test at " + new Date().toTimeString());
 
   let clientDeferred = promise.defer();
   let serverDeferred = promise.defer();
 
   // Ensure test files are not present from a failed run
   cleanup_files();
   let reallyLong = really_long();
   writeTestTempFile("bulk-input", reallyLong);
 
   do_check_eq(Object.keys(DebuggerServer._connections).length, 0);
 
-  let transport = transportFactory();
+  let transport = yield transportFactory();
 
   // Sending from client to server
   function write_data({copyFrom}) {
     NetUtil.asyncFetch(getTestTempFile("bulk-input"), function(input, status) {
       copyFrom(input).then(() => {
         input.close();
       });
     });
@@ -99,17 +99,17 @@ function test_bulk_transfer_transport(tr
     onClosed: function() {
       do_throw("Transport closed before we expected");
     }
   };
 
   transport.ready();
 
   return promise.all([clientDeferred.promise, serverDeferred.promise]);
-}
+});
 
 /*** Test Utils ***/
 
 function verify() {
   let reallyLong = really_long();
 
   let inputFile = getTestTempFile("bulk-input");
   let outputFile = getTestTempFile("bulk-output");