@microsoft/dev-tunnels-ssh
Version:
SSH library for Dev Tunnels
191 lines • 8.55 kB
JavaScript
"use strict";
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
Object.defineProperty(exports, "__esModule", { value: true });
exports.MultiChannelStream = void 0;
const vscode_jsonrpc_1 = require("vscode-jsonrpc");
const sshChannel_1 = require("./sshChannel");
const sshStream_1 = require("./sshStream");
const sshSession_1 = require("./sshSession");
const sshSessionConfiguration_1 = require("./sshSessionConfiguration");
const transportMessages_1 = require("./messages/transportMessages");
const trace_1 = require("./trace");
const connectionMessages_1 = require("./messages/connectionMessages");
const sshReportProgressEventArgs_1 = require("./events/sshReportProgressEventArgs");
/**
* Multiplexes multiple virtual streams (channels) over a single transport stream, using the
* SSH protocol while providing a simplified interface without any encryption or authentication.
*
* This class is a complement to `SecureStream`, which provides only the encryption and
* authentication functions of SSH.
*
* To communicate over multiple channels, two sides first establish a transport stream
* over a pipe, socket, or anything else. Then one side accepts a channel while the
* other side opens a channel. Either side can both open and accept channels over the
* same transport stream, as long as the other side does the complementary action.
*/
class MultiChannelStream {
/**
* Creates a new multi-channel stream over an underlying transport stream.
* @param transportStream Stream that is used to multiplex all the channels.
*/
constructor(transportStream) {
this.transportStream = transportStream;
this.disposed = false;
this.disposables = [];
this.reportProgressEmitter = new vscode_jsonrpc_1.Emitter();
/**
* Event that is raised to report connection progress.
*
* See `Progress` for a description of the different progress events that can be reported.
*/
this.onReportProgress = this.reportProgressEmitter.event;
/**
* Gets or sets the maximum window size for channels within the multi-channel stream.
* @see `SshChannel.maxWindowSize`
*/
this.channelMaxWindowSize = sshChannel_1.SshChannel.defaultMaxWindowSize;
this.closedEmitter = new vscode_jsonrpc_1.Emitter();
this.onClosed = this.closedEmitter.event;
this.channelOpeningEmitter = new vscode_jsonrpc_1.Emitter();
this.onChannelOpening = this.channelOpeningEmitter.event;
if (!transportStream)
throw new TypeError('transportStream is required.');
const noSecurityConfig = new sshSessionConfiguration_1.SshSessionConfiguration(false);
this.session = new sshSession_1.SshSession(noSecurityConfig);
this.session.onReportProgress((args) => this.raiseReportProgress(args.progress, args.sessionNumber), this, this.disposables);
this.session.onClosed(this.onSessionClosed, this, this.disposables);
this.session.onChannelOpening(this.onSessionChannelOpening, this, this.disposables);
}
get trace() {
return this.session.trace;
}
set trace(trace) {
this.session.trace = trace;
}
raiseReportProgress(progress, sessionNumber) {
const args = new sshReportProgressEventArgs_1.SshReportProgressEventArgs(progress, sessionNumber);
this.reportProgressEmitter.fire(args);
}
get isClosed() {
return this.disposed || this.session.isClosed;
}
/**
* 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) {
await this.session.connect(this.transportStream, cancellation);
}
/**
* Asynchronously waits for the other side to open a channel.
* @param channelType optional channel type
* @param cancellation optional cancellation token.
*/
async acceptChannel(channelType, cancellation) {
await this.session.connect(this.transportStream, cancellation);
const channel = await this.session.acceptChannel(channelType, cancellation);
return channel;
}
/**
* Asynchronously waits for the other side to open a channel.
* @param channelType optional channel type
* @param cancellation optional cancellation token.
*/
async acceptStream(channelType, cancellation) {
return this.createStream(await this.acceptChannel(channelType, cancellation));
}
/**
* Opens a channel and asynchronously waits for the other side to accept it.
* @param channelType optional channel type
* @param cancellation optional cancellation token.
*/
async openChannel(channelType, cancellation) {
await this.session.connect(this.transportStream, cancellation);
const openMessage = new connectionMessages_1.ChannelOpenMessage();
openMessage.channelType = channelType !== null && channelType !== void 0 ? channelType : sshChannel_1.SshChannel.sessionChannelType;
openMessage.maxWindowSize = this.channelMaxWindowSize;
const channel = await this.session.openChannel(openMessage, null, cancellation);
return channel;
}
/**
* Opens a channel and asynchronously waits for the other side to accept it.
* @param channelType optional channel type
* @param cancellation optional cancellation token.
*/
async openStream(channelType, cancellation) {
return this.createStream(await this.openChannel(channelType, cancellation));
}
/**
* Creates a stream instance for a channel. May be overridden to create a `SshStream` subclass.
*/
createStream(channel) {
return new sshStream_1.SshStream(channel);
}
/**
* Connects, waits until the session closes or `cancellation` is cancelled, and then disposes the
* session and the transport stream.
* @param cancellation optional cancellation token.
*/
async connectAndRunUntilClosed(cancellation) {
const disposables = [];
const sessionClosedPromise = new Promise((resolve, reject) => {
cancellation === null || cancellation === void 0 ? void 0 : cancellation.onCancellationRequested(reject, null, disposables);
this.session.onClosed(resolve, null, disposables);
});
try {
await this.connect(cancellation);
await sessionClosedPromise;
}
finally {
disposables.forEach((d) => d.dispose());
await this.close();
}
}
dispose() {
if (!this.disposed) {
this.disposed = true;
this.session.dispose();
this.unsubscribe();
try {
if (this.transportStream)
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, 'SshSession disposed');
this.session.dispose();
this.unsubscribe();
await this.transportStream.close();
}
}
onSessionClosed(e) {
this.unsubscribe();
this.closedEmitter.fire(e);
}
onSessionChannelOpening(e) {
if (e.isRemoteRequest) {
e.channel.maxWindowSize = this.channelMaxWindowSize;
}
this.channelOpeningEmitter.fire(e);
}
unsubscribe() {
this.disposables.forEach((d) => d.dispose());
this.disposables = [];
}
}
exports.MultiChannelStream = MultiChannelStream;
//# sourceMappingURL=multiChannelStream.js.map