/* -*- 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, 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, 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"); loader.lazyRequireGetter(this, "cert", "devtools/toolkit/security/cert"); loader.lazyRequireGetter(this, "setTimeout", "Timer", true); loader.lazyRequireGetter(this, "clearTimeout", "Timer", true); DevToolsUtils.defineLazyGetter(this, "nsFile", () => { return CC("@mozilla.org/file/local;1", "nsIFile", "initWithPath"); }); DevToolsUtils.defineLazyGetter(this, "socketTransportService", () => { return Cc["@mozilla.org/network/socket-transport-service;1"] .getService(Ci.nsISocketTransportService); }); DevToolsUtils.defineLazyGetter(this, "certOverrideService", () => { return Cc["@mozilla.org/security/certoverride;1"] .getService(Ci.nsICertOverrideService); }); DevToolsUtils.defineLazyGetter(this, "nssErrorsService", () => { return Cc["@mozilla.org/nss_errors_service;1"] .getService(Ci.nsINSSErrorsService); }); DevToolsUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm"); const DBG_STRINGS_URI = "chrome://global/locale/devtools/debugger.properties"; let DebuggerSocket = {}; /** * Connects to a debugger server socket. * * @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. */ 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! s.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 2); // openOutputStream may throw NS_ERROR_NOT_INITIALIZED if we hit some race // where the nsISocketTransport gets shutdown in between its instantiation and // the call to this method. let input; let output; try { input = s.openInputStream(0, 0, 0); output = s.openOutputStream(0, 0, 0); } catch(e) { DevToolsUtils.reportException("_attemptConnect", e); throw e; } 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 */); } /** * Creates a new socket listener for remote connections to the DebuggerServer. * This helps contain and organize the parts of the server that may differ or * are particular to one given listener mechanism vs. another. */ function SocketListener() {} /** * Prompt the user to accept or decline the incoming connection. This is the * default implementation that products embedding the debugger server may * choose to override. A separate security handler can be specified for each * socket via |allowConnection| on a socket listener instance. * * @return true if the connection should be permitted, false otherwise */ SocketListener.defaultAllowConnection = () => { let bundle = Services.strings.createBundle(DBG_STRINGS_URI); let title = bundle.GetStringFromName("remoteIncomingPromptTitle"); let msg = bundle.GetStringFromName("remoteIncomingPromptMessage"); let disableButton = bundle.GetStringFromName("remoteIncomingPromptDisable"); let prompt = Services.prompt; let flags = prompt.BUTTON_POS_0 * prompt.BUTTON_TITLE_OK + prompt.BUTTON_POS_1 * prompt.BUTTON_TITLE_CANCEL + prompt.BUTTON_POS_2 * prompt.BUTTON_TITLE_IS_STRING + prompt.BUTTON_POS_1_DEFAULT; let result = prompt.confirmEx(null, title, msg, flags, null, null, disableButton, null, { value: false }); if (result === 0) { return true; } if (result === 2) { DebuggerServer.closeAllListeners(); Services.prefs.setBoolPref("devtools.debugger.remote-enabled", false); } return false; }; SocketListener.prototype = { /* Socket Options */ /** * The port or path to listen on. * * If given an integer, the port to listen on. Use -1 to choose any available * port. Otherwise, the path to the unix socket domain file to listen on. */ portOrPath: null, /** * Prompt the user to accept or decline the incoming connection. The default * implementation is used unless this is overridden on a particular socket * listener instance. * * @return true if the connection should be permitted, false otherwise */ allowConnection: SocketListener.defaultAllowConnection, /** * Controls whether this listener is announced via the service discovery * mechanism. */ discoverable: false, /** * Controls whether this listener's transport uses encryption. */ encryption: false, /** * Validate that all options have been set to a supported configuration. */ _validateOptions: function() { if (this.portOrPath === null) { throw new Error("Must set a port / path to listen on."); } if (this.discoverable && !Number(this.portOrPath)) { throw new Error("Discovery only supported for TCP sockets."); } }, /** * Listens on the given port or socket file for remote debugger connections. */ open: function() { this._validateOptions(); DebuggerServer._addListener(this); let flags = Ci.nsIServerSocket.KeepWhenOffline; // A preference setting can force binding on the loopback interface. if (Services.prefs.getBoolPref("devtools.debugger.force-local")) { flags |= Ci.nsIServerSocket.LoopbackOnly; } let self = this; return Task.spawn(function*() { let backlog = 4; self._socket = self._createSocketInstance(); if (self.isPortBased) { let port = Number(self.portOrPath); self._socket.initSpecialConnection(port, flags, backlog); } else { let file = nsFile(self.portOrPath); if (file.exists()) { file.remove(false); } self._socket.initWithFilename(file, parseInt("666", 8), backlog); } 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, encryption: this.encryption }); } }).catch(e => { dumpn("Could not start debugging listener on '" + this.portOrPath + "': " + e); this.close(); }); }, _createSocketInstance: function() { if (this.encryption) { return Cc["@mozilla.org/network/tls-server-socket;1"] .createInstance(Ci.nsITLSServerSocket); } return Cc["@mozilla.org/network/server-socket;1"] .createInstance(Ci.nsIServerSocket); }, _setAdditionalSocketOptions: Task.async(function*() { if (this.encryption) { this._socket.serverCert = yield cert.local.getOrCreate(); this._socket.setSessionCache(false); this._socket.setSessionTickets(false); let requestCert = Ci.nsITLSServerSocket.REQUEST_NEVER; this._socket.setRequestClientCertificate(requestCert); } }), /** * Closes the SocketListener. Notifies the server to remove the listener from * the set of active SocketListeners. */ close: function() { if (this.discoverable && this.port) { discovery.removeService("devtools"); } if (this._socket) { this._socket.close(); this._socket = null; } 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.isPortBased || !this._socket) { return null; } return this._socket.port; }, // nsIServerSocketListener implementation onSocketAccepted: DevToolsUtils.makeInfallible(function(socket, socketTransport) { if (this.encryption) { new SecurityObserver(socketTransport); } if (Services.prefs.getBoolPref("devtools.debugger.prompt-connection") && !this.allowConnection()) { return; } dumpn("New debugging connection on " + socketTransport.host + ":" + socketTransport.port); let input = socketTransport.openInputStream(0, 0, 0); let output = socketTransport.openOutputStream(0, 0, 0); let transport = new DebuggerTransport(input, output); DebuggerServer._onConnection(transport); }, "SocketListener.onSocketAccepted"), onStopListening: function(socket, status) { dumpn("onStopListening, status: " + status); } }; // 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); }, // 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;