@microsoft/dev-tunnels-ssh
Version:
SSH library for Dev Tunnels
388 lines • 19.3 kB
JavaScript
"use strict";
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var ConnectionService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConnectionService = void 0;
const sshService_1 = require("./sshService");
const connectionMessages_1 = require("../messages/connectionMessages");
const promiseCompletionSource_1 = require("../util/promiseCompletionSource");
const sshChannel_1 = require("../sshChannel");
const cancellation_1 = require("../util/cancellation");
const errors_1 = require("../errors");
const sshChannelOpeningEventArgs_1 = require("../events/sshChannelOpeningEventArgs");
const serviceActivation_1 = require("./serviceActivation");
const trace_1 = require("../trace");
const sshExtendedDataEventArgs_1 = require("../events/sshExtendedDataEventArgs");
let ConnectionService = ConnectionService_1 = class ConnectionService extends sshService_1.SshService {
constructor(session) {
super(session);
this.channelCounter = 0;
this.channelMap = new Map();
this.nonAcceptedChannels = new Map();
this.pendingChannels = new Map();
this.pendingAcceptChannels = new Map();
}
get channels() {
return Array.from(this.channelMap.values());
}
close(e) {
let channelCompletions = [...this.pendingChannels.values()].map((pc) => pc.completionSource);
if (this.pendingAcceptChannels.size > 0) {
channelCompletions = channelCompletions.concat([...this.pendingAcceptChannels.values()].reduce((a, b) => a.concat(b)));
}
for (const channel of this.channelMap.values()) {
channel.close(e);
}
for (const channelCompletion of channelCompletions) {
channelCompletion.reject(e);
}
}
dispose() {
const channels = [...this.channelMap.values()];
let channelCompletions = [...this.pendingChannels.values()].map((pc) => pc.completionSource);
if (this.pendingAcceptChannels.size > 0) {
channelCompletions = channelCompletions.concat([...this.pendingAcceptChannels.values()].reduce((a, b) => a.concat(b)));
}
for (const channel of channels) {
channel.dispose();
}
for (const channelCompletion of channelCompletions) {
channelCompletion.reject(new errors_1.ObjectDisposedError('Session closed.'));
}
super.dispose();
}
async acceptChannel(channelType, cancellation) {
const completionSource = new promiseCompletionSource_1.PromiseCompletionSource();
let cancellationRegistration;
if (cancellation) {
if (cancellation.isCancellationRequested)
throw new cancellation_1.CancellationError();
cancellationRegistration = cancellation.onCancellationRequested(() => {
const list = this.pendingAcceptChannels.get(channelType);
if (list) {
const index = list.findIndex((item) => Object.is(item, completionSource));
if (index >= 0) {
list.splice(index, 1);
}
}
completionSource.reject(new cancellation_1.CancellationError());
});
}
let channel = null;
channel =
Array.from(this.nonAcceptedChannels.values()).find((c) => c.channelType === channelType) ||
null;
if (channel) {
// Found a channel that was already opened but not accepted.
this.nonAcceptedChannels.delete(channel.channelId);
}
else {
// Set up the completion source to wait for a channel of the requested type.
let list = this.pendingAcceptChannels.get(channelType);
if (!list) {
list = [];
this.pendingAcceptChannels.set(channelType, list);
}
list.push(completionSource);
}
try {
return channel || (await completionSource.promise);
}
finally {
if (cancellationRegistration)
cancellationRegistration.dispose();
}
}
async openChannel(openMessage, completionSource, cancellation) {
const channelId = ++this.channelCounter;
openMessage.senderChannel = channelId;
let cancellationRegistration = null;
if (cancellation) {
if (cancellation.isCancellationRequested)
throw new cancellation_1.CancellationError();
cancellationRegistration = cancellation.onCancellationRequested(() => {
if (this.pendingChannels.delete(channelId)) {
completionSource.reject(new cancellation_1.CancellationError());
}
});
}
this.pendingChannels.set(channelId, {
openMessage: openMessage,
completionSource: completionSource,
cancellationRegistration: cancellationRegistration,
});
await this.session.sendMessage(openMessage);
return channelId;
}
handleMessage(message, cancellation) {
if (message instanceof connectionMessages_1.ChannelDataMessage) {
return this.handleDataMessage(message);
}
else if (message instanceof connectionMessages_1.ChannelExtendedDataMessage) {
return this.handleExtendedDataMessage(message);
}
else if (message instanceof connectionMessages_1.ChannelWindowAdjustMessage) {
return this.handleAdjustWindowMessage(message);
}
else if (message instanceof connectionMessages_1.ChannelEofMessage) {
return this.handleEofMessage(message);
}
else if (message instanceof connectionMessages_1.ChannelOpenMessage) {
return this.handleOpenMessage(message, cancellation);
}
else if (message instanceof connectionMessages_1.ChannelCloseMessage) {
return this.handleCloseMessage(message);
}
else if (message instanceof connectionMessages_1.ChannelOpenConfirmationMessage) {
return this.handleOpenConfirmationMessage(message, cancellation);
}
else if (message instanceof connectionMessages_1.ChannelOpenFailureMessage) {
return this.handleOpenFailureMessage(message);
}
else if (message instanceof connectionMessages_1.ChannelRequestMessage) {
return this.handleRequestMessage(message, cancellation);
}
else if (message instanceof connectionMessages_1.ChannelSuccessMessage) {
return this.handleSuccessMessage(message);
}
else if (message instanceof connectionMessages_1.ChannelFailureMessage) {
return this.handleFailureMessage(message);
}
else {
throw new Error(`Message not implemented: ${message}`);
}
}
async handleOpenMessage(message, cancellation) {
var _a;
const senderChannel = message.senderChannel;
if (!this.session.canAcceptRequests) {
this.trace(trace_1.TraceLevel.Warning, trace_1.SshTraceEventIds.channelOpenFailed, 'Channel open request blocked because the session is not yet authenticated.');
const openFailureMessage = new connectionMessages_1.ChannelOpenFailureMessage();
openFailureMessage.recipientChannel = senderChannel;
openFailureMessage.reasonCode = connectionMessages_1.SshChannelOpenFailureReason.administrativelyProhibited;
openFailureMessage.description = 'Authenticate before opening channels.';
await this.session.sendMessage(openFailureMessage, cancellation);
return;
}
else if (!message.channelType) {
const openFailureMessage = new connectionMessages_1.ChannelOpenFailureMessage();
openFailureMessage.recipientChannel = senderChannel;
openFailureMessage.reasonCode = connectionMessages_1.SshChannelOpenFailureReason.unknownChannelType;
openFailureMessage.description = 'Channel type not specified.';
await this.session.sendMessage(openFailureMessage, cancellation);
return;
}
// Save a copy of the message because its buffer will be overwitten by the next receive.
message = message.convertTo(new connectionMessages_1.ChannelOpenMessage(), true);
// The confirmation message may be reassigned if the opening task returns a custom message.
let confirmationMessage = new connectionMessages_1.ChannelOpenConfirmationMessage();
const channelId = ++this.channelCounter;
const channel = new sshChannel_1.SshChannel(this, message.channelType, channelId, senderChannel, message.maxWindowSize, message.maxPacketSize, message, confirmationMessage);
let responseMessage;
const args = new sshChannelOpeningEventArgs_1.SshChannelOpeningEventArgs(message, channel, true);
try {
await this.session.handleChannelOpening(args, cancellation);
if (args.openingPromise) {
responseMessage = await args.openingPromise;
}
else if (args.failureReason !== connectionMessages_1.SshChannelOpenFailureReason.none) {
const failureMessage = new connectionMessages_1.ChannelOpenFailureMessage();
failureMessage.reasonCode = args.failureReason;
failureMessage.description = (_a = args.failureDescription) !== null && _a !== void 0 ? _a : undefined;
responseMessage = failureMessage;
}
else {
responseMessage = confirmationMessage;
}
}
catch (e) {
channel.dispose();
throw e;
}
if (responseMessage instanceof connectionMessages_1.ChannelOpenFailureMessage) {
responseMessage.recipientChannel = senderChannel;
try {
await this.session.sendMessage(responseMessage, cancellation);
}
finally {
channel.dispose();
}
return;
}
// The session might have been closed while opening the channel.
if (this.session.isClosed) {
channel.dispose();
return;
}
// Prevent any changes to the channel max window size after sending the value in the
// open confirmation message.
channel.isMaxWindowSizeLocked = true;
this.channelMap.set(channel.channelId, channel);
confirmationMessage = responseMessage;
confirmationMessage.recipientChannel = channel.remoteChannelId;
confirmationMessage.senderChannel = channel.channelId;
confirmationMessage.maxWindowSize = channel.maxWindowSize;
confirmationMessage.maxPacketSize = channel.maxPacketSize;
confirmationMessage.rewrite();
channel.openConfirmationMessage = confirmationMessage;
await this.session.sendMessage(confirmationMessage, cancellation);
// Check if there are any accept operations waiting on this channel type.
let accepted = false;
const list = this.pendingAcceptChannels.get(channel.channelType);
while (list && list.length > 0) {
const acceptCompletionSource = list.shift();
acceptCompletionSource.resolve(channel);
accepted = true;
break;
}
if (!accepted) {
this.nonAcceptedChannels.set(channel.channelId, channel);
}
this.onChannelOpenCompleted(channel.channelId, channel);
channel.enableSending();
}
handleCloseMessage(message) {
const channel = this.findChannelById(message.recipientChannel);
if (channel) {
channel.handleClose();
}
}
async handleOpenConfirmationMessage(message, cancellation) {
var _a;
let completionSource = null;
let openMessage;
const pendingChannel = this.pendingChannels.get(message.recipientChannel);
if (pendingChannel) {
openMessage = pendingChannel.openMessage;
completionSource = pendingChannel.completionSource;
if (pendingChannel.cancellationRegistration) {
pendingChannel.cancellationRegistration.dispose();
}
this.pendingChannels.delete(message.recipientChannel);
}
else if (this.channelMap.has(message.recipientChannel)) {
throw new Error('Duplicate channel ID.');
}
else {
throw new Error('Channel confirmation was not requested.');
}
// Save a copy of the message because its buffer will be overwitten by the next receive.
message = message.convertTo(new connectionMessages_1.ChannelOpenConfirmationMessage(), true);
const channel = new sshChannel_1.SshChannel(this, openMessage.channelType || sshChannel_1.SshChannel.sessionChannelType, message.recipientChannel, message.senderChannel, message.maxWindowSize, message.maxPacketSize, openMessage, message);
// Set the channel max window size property to match the value sent in the open message,
// (if specified) and lock it to prevent any further changes.
if (typeof openMessage.maxWindowSize === 'number') {
channel.maxWindowSize = openMessage.maxWindowSize;
}
channel.isMaxWindowSizeLocked = true;
this.channelMap.set(channel.channelId, channel);
const args = new sshChannelOpeningEventArgs_1.SshChannelOpeningEventArgs(openMessage, channel, false);
await this.session.handleChannelOpening(args, cancellation);
if (completionSource) {
if (args.failureReason === connectionMessages_1.SshChannelOpenFailureReason.none) {
completionSource.resolve(channel);
}
else {
completionSource.reject(new errors_1.SshChannelError((_a = args.failureDescription) !== null && _a !== void 0 ? _a : 'Channel open failure.', args.failureReason));
return;
}
}
else {
this.onChannelOpenCompleted(channel.channelId, channel);
}
channel.enableSending();
}
handleOpenFailureMessage(message) {
let completionSource = null;
const pendingChannel = this.pendingChannels.get(message.recipientChannel);
if (pendingChannel) {
completionSource = pendingChannel.completionSource;
if (pendingChannel.cancellationRegistration) {
pendingChannel.cancellationRegistration.dispose();
}
this.pendingChannels.delete(message.recipientChannel);
}
if (completionSource != null) {
completionSource.reject(new errors_1.SshChannelError(message.description || 'Channel open rejected.', message.reasonCode));
}
else {
this.onChannelOpenCompleted(message.recipientChannel, null);
}
}
async handleRequestMessage(message, cancellation) {
const channel = this.tryGetChannelForMessage(message);
if (!channel)
return;
await channel.handleRequest(message, cancellation);
}
handleSuccessMessage(message) {
const channel = this.tryGetChannelForMessage(message);
channel === null || channel === void 0 ? void 0 : channel.handleResponse(true);
}
handleFailureMessage(message) {
const channel = this.tryGetChannelForMessage(message);
channel === null || channel === void 0 ? void 0 : channel.handleResponse(false);
}
handleDataMessage(message) {
const channel = this.tryGetChannelForMessage(message);
channel === null || channel === void 0 ? void 0 : channel.handleDataReceived(message.data);
}
handleExtendedDataMessage(message) {
const channel = this.tryGetChannelForMessage(message);
channel === null || channel === void 0 ? void 0 : channel.handleExtendedDataReceived(new sshExtendedDataEventArgs_1.SshExtendedDataEventArgs(message.dataTypeCode, message.data));
}
handleAdjustWindowMessage(message) {
const channel = this.tryGetChannelForMessage(message);
channel === null || channel === void 0 ? void 0 : channel.adjustRemoteWindow(message.bytesToAdd);
}
handleEofMessage(message) {
const channel = this.findChannelById(message.recipientChannel);
channel === null || channel === void 0 ? void 0 : channel.handleEof();
}
onChannelOpenCompleted(channelId, channel) {
if (channel) {
this.trace(trace_1.TraceLevel.Info, trace_1.SshTraceEventIds.channelOpened, `${this.session} ChannelOpenCompleted(${channel})`);
}
else {
this.trace(trace_1.TraceLevel.Warning, trace_1.SshTraceEventIds.channelOpenFailed, `${this.session} ChannelOpenCompleted(${channelId} failed)`);
}
}
/**
* Gets the channel object based on the message `recipientChannel` property.
* Logs a warning if the channel was not found.
*/
tryGetChannelForMessage(channelMessage) {
const channel = this.findChannelById(channelMessage.recipientChannel);
if (!channel) {
const messageString = channelMessage instanceof connectionMessages_1.ChannelDataMessage
? 'channel data message'
: channelMessage.toString();
this.trace(trace_1.TraceLevel.Warning, trace_1.SshTraceEventIds.channelRequestFailed, `Invalid channel ID ${channelMessage.recipientChannel} in ${messageString}.`);
}
return channel;
}
findChannelById(id) {
var _a;
const channel = (_a = this.channelMap.get(id)) !== null && _a !== void 0 ? _a : null;
return channel;
}
/* @internal */
removeChannel(channel) {
this.channelMap.delete(channel.channelId);
this.pendingChannels.delete(channel.channelId);
}
};
ConnectionService.serviceName = 'ssh-connection';
ConnectionService = ConnectionService_1 = __decorate([
(0, serviceActivation_1.serviceActivation)({ serviceRequest: ConnectionService_1.serviceName })
], ConnectionService);
exports.ConnectionService = ConnectionService;
//# sourceMappingURL=connectionService.js.map