Bug 1059001 - Part 3: Add encryption socket option. r=past

This commit is contained in:
J. Ryan Stinnett 2014-12-10 20:55:52 -06:00
parent 39b5349e47
commit 46bbcd61dd
24 changed files with 818 additions and 155 deletions

View File

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

View File

@ -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;
}
/**

View File

@ -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) {

View File

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

View File

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

View File

@ -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() {

View File

@ -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 = {

View File

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

View File

@ -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;
}
};

View File

@ -21,5 +21,6 @@ FAIL_ON_WARNINGS = True
FINAL_LIBRARY = 'xul'
EXTRA_JS_MODULES.devtools.security += [
'cert.js',
'socket.js',
]

View File

@ -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;

View File

@ -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 = "<error converting error message to 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();
}

View File

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

View File

@ -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();
});

View File

@ -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);
};

View File

@ -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]

View File

@ -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 ***/

View File

@ -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 ***/

View File

@ -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 ***/

View File

@ -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()

View File

@ -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;
});

View File

@ -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;
}
});

View File

@ -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 ***/

View File

@ -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 ***/