@microsoft/dev-tunnels-ssh
Version:
SSH library for Dev Tunnels
182 lines • 8.76 kB
JavaScript
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
Object.defineProperty(exports, "__esModule", { value: true });
exports.PipeExtensions = void 0;
const buffer_1 = require("buffer");
const vscode_jsonrpc_1 = require("vscode-jsonrpc");
const transportMessages_1 = require("./messages/transportMessages");
const connectionMessages_1 = require("./messages/connectionMessages");
const promiseCompletionSource_1 = require("./util/promiseCompletionSource");
const errors_1 = require("./errors");
const trace_1 = require("./trace");
/**
* Extension methods for piping sessions and channels.
*
* Note this class is not exported from the package. Instead, the piping APIs are exposed via
* public methods on the `SshSession` and `SshChannel` classes. See those respective methods
* for API documentation.
*/
class PipeExtensions {
static async pipeSession(session, toSession) {
if (!session)
throw new TypeError('Session is required.');
if (!toSession)
throw new TypeError('Target session is required');
const endCompletion = new promiseCompletionSource_1.PromiseCompletionSource();
session.onRequest((e) => {
e.responsePromise = PipeExtensions.forwardSessionRequest(e, toSession, e.cancellation);
});
toSession.onRequest((e) => {
e.responsePromise = PipeExtensions.forwardSessionRequest(e, session, e.cancellation);
});
session.onChannelOpening((e) => {
if (e.isRemoteRequest) {
e.openingPromise = PipeExtensions.forwardChannel(e, toSession, e.cancellation);
}
});
toSession.onChannelOpening((e) => {
if (e.isRemoteRequest) {
e.openingPromise = PipeExtensions.forwardChannel(e, session, e.cancellation);
}
});
session.onClosed((e) => {
endCompletion.resolve(PipeExtensions.forwardSessionClose(toSession, e));
});
toSession.onClosed((e) => {
endCompletion.resolve(PipeExtensions.forwardSessionClose(session, e));
});
const endPromise = await endCompletion.promise;
await endPromise;
}
static async pipeChannel(channel, toChannel) {
if (!channel)
throw new TypeError('Channel is required.');
if (!toChannel)
throw new TypeError('Target channel is required');
const endCompletion = new promiseCompletionSource_1.PromiseCompletionSource();
let closed = false;
channel.onRequest((e) => {
e.responsePromise = PipeExtensions.forwardChannelRequest(e, toChannel, e.cancellation);
});
toChannel.onRequest((e) => {
e.responsePromise = PipeExtensions.forwardChannelRequest(e, channel, e.cancellation);
});
channel.onDataReceived((data) => {
void PipeExtensions.forwardData(channel, toChannel, data).catch();
});
toChannel.onDataReceived((data) => {
void PipeExtensions.forwardData(toChannel, channel, data).catch();
});
channel.onEof(() => {
void PipeExtensions.forwardData(channel, toChannel, buffer_1.Buffer.alloc(0)).catch();
});
toChannel.onEof(() => {
void PipeExtensions.forwardData(toChannel, channel, buffer_1.Buffer.alloc(0)).catch();
});
channel.onExtendedDataReceived((data) => {
void PipeExtensions.forwardExtendedData(channel, toChannel, data.dataTypeCode, data.data).catch();
});
toChannel.onExtendedDataReceived((data) => {
void PipeExtensions.forwardExtendedData(toChannel, channel, data.dataTypeCode, data.data).catch();
});
channel.onClosed((e) => {
if (!closed) {
closed = true;
endCompletion.resolve(PipeExtensions.forwardChannelClose(channel, toChannel, e));
}
});
toChannel.onClosed((e) => {
if (!closed) {
closed = true;
endCompletion.resolve(PipeExtensions.forwardChannelClose(toChannel, channel, e));
}
});
const endTask = await endCompletion.promise;
await endTask;
}
static async forwardSessionRequest(e, toSession, cancellation) {
// `SshSession.requestResponse()` always set `wantReply` to `true` internally and waits for a
// response, but since the message buffer is cached the updated `wantReply` value is not sent.
// Anyway, it's better to forward a no-reply message as another no-reply message, using
// `SshSession.request()` instead.
if (!e.request.wantReply) {
return toSession
.request(e.request, cancellation)
.then(() => new transportMessages_1.SessionRequestSuccessMessage());
}
return toSession.requestResponse(e.request, transportMessages_1.SessionRequestSuccessMessage, transportMessages_1.SessionRequestFailureMessage, cancellation);
}
static async forwardChannel(e, toSession, cancellation) {
var _a;
try {
const toChannel = await toSession.openChannel(e.request, null, cancellation);
void PipeExtensions.pipeChannel(e.channel, toChannel).catch();
return new connectionMessages_1.ChannelOpenConfirmationMessage();
}
catch (err) {
if (!(err instanceof Error))
throw err;
const failureMessage = new connectionMessages_1.ChannelOpenFailureMessage();
if (err instanceof errors_1.SshChannelError) {
failureMessage.reasonCode = (_a = err.reason) !== null && _a !== void 0 ? _a : connectionMessages_1.SshChannelOpenFailureReason.connectFailed;
}
else {
failureMessage.reasonCode = connectionMessages_1.SshChannelOpenFailureReason.connectFailed;
}
failureMessage.description = err.message;
return failureMessage;
}
}
static async forwardChannelRequest(e, toChannel, cancellation) {
e.request.recipientChannel = toChannel.remoteChannelId;
const result = await toChannel.request(e.request, cancellation);
return result ? new connectionMessages_1.ChannelSuccessMessage() : new connectionMessages_1.ChannelFailureMessage();
}
static async forwardSessionClose(session, e) {
var _a;
return session.close(e.reason, e.message, (_a = e.error) !== null && _a !== void 0 ? _a : undefined);
}
static async forwardData(channel, toChannel, data) {
// Make a copy of the buffer before sending because SshChannel.send() is an async operation
// (it may need to wait for the window to open), while the buffer will be re-used for the
// next message as sson as this task yields.
const buffer = buffer_1.Buffer.alloc(data.length);
data.copy(buffer);
const promise = toChannel.send(buffer, vscode_jsonrpc_1.CancellationToken.None);
channel.adjustWindow(buffer.length);
return promise;
}
static async forwardExtendedData(channel, toChannel, dataTypeCode, data) {
// Make a copy of the buffer before sending because SshChannel.send() is an async operation
// (it may need to wait for the window to open), while the buffer will be re-used for the
// next message as sson as this task yields.
const buffer = buffer_1.Buffer.alloc(data.length);
data.copy(buffer);
const promise = toChannel.sendExtendedData(dataTypeCode, buffer, vscode_jsonrpc_1.CancellationToken.None);
channel.adjustWindow(buffer.length);
return promise;
}
static async forwardChannelClose(fromChannel, toChannel, e) {
const message = `Piping channel closure.\n` +
`Source: ${fromChannel.session} ${fromChannel}\n` +
`Destination: ${toChannel.session} ${toChannel}\n`;
toChannel.session.trace(trace_1.TraceLevel.Verbose, trace_1.SshTraceEventIds.channelClosed, message);
if (e.error) {
toChannel.close(e.error);
return Promise.resolve();
}
else if (e.exitSignal) {
return toChannel.close(e.exitSignal, e.errorMessage);
}
else if (typeof e.exitStatus === 'number') {
return toChannel.close(e.exitStatus);
}
else {
return toChannel.close();
}
}
}
exports.PipeExtensions = PipeExtensions;
//# sourceMappingURL=pipeExtensions.js.map
;