UNPKG

@microsoft/dev-tunnels-ssh

Version:
486 lines 23.2 kB
"use strict"; // // Copyright (c) Microsoft Corporation. All rights reserved. // Object.defineProperty(exports, "__esModule", { value: true }); exports.SshChannel = void 0; const vscode_jsonrpc_1 = require("vscode-jsonrpc"); const serviceActivation_1 = require("./services/serviceActivation"); const connectionMessages_1 = require("./messages/connectionMessages"); const transportMessages_1 = require("./messages/transportMessages"); const channelMetrics_1 = require("./metrics/channelMetrics"); const promiseCompletionSource_1 = require("./util/promiseCompletionSource"); const errors_1 = require("./errors"); const sshRequestEventArgs_1 = require("./events/sshRequestEventArgs"); const sshChannelClosedEventArgs_1 = require("./events/sshChannelClosedEventArgs"); const cancellation_1 = require("./util/cancellation"); const semaphore_1 = require("./util/semaphore"); const trace_1 = require("./trace"); const pipeExtensions_1 = require("./pipeExtensions"); const queue_1 = require("./util/queue"); /** * Represents a channel on an SSH session. A session may include multiple channels, which * are multiplexed over the connection. Each channel within a session has a unique integer ID. */ class SshChannel { /* @internal */ constructor(connectionService, channelType, channelId, remoteChannelId, remoteMaxWindowSize, remoteMaxPacketSize, openMessage, openConfirmationMessage) { this.connectionService = connectionService; this.channelType = channelType; this.channelId = channelId; this.remoteChannelId = remoteChannelId; this.openMessage = openMessage; this.openConfirmationMessage = openConfirmationMessage; this.remoteClosed = false; this.localClosed = false; this.sentEof = false; this.disposed = false; this.openSendingWindowCompletionSource = null; this.requestCompletionSources = new queue_1.Queue(); this.sendSemaphore = new semaphore_1.Semaphore(0); /** * Gets an object that reports measurements about the channel. */ this.metrics = new channelMetrics_1.ChannelMetrics(); this.dataReceivedEmitter = new vscode_jsonrpc_1.Emitter(); this.extendedDataReceivedEmitter = new vscode_jsonrpc_1.Emitter(); /** * Event raised when a data message is received on the channel. * * Users of a channel MUST add a `onDataReceived` event handler immediately after a * channel is opened/accepted, or else all session communication will be blocked. * (The `SshStream` class does this automatically.) * * The event handler must call `adjustWindow` when the data has been consumed, * to notify the remote side that it may send more data. */ this.onDataReceived = this.dataReceivedEmitter.event; this.onExtendedDataReceived = this.extendedDataReceivedEmitter.event; this.eofEmitter = new vscode_jsonrpc_1.Emitter(); /** * Event raised when an EOF message is received on the channel. */ this.onEof = this.eofEmitter.event; this.closedEmitter = new vscode_jsonrpc_1.Emitter(); this.onClosed = this.closedEmitter.event; this.requestEmitter = new vscode_jsonrpc_1.Emitter(); this.onRequest = this.requestEmitter.event; /** * Gets or sets a value indicating whether `maxWindowSize is locked, so that it cannot be * changed after the channel is opened. */ /* @internal */ this.isMaxWindowSizeLocked = false; this.remoteWindowSize = remoteMaxWindowSize; this.maxWindowSizeValue = SshChannel.defaultMaxWindowSize; this.windowSize = this.maxWindowSizeValue; this.maxPacketSize = Math.min(remoteMaxPacketSize, SshChannel.defaultMaxPacketSize); } get session() { return this.connectionService.session; } get isClosed() { return this.localClosed || this.remoteClosed; } /** * Gets the maximum window size for received data. The other side will not send more * data than the window size until it receives an acknowledgement that some of the data was * received and processed by this side. */ get maxWindowSize() { return this.maxWindowSizeValue; } /** * Sets the maximum window size for received data. The other side will not send more * data than the window size until it receives an acknowledgement that some of the data was * received and processed by this side. * * The default value is `defaultMaxWindowSize`. The value may be configured for a channel * opened by this side by setting `ChannelOpenMessage.maxWindowSize` in the message object * passed to `SshSession.openChannel()`, or for a channel opened by the other side by * assigning to this property while handling the `SshSession.onChannelOpening` event. * Changing the maximum window size at any other time is not valid because the other * side would not be aware of the change. */ set maxWindowSize(value) { if (this.isMaxWindowSizeLocked) { throw new Error('Cannot change the max window size after opening the channel.'); } if (value < this.maxPacketSize) { throw new Error('Maximum window size cannot be less than maximum packet size.'); } this.maxWindowSizeValue = value; } /** * Sends a channel request and waits for a response. * * Note if `wantReply` is `false`, this method returns `true` immediately after sending the * request, without waiting for a reply. * * @returns The authorization status of the response; if false, the other side denied the * request. * @throws `ObjectDisposedError` if the channel was closed before sending the request. * @throws `SshChannelError` if the channel was closed while waiting for a reply to the request. */ async request(request, cancellation) { if (!request) throw new TypeError('Request is required.'); if (this.disposed) throw new errors_1.ObjectDisposedError(this); request.recipientChannel = this.remoteChannelId; if (!request.wantReply) { // If a reply is not requested, there's no need to set up a completion source. await this.session.sendMessage(request, cancellation); return true; } const requestCompletionSource = new promiseCompletionSource_1.PromiseCompletionSource(); if (cancellation) { if (cancellation.isCancellationRequested) throw new cancellation_1.CancellationError(); cancellation.onCancellationRequested(() => { requestCompletionSource.reject(new cancellation_1.CancellationError()); }); } this.requestCompletionSources.enqueue(requestCompletionSource); await this.session.sendMessage(request, cancellation); return await requestCompletionSource.promise; } async send(data, cancellation) { return this.sendCommon(data, undefined, cancellation); } async sendExtendedData(dataTypeCode, data, cancellation) { return this.sendCommon(data, dataTypeCode, cancellation); } async sendCommon(data, extendedDataType, cancellation) { if (this.disposed) throw new errors_1.ObjectDisposedError(this); if (data.length === 0) { await this.sendEof(); return; } else if (this.sentEof) { throw new Error('Cannot send more data after EOF.'); } // Prevent out-of-order message chunks even if the caller does not await. // Also don't send until the channel is fully opened. await this.sendSemaphore.wait(cancellation); try { let offset = 0; let count = data.length; while (count > 0) { let packetSize = Math.min(Math.min(this.remoteWindowSize, this.maxPacketSize), count); while (packetSize === 0) { if (!this.openSendingWindowCompletionSource) { this.openSendingWindowCompletionSource = new promiseCompletionSource_1.PromiseCompletionSource(); } this.session.trace(trace_1.TraceLevel.Warning, trace_1.SshTraceEventIds.channelWaitForWindowAdjust, `${this} send window is full. Waiting for window adjustment before sending.`); await (0, cancellation_1.withCancellation)(this.openSendingWindowCompletionSource.promise, cancellation); this.openSendingWindowCompletionSource = null; packetSize = Math.min(Math.min(this.remoteWindowSize, this.maxPacketSize), count); } let msg; if (extendedDataType !== undefined) { msg = new connectionMessages_1.ChannelExtendedDataMessage(); msg.dataTypeCode = extendedDataType; } else { msg = new connectionMessages_1.ChannelDataMessage(); } msg.recipientChannel = this.remoteChannelId; // Unfortunately the data must be copied to a new buffer at this point // to ensure it is still available to be re-sent later in case of disconnect. msg.data = Buffer.from(data.slice(offset, offset + packetSize)); await this.session.sendMessage(msg, cancellation); this.remoteWindowSize -= packetSize; count -= packetSize; offset += packetSize; this.metrics.addBytesSent(packetSize); } } finally { this.sendSemaphore.tryRelease(); } } /* @internal */ enableSending() { this.sendSemaphore.tryRelease(); } async sendEof(cancellation) { if (this.sentEof) { return; } await this.sendSemaphore.wait(cancellation); try { this.sentEof = true; const msg = new connectionMessages_1.ChannelEofMessage(); msg.recipientChannel = this.remoteChannelId; await this.session.sendMessage(msg, cancellation); } finally { this.sendSemaphore.tryRelease(); } } /* @internal */ async handleRequest(request, cancellation) { if (!request.requestType) { throw new errors_1.SshConnectionError('Channel request type not specified.', transportMessages_1.SshDisconnectReason.protocolError); } if (request.requestType === connectionMessages_1.ChannelRequestType.exitStatus) { const signal = new connectionMessages_1.ChannelSignalMessage(); request.convertTo(signal); this.exitStatus = signal.exitStatus; return true; } else if (request.requestType === connectionMessages_1.ChannelRequestType.exitSignal) { const signal = new connectionMessages_1.ChannelSignalMessage(); request.convertTo(signal); this.exitSignal = signal.exitSignal; this.exitErrorMessage = signal.errorMessage; return true; } else if (request.requestType === connectionMessages_1.ChannelRequestType.signal) { const signal = new connectionMessages_1.ChannelSignalMessage(); request.convertTo(signal); } const args = new sshRequestEventArgs_1.SshRequestEventArgs(request.requestType, request, this.session.principal, cancellation); const serviceType = (0, serviceActivation_1.findService)(this.session.config.services, (a) => (!a.channelType || a.channelType === this.channelType) && a.channelRequest === request.requestType); await this.sendSemaphore.wait(cancellation); try { let response = null; if (serviceType) { // A service was configured for activation via this session request type. const service = this.session.activateService(serviceType); // `onChannelRequest` should really be 'protected internal'. await service.onChannelRequest(this, args, cancellation); } else { this.requestEmitter.fire(args); } // TODO: do not block requests in TS (similar to CS) // see https://dev.azure.com/devdiv/DevDiv/_git/SSH/commit/0b84a48811e2f015107c73bf4584b6c3b676a6de if (args.responsePromise) { response = await args.responsePromise; args.isAuthorized = response instanceof connectionMessages_1.ChannelSuccessMessage; } if (request.wantReply) { if (args.isAuthorized) { response = response !== null && response !== void 0 ? response : new connectionMessages_1.ChannelSuccessMessage(); response.recipientChannel = this.remoteChannelId; } else { response = response !== null && response !== void 0 ? response : new connectionMessages_1.ChannelFailureMessage(); response.recipientChannel = this.remoteChannelId; } await this.session.sendMessage(response, cancellation); } } finally { this.sendSemaphore.tryRelease(); } return args.isAuthorized || false; } /* @internal */ handleResponse(result) { const requestCompletionSource = this.requestCompletionSources.dequeue(); if (requestCompletionSource) { requestCompletionSource.resolve(result); } } /* @internal */ handleDataReceived(data) { this.metrics.addBytesReceived(data.length); // DataReceived handler is to adjust the window when it's done with the data. this.dataReceivedEmitter.fire(data); } handleExtendedDataReceived(data) { this.metrics.addBytesReceived(data.data.length); this.extendedDataReceivedEmitter.fire(data); } /** * Adjusts the local receiving window size by the specified amount, notifying * the remote side that it is free to send more data. * * This method MUST be called either immediately or eventually by the * `onDataReceived` event handler as incoming data is processed. */ adjustWindow(messageLength) { if (this.disposed) return; this.windowSize -= messageLength; if (this.windowSize <= this.maxWindowSizeValue / 2) { const windowAdjustMessage = new connectionMessages_1.ChannelWindowAdjustMessage(); windowAdjustMessage.recipientChannel = this.remoteChannelId; windowAdjustMessage.bytesToAdd = this.maxWindowSizeValue - this.windowSize; this.windowSize = this.maxWindowSizeValue; this.session.sendMessage(windowAdjustMessage).catch((e) => { this.session.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.channelWindowAdjustFailed, `Error sending window adjust message: ${e.message}`, e); }); } } /* @internal */ adjustRemoteWindow(bytesToAdd) { this.remoteWindowSize += bytesToAdd; if (this.openSendingWindowCompletionSource) { this.openSendingWindowCompletionSource.resolve(undefined); } } /* @internal */ handleEof() { this.session.trace(trace_1.TraceLevel.Info, trace_1.SshTraceEventIds.channelEofReceived, `${this} EOF received.`); this.eofEmitter.fire(); } close(exitStatusOrSignal, errorMessage, cancellation) { if (exitStatusOrSignal instanceof Error) { const error = exitStatusOrSignal; if (!this.localClosed) { this.localClosed = true; this.session.trace(trace_1.TraceLevel.Info, trace_1.SshTraceEventIds.channelClosed, `${this} Closed: ${error.message}`); this.closedEmitter.fire(new sshChannelClosedEventArgs_1.SshChannelClosedEventArgs(error)); } this.disposeInternal(); return; } if (typeof exitStatusOrSignal === 'number') { return this.closeWithStatus(exitStatusOrSignal, errorMessage); } else if (typeof exitStatusOrSignal === 'string') { return this.closeWithSignal(exitStatusOrSignal, errorMessage, cancellation); } else { return this.closeDefault(exitStatusOrSignal); } } async closeDefault(cancellation) { if (!this.remoteClosed && !this.localClosed) { this.remoteClosed = true; await this.sendSemaphore.wait(cancellation); try { const closeMessage = new connectionMessages_1.ChannelCloseMessage(); closeMessage.recipientChannel = this.remoteChannelId; await this.session.sendMessage(closeMessage); } catch (e) { // The session was already closed. } finally { this.sendSemaphore.tryRelease(); } } if (!this.localClosed) { this.localClosed = true; const closedMessage = this.raiseClosedEvent(); } this.disposeInternal(); } async closeWithStatus(exitStatus, cancellation) { if (!this.remoteClosed && !this.localClosed) { this.exitStatus = exitStatus; const signalMessage = new connectionMessages_1.ChannelSignalMessage(); signalMessage.recipientChannel = this.remoteChannelId; signalMessage.exitStatus = exitStatus; await this.session.sendMessage(signalMessage); } await this.closeDefault(cancellation); } async closeWithSignal(exitSignal, errorMessage, cancellation) { if (!this.remoteClosed && !this.localClosed) { this.exitSignal = exitSignal; this.exitErrorMessage = errorMessage; const signalMessage = new connectionMessages_1.ChannelSignalMessage(); signalMessage.recipientChannel = this.remoteChannelId; signalMessage.exitSignal = exitSignal; signalMessage.errorMessage = errorMessage; await this.session.sendMessage(signalMessage); } await this.closeDefault(cancellation); } /* @internal */ handleClose() { if (!this.localClosed) { this.localClosed = true; const closedMessage = this.raiseClosedEvent(true); } this.disposeInternal(); } raiseClosedEvent(closedByRemote = false) { const metricsMessage = ` (S: ${this.metrics.bytesSent}, R: ${this.metrics.bytesReceived})`; const originMessage = closedByRemote ? 'remotely' : 'locally'; let closedMessage; let args; if (typeof this.exitStatus !== 'undefined') { args = new sshChannelClosedEventArgs_1.SshChannelClosedEventArgs(this.exitStatus); closedMessage = `${this} closed ${originMessage}: status=${this.exitStatus}`; } else if (typeof this.exitSignal !== 'undefined') { args = new sshChannelClosedEventArgs_1.SshChannelClosedEventArgs(this.exitSignal, this.exitErrorMessage); closedMessage = `${this} closed ${originMessage}: signal=${this.exitSignal} ${this.exitErrorMessage}`; } else { args = new sshChannelClosedEventArgs_1.SshChannelClosedEventArgs(); closedMessage = `${this} closed ${originMessage}.`; } this.session.trace(trace_1.TraceLevel.Info, trace_1.SshTraceEventIds.channelClosed, closedMessage + metricsMessage); this.closedEmitter.fire(args); return closedMessage; } dispose() { if (!this.disposed && !this.localClosed) { if (!this.remoteClosed) { this.remoteClosed = true; const closeMessage = new connectionMessages_1.ChannelCloseMessage(); closeMessage.recipientChannel = this.remoteChannelId; this.session.sendMessage(closeMessage).catch((e) => { // The session was already closed, or some other sending error occurred. // The details have already been traced. }); } const message = this.session.isClosed ? `${this.session} closed.` : `${this} disposed.`; this.session.trace(trace_1.TraceLevel.Info, trace_1.SshTraceEventIds.channelClosed, message); const args = new sshChannelClosedEventArgs_1.SshChannelClosedEventArgs('SIGABRT', message); this.localClosed = true; this.closedEmitter.fire(args); } this.disposeInternal(); } disposeInternal() { if (this.disposed) return; this.disposed = true; this.cancelPendingRequests(); this.connectionService.removeChannel(this); this.sendSemaphore.dispose(); } /** * Pipes one SSH channel into another, relaying all data between them. * @param toChannel Channel to which the current channel will be connected via the pipe. * @returns A promise that resolves when the channels are closed. */ pipe(toChannel) { return pipeExtensions_1.PipeExtensions.pipeChannel(this, toChannel); } cancelPendingRequests() { for (const completion of this.requestCompletionSources) { completion.resolve(false); } } toString() { return `SshChannel(Type: ${this.channelType}, Id: ${this.channelId}, RemoteId: ${this.remoteChannelId})`; } } exports.SshChannel = SshChannel; SshChannel.sessionChannelType = 'session'; /** * Default maximum packet size. Channel data payloads larger than the max packet size will * be broken into chunks before sending. The actual `maxPacketSize` may be smaller (but * never larger) than the default if requested by the other side. */ SshChannel.defaultMaxPacketSize = connectionMessages_1.ChannelOpenMessage.defaultMaxPacketSize; /** * Default maximum window size for received data. The other side will not send more data than * the window size until it receives an acknowledgement that some of the data was received and * processed by this side. A non-default `maxWindowSize` may be configured at the time of * opening the channel. */ SshChannel.defaultMaxWindowSize = connectionMessages_1.ChannelOpenMessage.defaultMaxWindowSize; //# sourceMappingURL=sshChannel.js.map