UNPKG

@microsoft/dev-tunnels-ssh

Version:
283 lines 13.2 kB
"use strict"; // // Copyright (c) Microsoft Corporation. All rights reserved. // Object.defineProperty(exports, "__esModule", { value: true }); exports.SecureStream = void 0; const vscode_jsonrpc_1 = require("vscode-jsonrpc"); const streams_1 = require("./streams"); const sshStream_1 = require("./sshStream"); const sshSessionConfiguration_1 = require("./sshSessionConfiguration"); const transportMessages_1 = require("./messages/transportMessages"); const trace_1 = require("./trace"); const sshClientSession_1 = require("./sshClientSession"); const sshServerSession_1 = require("./sshServerSession"); const errors_1 = require("./errors"); const stream_1 = require("stream"); const promiseCompletionSource_1 = require("./util/promiseCompletionSource"); /** * Establishes an end-to-end encrypted two-way authenticated data stream over an underlying * transport stream, using the SSH protocol but providing simplified interface that is limited to * a single duplex stream (channel). * * This class is a complement to `MultiChannelStream`, which provides only the channel-multiplexing * functions of SSH. * * To establish a secure connection, the two sides first establish an insecure transport stream * over a pipe, socket, or anything else. Then they encrypt and authenticate the connection * before beginning to send and receive data. */ class SecureStream extends stream_1.Duplex { /** * Creates a new encrypted and authenticated stream over an underlying transport stream. * @param transportStream Stream that is used to multiplex all the channels. * @param credentials Client or server credentials for authenticating the secure connection. * @param reconnectableSessions Optional parameter that enables the stream to be reconnected * with a new transport stream after a temporary disconnection. For a stream client it is * a boolean value; for a stream server it must be an array. */ constructor(transportStream, credentials, reconnectableSessions) { super({ write(chunk, encoding, cb) { this.connectCompletion.promise.then((stream) => { if (!stream) { cb(new errors_1.ObjectDisposedError('SecureStream')); } else { // eslint-disable-next-line no-underscore-dangle stream._write(chunk, encoding, cb); } }, cb); }, writev(chunks, cb) { this.connectCompletion.promise.then((stream) => { if (!stream) { cb(new errors_1.ObjectDisposedError('SecureStream')); } else { // eslint-disable-next-line no-underscore-dangle stream._writev(chunks, cb); } }, cb); }, final(cb) { this.connectCompletion.promise.then((stream) => { if (!stream) { cb(new errors_1.ObjectDisposedError('SecureStream')); } else { // eslint-disable-next-line no-underscore-dangle stream._final(cb); } }, cb); }, read(size) { this.connectCompletion.promise.then((stream) => { if (!stream) { this.push(null); // EOF } else { // eslint-disable-next-line no-underscore-dangle stream._read(size); } }, (e) => { // The error will be thrown from the connect() method. }); }, }); this.transportStream = transportStream; this.clientCredentials = null; this.serverCredentials = null; this.connectCompletion = new promiseCompletionSource_1.PromiseCompletionSource(); this.disposed = false; this.disposables = []; this.disconnectedEmitter = new vscode_jsonrpc_1.Emitter(); this.onDisconnected = this.disconnectedEmitter.event; this.closedEmitter = new vscode_jsonrpc_1.Emitter(); this.onClosed = this.closedEmitter.event; if (!transportStream) throw new TypeError('A transport stream is required.'); if (!credentials) throw new TypeError('Client or server credentials are required.'); const sessionConfig = new sshSessionConfiguration_1.SshSessionConfiguration(true); if (reconnectableSessions) { sessionConfig.protocolExtensions.push(sshSessionConfiguration_1.SshProtocolExtensionNames.sessionReconnect); } if ('username' in credentials) { if (typeof reconnectableSessions !== 'undefined' && typeof reconnectableSessions !== 'boolean') { throw new TypeError('SecureStream client reconnectable sessions must be a boolean.'); } this.clientCredentials = credentials; this.session = new sshClientSession_1.SshClientSession(sessionConfig); } else if (credentials.publicKeys) { if (typeof reconnectableSessions !== 'undefined' && !Array.isArray(reconnectableSessions)) { throw new TypeError('SecureStream server reconnectable sessions must be an array.'); } this.serverCredentials = credentials; this.session = new sshServerSession_1.SshServerSession(sessionConfig, reconnectableSessions); } else { throw new TypeError('Client or server credentials are required.'); } this.session.onDisconnected(this.onSessionDisconnected, this, this.disposables); this.session.onClosed(this.onSessionClosed, this, this.disposables); } get trace() { return this.session.trace; } set trace(trace) { this.session.trace = trace; } get isClosed() { return this.disposed || this.session.isClosed; } onAuthenticating(listener, thisArgs, disposables) { return this.session.onAuthenticating(listener, thisArgs, disposables); } /** * Initiates the SSH session over the transport stream by exchanging initial messages with the * remote peer. Waits for the protocol version exchange and key exchange. Additional message * processing is kicked off as a background promise chain. * @param cancellation optional cancellation token. */ async connect(cancellation) { let sessionConnected = false; try { if (this.serverCredentials) { const serverSession = this.session; serverSession.credentials = this.serverCredentials; } let stream = this.transportStream; if (stream instanceof stream_1.Duplex) { stream = new streams_1.NodeStream(stream); } await this.session.connect(stream, cancellation); sessionConnected = true; let channel = null; if (this.clientCredentials) { const clientSession = this.session; if (!(await clientSession.authenticateServer(cancellation))) { throw new errors_1.SshConnectionError('Server authentication failed.', transportMessages_1.SshDisconnectReason.hostKeyNotVerifiable); } if (!(await clientSession.authenticateClient(this.clientCredentials, cancellation))) { throw new errors_1.SshConnectionError('Client authentication failed.', transportMessages_1.SshDisconnectReason.noMoreAuthMethodsAvailable); } channel = await this.session.openChannel(cancellation); } else { channel = await this.session.acceptChannel(cancellation); } this.stream = this.createStream(channel); // Do not forward the 'readable' event because adding a listener causes a read. this.stream.on('data', (data) => this.emit('data', data)); this.stream.on('end', () => this.emit('end')); this.stream.on('close', () => this.emit('close')); this.stream.on('error', () => this.emit('error')); channel.onClosed(() => this.dispose()); this.connectCompletion.resolve(this.stream); } catch (e) { if (!(e instanceof Error)) throw e; if (e instanceof errors_1.ObjectDisposedError && this.session.isClosed) { // The session was closed while waiting for the channel. // This can happen in reconnect scenarios. this.connectCompletion.resolve(null); } else { let disconnectReason = e instanceof errors_1.SshConnectionError ? e.reason : undefined; disconnectReason !== null && disconnectReason !== void 0 ? disconnectReason : (disconnectReason = transportMessages_1.SshDisconnectReason.protocolError); await this.session.close(disconnectReason, e.message, e); this.connectCompletion.reject(e); throw e; } } } /** * Re-initiates the SSH session over a NEW transport stream by exchanging initial messages * with the remote server. Waits for the secure reconnect handshake to complete. Additional * message processing is kicked off as a background task chain. * * Applies only to a secure stream client. (The secure stream server handles reconnections * automatically during the session handshake.) */ async reconnect(transportStream, cancellation) { if (!(this.session instanceof sshClientSession_1.SshClientSession)) { throw new Error('Cannot reconnect SecureStream server.'); } if (!transportStream) throw new TypeError('A transport stream is required.'); this.transportStream = transportStream; let stream = this.transportStream; if (stream instanceof stream_1.Duplex) { stream = new streams_1.NodeStream(stream); } await this.session.reconnect(stream, cancellation); } /** * Creates a stream instance for a channel. May be overridden to create a `SshStream` subclass. */ createStream(channel) { return new sshStream_1.SshStream(channel); } dispose() { if (!this.disposed) { const sessionWasConnected = this.session.isConnected || this.session.isClosed; this.disposed = true; this.session.dispose(); this.unsubscribe(); try { // If the session did not connect yet, it doesn't know about the stream and // won't dispose it. So dispose it here. if (!sessionWasConnected && this.transportStream) { if (this.transportStream instanceof stream_1.Duplex) { this.transportStream.end(); this.transportStream.destroy(); } else { this.transportStream.close().catch((e) => { this.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.streamCloseError, `Error closing transport stream: ${e.message}`, e); }); } } } catch (e) { if (!(e instanceof Error)) throw e; this.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.streamCloseError, `Error closing transport stream: ${e.message}`, e); } } } async close() { if (!this.disposed) { this.disposed = true; await this.session.close(transportMessages_1.SshDisconnectReason.none, this.session.constructor.name + ' disposed.'); this.session.dispose(); this.unsubscribe(); if (this.transportStream instanceof stream_1.Duplex) { await new Promise((resolve) => { this.transportStream.end(resolve); }); } else { await this.transportStream.close(); } } } onSessionDisconnected() { this.disconnectedEmitter.fire(); } onSessionClosed(e) { this.unsubscribe(); this.closedEmitter.fire(e); } unsubscribe() { this.disposables.forEach((d) => d.dispose()); this.disposables = []; } } exports.SecureStream = SecureStream; //# sourceMappingURL=secureStream.js.map