From ccf18a8ec451aa30b6548474d3b32aee7c2cd5a1 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 10 Dec 2014 20:55:52 -0600 Subject: [PATCH] Bug 1059001 - Part 3: Add encryption socket option. r=past --- b2g/chrome/content/devtools/debugger.js | 1 + browser/devtools/framework/connect/connect.js | 26 +- .../framework/toolbox-process-window.js | 15 +- modules/libpref/init/all.js | 2 + .../apps/tests/debugger-protocol-helper.js | 8 +- toolkit/devtools/client/connection-manager.js | 59 ++-- toolkit/devtools/client/dbg-client.jsm | 4 +- .../gcli/source/lib/gcli/connectors/rdp.js | 13 +- toolkit/devtools/security/cert.js | 66 ++++ toolkit/devtools/security/moz.build | 1 + toolkit/devtools/security/socket.js | 313 +++++++++++++++--- .../devtools/security/tests/unit/head_dbg.js | 107 ++++++ .../devtools/security/tests/unit/test_cert.js | 6 +- .../security/tests/unit/test_encryption.js | 98 ++++++ .../security/tests/unit/testactors.js | 131 ++++++++ .../devtools/security/tests/unit/xpcshell.ini | 6 +- .../devtools/transport/tests/unit/head_dbg.js | 11 +- .../transport/tests/unit/test_bulk_error.js | 6 +- .../tests/unit/test_client_server_bulk.js | 12 +- .../transport/tests/unit/test_dbgsocket.js | 49 +-- .../unit/test_dbgsocket_connection_drop.js | 21 +- .../transport/tests/unit/test_no_bulk.js | 6 +- .../transport/tests/unit/test_queue.js | 6 +- .../tests/unit/test_transport_bulk.js | 6 +- 24 files changed, 818 insertions(+), 155 deletions(-) create mode 100644 toolkit/devtools/security/cert.js create mode 100644 toolkit/devtools/security/tests/unit/head_dbg.js create mode 100644 toolkit/devtools/security/tests/unit/test_encryption.js create mode 100644 toolkit/devtools/security/tests/unit/testactors.js diff --git a/b2g/chrome/content/devtools/debugger.js b/b2g/chrome/content/devtools/debugger.js index 129f793aa82d..e4328d23fc59 100644 --- a/b2g/chrome/content/devtools/debugger.js +++ b/b2g/chrome/content/devtools/debugger.js @@ -183,6 +183,7 @@ let WiFiRemoteDebugger = { 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); diff --git a/browser/devtools/framework/connect/connect.js b/browser/devtools/framework/connect/connect.js index 5a1ed579621b..c4cd31759d09 100644 --- a/browser/devtools/framework/connect/connect.js +++ b/browser/devtools/framework/connect/connect.js @@ -41,14 +41,18 @@ window.addEventListener("DOMContentLoaded", function onDOMReady() { 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"); @@ -64,18 +68,18 @@ function submit() { } // 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; } /** diff --git a/browser/devtools/framework/toolbox-process-window.js b/browser/devtools/framework/toolbox-process-window.js index 54aa90ba4631..499c2dc69625 100644 --- a/browser/devtools/framework/toolbox-process-window.js +++ b/browser/devtools/framework/toolbox-process-window.js @@ -12,6 +12,7 @@ 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. @@ -23,13 +24,13 @@ let Prefs = new ViewHelpers.Prefs("devtools.debugger", { 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"); @@ -43,7 +44,7 @@ function connect() { gClient.listTabs(openToolbox); } }); -} +}); // Certain options should be toggled since we can assume chrome debugging here function setPrefDefaults() { @@ -56,7 +57,7 @@ window.addEventListener("load", function() { let cmdClose = document.getElementById("toolbox-cmd-close"); cmdClose.addEventListener("command", onCloseCommand); setPrefDefaults(); - connect(); + connect().catch(Cu.reportError); }); function onCloseCommand(event) { diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index fe5871e64e65..dfa4e12429bf 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -799,6 +799,8 @@ pref("devtools.remote.wifi.scan", false); // 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); diff --git a/toolkit/devtools/apps/tests/debugger-protocol-helper.js b/toolkit/devtools/apps/tests/debugger-protocol-helper.js index 42c8b53281f1..0c57895bc92a 100644 --- a/toolkit/devtools/apps/tests/debugger-protocol-helper.js +++ b/toolkit/devtools/apps/tests/debugger-protocol-helper.js @@ -28,8 +28,12 @@ function connect(onDone) { 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); diff --git a/toolkit/devtools/client/connection-manager.js b/toolkit/devtools/client/connection-manager.js index 2ae8d25f96f2..c6f58f8e6439 100644 --- a/toolkit/devtools/client/connection-manager.js +++ b/toolkit/devtools/client/connection-manager.js @@ -9,10 +9,13 @@ 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. @@ -48,7 +51,8 @@ Cu.import("resource://gre/modules/devtools/dbg-server.jsm"); * . 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 @@ -113,6 +117,7 @@ function Connection(host, port) { this._onConnected = this._onConnected.bind(this); this._onTimeout = this._onTimeout.bind(this); this.keepConnecting = false; + this.encryption = false; } Connection.Status = { @@ -222,30 +227,38 @@ Connection.prototype = { 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; } - this._client = new DebuggerClient(transport); - this._client.addOneTimeListener("closed", this._onDisconnected); - this._client.connect(this._onConnected); + 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); + }, 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() { diff --git a/toolkit/devtools/client/dbg-client.jsm b/toolkit/devtools/client/dbg-client.jsm index e755f0f652ab..a7f559a1beb8 100644 --- a/toolkit/devtools/client/dbg-client.jsm +++ b/toolkit/devtools/client/dbg-client.jsm @@ -372,9 +372,9 @@ DebuggerClient.Argument.prototype.getArgument = function (aParams) { }; // 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 = { diff --git a/toolkit/devtools/gcli/source/lib/gcli/connectors/rdp.js b/toolkit/devtools/gcli/source/lib/gcli/connectors/rdp.js index 3c3e30f95f4e..abfe7b31abe9 100644 --- a/toolkit/devtools/gcli/source/lib/gcli/connectors/rdp.js +++ b/toolkit/devtools/gcli/source/lib/gcli/connectors/rdp.js @@ -19,6 +19,7 @@ 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; @@ -61,7 +62,7 @@ function RdpConnection(url) { /** * Asynchronous construction */ -RdpConnection.create = function(url) { +RdpConnection.create = Task.async(function*(url) { this.host = url; this.port = undefined; // TODO: Split out the port number @@ -70,9 +71,13 @@ RdpConnection.create = function(url) { 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; @@ -80,7 +85,7 @@ RdpConnection.create = function(url) { }.bind(this)); }.bind(this)); }.bind(this)); -}; +}); RdpConnection.prototype = Object.create(Connection.prototype); diff --git a/toolkit/devtools/security/cert.js b/toolkit/devtools/security/cert.js new file mode 100644 index 000000000000..b3de2e5ec46d --- /dev/null +++ b/toolkit/devtools/security/cert.js @@ -0,0 +1,66 @@ +/* -*- 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", () => { + // Ensure PSM is initialized to support TLS sockets + Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports); + 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; + } + +}; diff --git a/toolkit/devtools/security/moz.build b/toolkit/devtools/security/moz.build index 35c365340f6f..2d5c5506c4cb 100644 --- a/toolkit/devtools/security/moz.build +++ b/toolkit/devtools/security/moz.build @@ -21,5 +21,6 @@ FAIL_ON_WARNINGS = True FINAL_LIBRARY = 'xul' EXTRA_JS_MODULES.devtools.security += [ + 'cert.js', 'socket.js', ] diff --git a/toolkit/devtools/security/socket.js b/toolkit/devtools/security/socket.js index 60a177eb897a..4cccb9de4d90 100644 --- a/toolkit/devtools/security/socket.js +++ b/toolkit/devtools/security/socket.js @@ -7,27 +7,24 @@ "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"); @@ -38,18 +35,111 @@ DevToolsUtils.defineLazyGetter(this, "socketTransportService", () => { .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 }) { + let attempt = yield _attemptTransport({ host, port, encryption }); + if (attempt.transport) { + return attempt.transport; // Success + } + + // If the server cert failed validation, store a temporary override and make + // a second attempt. + if (encryption && attempt.certError) { + _storeCertOverride(attempt.s, host, port); + } else { + throw new Error("Connection failed"); + } + + attempt = yield _attemptTransport({ host, port, encryption }); + if (attempt.transport) { + return attempt.transport; // Success + } + + throw new Error("Connection failed even after cert override"); +}); + +/** + * Try to connect and create a DevTools transport. + * + * @return transport DebuggerTransport + * A possible DevTools transport (if connection succeeded and streams + * are actually alive and working) + * @return certError boolean + * Flag noting if cert trouble caused the streams to fail + * @return s nsISocketTransport + * Underlying socket transport, in case more details are needed. + */ +let _attemptTransport = 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); + + let transport; + if (alive) { + transport = new DebuggerTransport(input, output); + } else { + // Something went wrong, close the streams. + input.close(); + output.close(); + } + + return { transport, certError, s }; +}); + +/** + * Try to connect to a remote server socket. + * + * If successsful, the socket transport and its opened streams are returned. + * Typically, this will only fail if the host / port is unreachable. Other + * problems, such as security errors, will allow this stage to succeed, but then + * fail later when the streams are actually used. + * @return s nsISocketTransport + * Underlying socket transport, in case more details are needed. + * @return input nsIAsyncInputStream + * The socket's input stream. + * @return output nsIAsyncOutputStream + * The socket's output stream. + */ +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! @@ -58,15 +148,61 @@ function socketConnect(host, port) { // 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); + DevToolsUtils.reportException("_attemptConnect", 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.serverCert; + let overrideBits = Ci.nsICertOverrideService.ERROR_UNTRUSTED | + Ci.nsICertOverrideService.ERROR_MISMATCH; + certOverrideService.rememberValidityOverride(host, port, cert, overrideBits, + true /* temporary */); } /** @@ -133,6 +269,11 @@ SocketListener.prototype = { */ discoverable: false, + /** + * Controls whether this listener's transport uses encryption. + */ + encryption: false, + /** * Validate that all options have been set to a supported configuration. */ @@ -158,31 +299,53 @@ SocketListener.prototype = { flags |= Ci.nsIServerSocket.LoopbackOnly; } - try { + let self = this; + return Task.spawn(function*() { let backlog = 4; - let port = Number(this.portOrPath); - if (port) { - this._socket = new ServerSocket(port, flags, backlog); + self._socket = self._createSocketInstance(); + if (self.isPortBased) { + let port = Number(self.portOrPath); + 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: " + (self.port || self.portOrPath)); + }).then(() => { + if (this.discoverable && this.port) { + discovery.addService("devtools", { port: this.port }); + } + }).catch(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. @@ -198,12 +361,19 @@ SocketListener.prototype = { DebuggerServer._removeListener(this); }, + /** + * Gets whether this listener uses a port number vs. a path. + */ + get isPortBased() { + return !!Number(this.portOrPath); + }, + /** * Gets the port that a TCP socket listener is listening on, or null if this * is not a TCP socket (so there is no port). */ get port() { - if (!this._socket) { + if (!this.isPortBased || !this._socket) { return null; } return this._socket.port; @@ -213,6 +383,9 @@ SocketListener.prototype = { onSocketAccepted: DevToolsUtils.makeInfallible(function(socket, socketTransport) { + if (this.encryption) { + new SecurityObserver(socketTransport); + } if (Services.prefs.getBoolPref("devtools.debugger.prompt-connection") && !this.allowConnection()) { return; @@ -232,13 +405,63 @@ SocketListener.prototype = { }; -// 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.destroy(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.destroy(Cr.NS_ERROR_CONNECTION_REFUSED); + } + }, + + destroy(result) { + clearTimeout(this._handshakeTimeout); + let connectionInfo = this.socketTransport.securityInfo + .QueryInterface(Ci.nsITLSServerConnectionInfo); + connectionInfo.setSecurityObserver(null); + this.socketTransport.close(result); + this.socketTransport = null; } + }; + +DebuggerSocket.createListener = function() { + return new SocketListener(); +}; + +exports.DebuggerSocket = DebuggerSocket; diff --git a/toolkit/devtools/security/tests/unit/head_dbg.js b/toolkit/devtools/security/tests/unit/head_dbg.js new file mode 100644 index 000000000000..e57348f66525 --- /dev/null +++ b/toolkit/devtools/security/tests/unit/head_dbg.js @@ -0,0 +1,107 @@ +/* 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"); +const xpcInspector = devtools.require("xpcInspector"); + +// 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 = ""; + } + } + + // Make sure we exit all nested event loops so that the test can finish. + while (xpcInspector.eventLoopNestLevel > 0) { + xpcInspector.exitNestedEventLoop(); + } + + // Print in most cases, but ignore the "strict" messages + if (!(aMessage.flags & Ci.nsIScriptError.strictFlag)) { + do_print("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(); +} diff --git a/toolkit/devtools/security/tests/unit/test_cert.js b/toolkit/devtools/security/tests/unit/test_cert.js index a8aaa1eec83a..d4f302e2381b 100644 --- a/toolkit/devtools/security/tests/unit/test_cert.js +++ b/toolkit/devtools/security/tests/unit/test_cert.js @@ -3,10 +3,6 @@ "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); @@ -49,7 +45,7 @@ function removeCert(nickname) { } 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); diff --git a/toolkit/devtools/security/tests/unit/test_encryption.js b/toolkit/devtools/security/tests/unit/test_encryption.js new file mode 100644 index 000000000000..61c3307900ca --- /dev/null +++ b/toolkit/devtools/security/tests/unit/test_encryption.js @@ -0,0 +1,98 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test basic functionality of DevTools client and server TLS encryption mode +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); + let onUnexpectedClose = () => { + do_throw("Closed unexpectedly"); + }; + client.addListener("closed", onUnexpectedClose); + 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"); + + client.removeListener("closed", onUnexpectedClose); + transport.close(); + 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"); + listener.close(); + equal(DebuggerServer.listeningSockets, 0, "0 listening sockets"); + return; + } + + do_throw("Connection unexpectedly succeeded"); +}); + +add_task(function*() { + DebuggerServer.destroy(); +}); diff --git a/toolkit/devtools/security/tests/unit/testactors.js b/toolkit/devtools/security/tests/unit/testactors.js new file mode 100644 index 000000000000..20a346f2444c --- /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); +}; diff --git a/toolkit/devtools/security/tests/unit/xpcshell.ini b/toolkit/devtools/security/tests/unit/xpcshell.ini index b1bccfcb1924..c6bfe1e3db71 100644 --- 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] diff --git a/toolkit/devtools/transport/tests/unit/head_dbg.js b/toolkit/devtools/transport/tests/unit/head_dbg.js index 947697f9055e..d33b257f3fa5 100644 --- a/toolkit/devtools/transport/tests/unit/head_dbg.js +++ b/toolkit/devtools/transport/tests/unit/head_dbg.js @@ -12,6 +12,7 @@ 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"); @@ -258,20 +259,20 @@ function writeTestTempFile(aFileName, aContent) { /*** 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 ***/ diff --git a/toolkit/devtools/transport/tests/unit/test_bulk_error.js b/toolkit/devtools/transport/tests/unit/test_bulk_error.js index 2dd45ee8088d..4ff666e94046 100644 --- a/toolkit/devtools/transport/tests/unit/test_bulk_error.js +++ b/toolkit/devtools/transport/tests/unit/test_bulk_error.js @@ -51,9 +51,9 @@ function add_test_bulk_actor() { /*** 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) => { @@ -67,7 +67,7 @@ function test_string_error(transportFactory, onReady) { }); return deferred.promise; -} +}); /*** Reply Types ***/ diff --git a/toolkit/devtools/transport/tests/unit/test_client_server_bulk.js b/toolkit/devtools/transport/tests/unit/test_client_server_bulk.js index 713aabdcf02d..d2b91e0e3f1d 100644 --- a/toolkit/devtools/transport/tests/unit/test_client_server_bulk.js +++ b/toolkit/devtools/transport/tests/unit/test_client_server_bulk.js @@ -134,7 +134,7 @@ let replyHandlers = { /*** 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()); @@ -143,7 +143,7 @@ function test_bulk_request_cs(transportFactory, actorType, replyType) { let serverDeferred = promise.defer(); let bulkCopyDeferred = promise.defer(); - let transport = transportFactory(); + let transport = yield transportFactory(); let client = new DebuggerClient(transport); client.connect((app, traits) => { @@ -186,9 +186,9 @@ function test_bulk_request_cs(transportFactory, actorType, replyType) { 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()); @@ -196,7 +196,7 @@ function test_json_request_cs(transportFactory, actorType, replyType) { let clientDeferred = promise.defer(); let serverDeferred = promise.defer(); - let transport = transportFactory(); + let transport = yield transportFactory(); let client = new DebuggerClient(transport); client.connect((app, traits) => { @@ -227,7 +227,7 @@ function test_json_request_cs(transportFactory, actorType, replyType) { clientDeferred.promise, serverDeferred.promise ]); -} +}); /*** Test Utils ***/ diff --git a/toolkit/devtools/transport/tests/unit/test_dbgsocket.js b/toolkit/devtools/transport/tests/unit/test_dbgsocket.js index d9857676cb39..5dd441edfeeb 100644 --- a/toolkit/devtools/transport/tests/unit/test_dbgsocket.js +++ b/toolkit/devtools/transport/tests/unit/test_dbgsocket.js @@ -12,14 +12,14 @@ 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(); @@ -39,7 +39,11 @@ function test_socket_conn() 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) { @@ -55,13 +59,14 @@ function test_socket_conn() do_check_eq(aPacket.from, "root"); }, onClosed: function(aStatus) { - run_next_test(); + closedDeferred.resolve(); }, }; transport.ready(); + return closedDeferred.promise; } -function test_socket_shutdown() +function* test_socket_shutdown() { do_check_eq(DebuggerServer.listeningSockets, 2); gExtraListener.close(); @@ -73,25 +78,21 @@ function test_socket_shutdown() 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) { + // The connection should be refused here, but on slow or overloaded + // machines it may just time out. + 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() diff --git a/toolkit/devtools/transport/tests/unit/test_dbgsocket_connection_drop.js b/toolkit/devtools/transport/tests/unit/test_dbgsocket_connection_drop.js index cf0026db8077..9fafa412e914 100644 --- a/toolkit/devtools/transport/tests/unit/test_dbgsocket_connection_drop.js +++ b/toolkit/devtools/transport/tests/unit/test_dbgsocket_connection_drop.js @@ -17,10 +17,10 @@ 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(); } @@ -46,13 +46,17 @@ function test_socket_conn_drops_after_too_long_header() { 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) { @@ -66,8 +70,9 @@ function test_helper(payload) { }, onClosed: function(aStatus) { do_check_true(true); - run_next_test(); + closedDeferred.resolve(); }, }; transport.ready(); -} + return closedDeferred.promise; +}); diff --git a/toolkit/devtools/transport/tests/unit/test_no_bulk.js b/toolkit/devtools/transport/tests/unit/test_no_bulk.js index df9f48beb22c..24838c2daca0 100644 --- a/toolkit/devtools/transport/tests/unit/test_no_bulk.js +++ b/toolkit/devtools/transport/tests/unit/test_no_bulk.js @@ -26,9 +26,9 @@ function run_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) => { @@ -45,4 +45,4 @@ function test_bulk_send_error(transportFactory) { }); return deferred.promise; -} +}); diff --git a/toolkit/devtools/transport/tests/unit/test_queue.js b/toolkit/devtools/transport/tests/unit/test_queue.js index c67b37e6aa4e..bba00d6b3398 100644 --- a/toolkit/devtools/transport/tests/unit/test_queue.js +++ b/toolkit/devtools/transport/tests/unit/test_queue.js @@ -25,7 +25,7 @@ function run_test() { /*** Tests ***/ -function test_transport(transportFactory) { +let test_transport = Task.async(function*(transportFactory) { let clientDeferred = promise.defer(); let serverDeferred = promise.defer(); @@ -36,7 +36,7 @@ function test_transport(transportFactory) { 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}) { @@ -133,7 +133,7 @@ function test_transport(transportFactory) { transport.ready(); return promise.all([clientDeferred.promise, serverDeferred.promise]); -} +}); /*** Test Utils ***/ diff --git a/toolkit/devtools/transport/tests/unit/test_transport_bulk.js b/toolkit/devtools/transport/tests/unit/test_transport_bulk.js index ca1eb5b3d60a..0c3d95bd9d2f 100644 --- a/toolkit/devtools/transport/tests/unit/test_transport_bulk.js +++ b/toolkit/devtools/transport/tests/unit/test_transport_bulk.js @@ -23,7 +23,7 @@ function run_test() { /** * 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(); @@ -36,7 +36,7 @@ function test_bulk_transfer_transport(transportFactory) { 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}) { @@ -104,7 +104,7 @@ function test_bulk_transfer_transport(transportFactory) { transport.ready(); return promise.all([clientDeferred.promise, serverDeferred.promise]); -} +}); /*** Test Utils ***/