1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 var { Ci, Cc, CC, Cr } = require("chrome");
9 // Ensure PSM is initialized to support TLS sockets
10 Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
12 var Services = require("Services");
13 var promise = require("promise");
14 var defer = require("devtools/shared/defer");
15 var DevToolsUtils = require("devtools/shared/DevToolsUtils");
16 var { dumpn, dumpv } = DevToolsUtils;
17 loader.lazyRequireGetter(
20 "devtools/server/socket/websocket-server"
22 loader.lazyRequireGetter(
25 "devtools/shared/transport/transport",
28 loader.lazyRequireGetter(
30 "WebSocketDebuggerTransport",
31 "devtools/shared/transport/websocket-transport"
33 loader.lazyRequireGetter(
36 "devtools/shared/discovery/discovery"
38 loader.lazyRequireGetter(this, "cert", "devtools/shared/security/cert");
39 loader.lazyRequireGetter(
42 "devtools/shared/security/auth",
45 loader.lazyRequireGetter(
47 "AuthenticationResult",
48 "devtools/shared/security/auth",
51 loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
53 DevToolsUtils.defineLazyGetter(this, "nsFile", () => {
54 return CC("@mozilla.org/file/local;1", "nsIFile", "initWithPath");
57 DevToolsUtils.defineLazyGetter(this, "socketTransportService", () => {
58 return Cc["@mozilla.org/network/socket-transport-service;1"].getService(
59 Ci.nsISocketTransportService
63 DevToolsUtils.defineLazyGetter(this, "certOverrideService", () => {
64 return Cc["@mozilla.org/security/certoverride;1"].getService(
65 Ci.nsICertOverrideService
69 DevToolsUtils.defineLazyGetter(this, "nssErrorsService", () => {
70 return Cc["@mozilla.org/nss_errors_service;1"].getService(
71 Ci.nsINSSErrorsService
75 var DebuggerSocket = {};
78 * Connects to a devtools server socket.
81 * The host name or IP address of the devtools server.
83 * The port number of the devtools server.
84 * @param encryption boolean (optional)
85 * Whether the server requires encryption. Defaults to false.
86 * @param webSocket boolean (optional)
87 * Whether to use WebSocket protocol to connect. Defaults to false.
88 * @param authenticator Authenticator (optional)
89 * |Authenticator| instance matching the mode in use by the server.
90 * Defaults to a PROMPT instance if not supplied.
91 * @param cert object (optional)
92 * The server's cert details. Used with OOB_CERT authentication.
94 * Resolved to a DebuggerTransport instance.
96 DebuggerSocket.connect = async function(settings) {
97 // Default to PROMPT |Authenticator| instance if not supplied
98 if (!settings.authenticator) {
99 settings.authenticator = new (Authenticators.get().Client)();
101 _validateSettings(settings);
102 // eslint-disable-next-line no-shadow
103 const { host, port, encryption, authenticator, cert } = settings;
104 const transport = await _getTransport(settings);
105 await authenticator.authenticate({
112 transport.connectionSettings = settings;
117 * Validate that the connection settings have been set to a supported configuration.
119 function _validateSettings(settings) {
120 const { encryption, webSocket, authenticator } = settings;
122 if (webSocket && encryption) {
123 throw new Error("Encryption not supported on WebSocket transport");
125 authenticator.validateSettings(settings);
129 * Try very hard to create a DevTools transport, potentially making several
130 * connect attempts in the process.
133 * The host name or IP address of the devtools server.
135 * The port number of the devtools server.
136 * @param encryption boolean (optional)
137 * Whether the server requires encryption. Defaults to false.
138 * @param webSocket boolean (optional)
139 * Whether to use WebSocket protocol to connect to the server. Defaults to false.
140 * @param authenticator Authenticator
141 * |Authenticator| instance matching the mode in use by the server.
142 * Defaults to a PROMPT instance if not supplied.
143 * @param cert object (optional)
144 * The server's cert details. Used with OOB_CERT authentication.
145 * @return transport DebuggerTransport
146 * A possible DevTools transport (if connection succeeded and streams
147 * are actually alive and working)
149 var _getTransport = async function(settings) {
150 const { host, port, encryption, webSocket } = settings;
153 // Establish a connection and wait until the WebSocket is ready to send and receive
154 const socket = await new Promise((resolve, reject) => {
155 const s = new WebSocket(`ws://${host}:${port}`);
156 s.onopen = () => resolve(s);
157 s.onerror = err => reject(err);
160 return new WebSocketDebuggerTransport(socket);
163 let attempt = await _attemptTransport(settings);
164 if (attempt.transport) {
166 return attempt.transport;
169 // If the server cert failed validation, store a temporary override and make
171 if (encryption && attempt.certError) {
172 _storeCertOverride(attempt.s, host, port);
174 throw new Error("Connection failed");
177 attempt = await _attemptTransport(settings);
178 if (attempt.transport) {
180 return attempt.transport;
183 throw new Error("Connection failed even after cert override");
187 * Make a single attempt to connect and create a DevTools transport. This could
188 * fail if the remote host is unreachable, for example. If there is security
189 * error due to the use of self-signed certs, you should make another attempt
190 * after storing a cert override.
193 * The host name or IP address of the devtools server.
195 * The port number of the devtools server.
196 * @param encryption boolean (optional)
197 * Whether the server requires encryption. Defaults to false.
198 * @param authenticator Authenticator
199 * |Authenticator| instance matching the mode in use by the server.
200 * Defaults to a PROMPT instance if not supplied.
201 * @param cert object (optional)
202 * The server's cert details. Used with OOB_CERT authentication.
203 * @return transport DebuggerTransport
204 * A possible DevTools transport (if connection succeeded and streams
205 * are actually alive and working)
206 * @return certError boolean
207 * Flag noting if cert trouble caused the streams to fail
208 * @return s nsISocketTransport
209 * Underlying socket transport, in case more details are needed.
211 var _attemptTransport = async function(settings) {
212 const { authenticator } = settings;
213 // _attemptConnect only opens the streams. Any failures at that stage
214 // aborts the connection process immedidately.
215 const { s, input, output } = await _attemptConnect(settings);
217 // Check if the input stream is alive. If encryption is enabled, we need to
218 // watch out for cert errors by testing the input stream.
219 let alive, certError;
221 const results = await _isInputAlive(input);
222 alive = results.alive;
223 certError = results.certError;
225 // For other unexpected errors, like NS_ERROR_CONNECTION_REFUSED, we reach
231 dumpv("Server cert accepted? " + !certError);
233 // The |Authenticator| examines the connection as well and may determine it
234 // should be dropped.
237 authenticator.validateConnection({
240 encryption: settings.encryption,
247 transport = new DebuggerTransport(input, output);
249 // Something went wrong, close the streams.
254 return { transport, certError, s };
258 * Try to connect to a remote server socket.
260 * If successsful, the socket transport and its opened streams are returned.
261 * Typically, this will only fail if the host / port is unreachable. Other
262 * problems, such as security errors, will allow this stage to succeed, but then
263 * fail later when the streams are actually used.
264 * @return s nsISocketTransport
265 * Underlying socket transport, in case more details are needed.
266 * @return input nsIAsyncInputStream
267 * The socket's input stream.
268 * @return output nsIAsyncOutputStream
269 * The socket's output stream.
271 var _attemptConnect = async function({ host, port, encryption }) {
274 s = socketTransportService.createTransport(["ssl"], host, port, null);
276 s = socketTransportService.createTransport([], host, port, null);
278 // By default the CONNECT socket timeout is very long, 65535 seconds,
279 // so that if we race to be in CONNECT state while the server socket is still
280 // initializing, the connection is stuck in connecting state for 18.20 hours!
281 s.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 2);
283 // If encrypting, load the client cert now, so we can deliver it at just the
287 clientCert = await cert.local.getOrCreate();
290 const deferred = defer();
293 // Delay opening the input stream until the transport has fully connected.
294 // The goal is to avoid showing the user a client cert UI prompt when
295 // encryption is used. This prompt is shown when the client opens the input
296 // stream and does not know which client cert to present to the server. To
297 // specify a client cert programmatically, we need to access the transport's
298 // nsISSLSocketControl interface, which is not accessible until the transport
302 onTransportStatus(transport, status) {
303 if (status != Ci.nsISocketTransport.STATUS_CONNECTING_TO) {
307 const sslSocketControl = transport.securityInfo.QueryInterface(
308 Ci.nsISSLSocketControl
310 sslSocketControl.clientCert = clientCert;
313 input = s.openInputStream(0, 0, 0);
317 deferred.resolve({ s, input, output });
320 Services.tm.currentThread
323 // openOutputStream may throw NS_ERROR_NOT_INITIALIZED if we hit some race
324 // where the nsISocketTransport gets shutdown in between its instantiation and
325 // the call to this method.
327 output = s.openOutputStream(0, 0, 0);
332 deferred.promise.catch(e => {
339 DevToolsUtils.reportException("_attemptConnect", e);
342 return deferred.promise;
346 * Check if the input stream is alive. For an encrypted connection, it may not
347 * be if the client refuses the server's cert. A cert error is expected on
348 * first connection to a new host because the cert is self-signed.
350 function _isInputAlive(input) {
351 const deferred = defer();
354 onInputStreamReady(stream) {
357 deferred.resolve({ alive: true });
360 // getErrorClass may throw if you pass a non-NSS error
361 const errorClass = nssErrorsService.getErrorClass(e.result);
362 if (errorClass === Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT) {
363 deferred.resolve({ certError: true });
375 Services.tm.currentThread
377 return deferred.promise;
381 * To allow the connection to proceed with self-signed cert, we store a cert
382 * override. This implies that we take on the burden of authentication for
385 function _storeCertOverride(s, host, port) {
386 // eslint-disable-next-line no-shadow
387 const cert = s.securityInfo.QueryInterface(Ci.nsITransportSecurityInfo)
390 Ci.nsICertOverrideService.ERROR_UNTRUSTED |
391 Ci.nsICertOverrideService.ERROR_MISMATCH;
392 certOverrideService.rememberValidityOverride(
402 * Creates a new socket listener for remote connections to the DevToolsServer.
403 * This helps contain and organize the parts of the server that may differ or
404 * are particular to one given listener mechanism vs. another.
405 * This can be closed at any later time by calling |close|.
406 * If remote connections are disabled, an error is thrown.
408 * @param {DevToolsServer} devToolsServer
409 * @param {Object} socketOptions
410 * options of socket as follows
413 * Controls the |Authenticator| used, which hooks various socket steps to
414 * implement an authentication policy. It is expected that different use
415 * cases may override pieces of the |Authenticator|. See auth.js.
416 * We set the default |Authenticator|, which is |Prompt|.
418 * Controls whether this listener is announced via the service discovery
419 * mechanism. Defaults is false.
421 * Controls whether this listener's transport uses encryption.
424 * The port or path to listen on.
425 * If given an integer, the port to listen on. Use -1 to choose any available
426 * port. Otherwise, the path to the unix socket domain file to listen on.
429 * Whether to use WebSocket protocol. Defaults is false.
432 function SocketListener(devToolsServer, socketOptions) {
433 this._devToolsServer = devToolsServer;
435 // Set socket options with default value
436 this._socketOptions = {
438 socketOptions.authenticator || new (Authenticators.get().Server)(),
439 discoverable: !!socketOptions.discoverable,
440 encryption: !!socketOptions.encryption,
441 portOrPath: socketOptions.portOrPath || null,
442 webSocket: !!socketOptions.webSocket,
445 EventEmitter.decorate(this);
448 SocketListener.prototype = {
449 get authenticator() {
450 return this._socketOptions.authenticator;
454 return this._socketOptions.discoverable;
458 return this._socketOptions.encryption;
462 return this._socketOptions.portOrPath;
466 return this._socketOptions.webSocket;
470 * Validate that all options have been set to a supported configuration.
472 _validateOptions: function() {
473 if (this.portOrPath === null) {
474 throw new Error("Must set a port / path to listen on.");
476 if (this.discoverable && !Number(this.portOrPath)) {
477 throw new Error("Discovery only supported for TCP sockets.");
479 if (this.encryption && this.webSocket) {
480 throw new Error("Encryption not supported on WebSocket transport");
482 this.authenticator.validateOptions(this);
486 * Listens on the given port or socket file for remote debugger connections.
489 this._validateOptions();
490 this._devToolsServer.addSocketListener(this);
492 let flags = Ci.nsIServerSocket.KeepWhenOffline;
493 // A preference setting can force binding on the loopback interface.
494 if (Services.prefs.getBoolPref("devtools.debugger.force-local")) {
495 flags |= Ci.nsIServerSocket.LoopbackOnly;
499 return (async function() {
501 self._socket = self._createSocketInstance();
502 if (self.isPortBased) {
503 const port = Number(self.portOrPath);
504 self._socket.initSpecialConnection(port, flags, backlog);
505 } else if (self.portOrPath.startsWith("/")) {
506 const file = nsFile(self.portOrPath);
510 self._socket.initWithFilename(file, parseInt("666", 8), backlog);
512 // Path isn't absolute path, so we use abstract socket address
513 self._socket.initWithAbstractAddress(self.portOrPath, backlog);
515 await self._setAdditionalSocketOptions();
516 self._socket.asyncListen(self);
517 dumpn("Socket listening on: " + (self.port || self.portOrPath));
524 "Could not start debugging listener on '" +
533 _advertise: function() {
534 if (!this.discoverable || !this.port) {
538 const advertisement = {
540 encryption: this.encryption,
543 this.authenticator.augmentAdvertisement(this, advertisement);
545 discovery.addService("devtools", advertisement);
548 _createSocketInstance: function() {
549 if (this.encryption) {
550 return Cc["@mozilla.org/network/tls-server-socket;1"].createInstance(
551 Ci.nsITLSServerSocket
554 return Cc["@mozilla.org/network/server-socket;1"].createInstance(
559 async _setAdditionalSocketOptions() {
560 if (this.encryption) {
561 this._socket.serverCert = await cert.local.getOrCreate();
562 this._socket.setSessionTickets(false);
563 const requestCert = Ci.nsITLSServerSocket.REQUEST_NEVER;
564 this._socket.setRequestClientCertificate(requestCert);
566 this.authenticator.augmentSocketOptions(this, this._socket);
570 * Closes the SocketListener. Notifies the server to remove the listener from
571 * the set of active SocketListeners.
574 if (this.discoverable && this.port) {
575 discovery.removeService("devtools");
578 this._socket.close();
581 this._devToolsServer.removeSocketListener(this);
588 if (Services.prefs.getBoolPref("devtools.debugger.force-local")) {
595 * Gets whether this listener uses a port number vs. a path.
598 return !!Number(this.portOrPath);
602 * Gets the port that a TCP socket listener is listening on, or null if this
603 * is not a TCP socket (so there is no port).
606 if (!this.isPortBased || !this._socket) {
609 return this._socket.port;
613 if (!this._socket || !this._socket.serverCert) {
617 sha256: this._socket.serverCert.sha256Fingerprint,
621 onAllowedConnection(transport) {
622 dumpn("onAllowedConnection, transport: " + transport);
623 this.emit("accepted", transport, this);
626 // nsIServerSocketListener implementation
628 onSocketAccepted: DevToolsUtils.makeInfallible(function(
632 const connection = new ServerSocketConnection(this, socketTransport);
633 connection.once("allowed", this.onAllowedConnection.bind(this));
635 "SocketListener.onSocketAccepted"),
637 onStopListening: function(socket, status) {
638 dumpn("onStopListening, status: " + status);
642 // Client must complete TLS handshake within this window (ms)
643 loader.lazyGetter(this, "HANDSHAKE_TIMEOUT", () => {
644 return Services.prefs.getIntPref("devtools.remote.tls-handshake-timeout");
648 * A |ServerSocketConnection| is created by a |SocketListener| for each accepted
649 * incoming socket. This is a short-lived object used to implement
650 * authentication and verify encryption prior to handing off the connection to
651 * the |DevToolsServer|.
653 function ServerSocketConnection(listener, socketTransport) {
654 this._listener = listener;
655 this._socketTransport = socketTransport;
657 EventEmitter.decorate(this);
660 ServerSocketConnection.prototype = {
661 get authentication() {
662 return this._listener.authenticator.mode;
666 return this._socketTransport.host;
670 return this._socketTransport.port;
674 if (!this._clientCert) {
678 sha256: this._clientCert.sha256Fingerprint,
683 return this.host + ":" + this.port;
692 client.cert = this.cert;
699 host: this._listener.host,
700 port: this._listener.port,
702 if (this._listener.cert) {
703 server.cert = this._listener.cert;
709 * This is the main authentication workflow. If any pieces reject a promise,
710 * the connection is denied. If the entire process resolves successfully,
711 * the connection is finally handed off to the |DevToolsServer|.
714 dumpn("Debugging connection starting authentication on " + this.address);
716 this._listenForTLSHandshake();
717 await this._createTransport();
718 await this._awaitTLSHandshake();
719 await this._authenticate();
727 * We need to open the streams early on, as that is required in the case of
728 * TLS sockets to keep the handshake moving.
730 async _createTransport() {
731 const input = this._socketTransport.openInputStream(0, 0, 0);
732 const output = this._socketTransport.openOutputStream(0, 0, 0);
734 if (this._listener.webSocket) {
735 const socket = await WebSocketServer.accept(
736 this._socketTransport,
740 this._transport = new WebSocketDebuggerTransport(socket);
742 this._transport = new DebuggerTransport(input, output);
745 // Start up the transport to observe the streams in case they are closed
746 // early. This allows us to clean up our state as well.
747 this._transport.hooks = {
748 onClosed: reason => {
752 this._transport.ready();
756 * Set the socket's security observer, which receives an event via the
757 * |onHandshakeDone| callback when the TLS handshake completes.
759 _setSecurityObserver(observer) {
760 if (!this._socketTransport || !this._socketTransport.securityInfo) {
763 const connectionInfo = this._socketTransport.securityInfo.QueryInterface(
764 Ci.nsITLSServerConnectionInfo
766 connectionInfo.setSecurityObserver(observer);
770 * When encryption is used, we wait for the client to complete the TLS
771 * handshake before proceeding. The handshake details are validated in
774 _listenForTLSHandshake() {
775 this._handshakeDeferred = defer();
776 if (!this._listener.encryption) {
777 this._handshakeDeferred.resolve();
780 this._setSecurityObserver(this);
781 this._handshakeTimeout = setTimeout(
782 this._onHandshakeTimeout.bind(this),
787 _awaitTLSHandshake() {
788 return this._handshakeDeferred.promise;
791 _onHandshakeTimeout() {
792 dumpv("Client failed to complete TLS handshake");
793 this._handshakeDeferred.reject(Cr.NS_ERROR_NET_TIMEOUT);
796 // nsITLSServerSecurityObserver implementation
797 onHandshakeDone(socket, clientStatus) {
798 clearTimeout(this._handshakeTimeout);
799 this._setSecurityObserver(null);
800 dumpv("TLS version: " + clientStatus.tlsVersionUsed.toString(16));
801 dumpv("TLS cipher: " + clientStatus.cipherName);
802 dumpv("TLS key length: " + clientStatus.keyLength);
803 dumpv("TLS MAC length: " + clientStatus.macLength);
804 this._clientCert = clientStatus.peerCert;
806 * TODO: These rules should be really be set on the TLS socket directly, but
807 * this would need more platform work to expose it via XPCOM.
809 * Enforcing cipher suites here would be a bad idea, as we want TLS
810 * cipher negotiation to work correctly. The server already allows only
811 * Gecko's normal set of cipher suites.
813 if (clientStatus.tlsVersionUsed < Ci.nsITLSClientStatus.TLS_VERSION_1_2) {
814 this._handshakeDeferred.reject(Cr.NS_ERROR_CONNECTION_REFUSED);
818 this._handshakeDeferred.resolve();
821 async _authenticate() {
822 const result = await this._listener.authenticator.authenticate({
825 transport: this._transport,
828 case AuthenticationResult.DISABLE_ALL:
829 this._listener._devToolsServer.closeAllSocketListeners();
830 Services.prefs.setBoolPref("devtools.debugger.remote-enabled", false);
831 return promise.reject(Cr.NS_ERROR_CONNECTION_REFUSED);
832 case AuthenticationResult.DENY:
833 return promise.reject(Cr.NS_ERROR_CONNECTION_REFUSED);
834 case AuthenticationResult.ALLOW:
835 case AuthenticationResult.ALLOW_PERSIST:
836 return promise.resolve();
838 return promise.reject(Cr.NS_ERROR_CONNECTION_REFUSED);
843 if (this._destroyed) {
846 let errorName = result;
847 for (const name in Cr) {
848 if (Cr[name] === result) {
854 "Debugging connection denied on " + this.address + " (" + errorName + ")"
856 if (this._transport) {
857 this._transport.hooks = null;
858 this._transport.close(result);
860 this._socketTransport.close(result);
865 if (this._destroyed) {
868 dumpn("Debugging connection allowed on " + this.address);
869 this.emit("allowed", this._transport);
874 this._destroyed = true;
875 clearTimeout(this._handshakeTimeout);
876 this._setSecurityObserver(null);
877 this._listener = null;
878 this._socketTransport = null;
879 this._transport = null;
880 this._clientCert = null;
884 exports.DebuggerSocket = DebuggerSocket;
885 exports.SocketListener = SocketListener;