utils.js•9.77 kB
'use strict';
const { SFTP } = require('./protocol/SFTP.js');
const MAX_CHANNEL = 2 ** 32 - 1;
function onChannelOpenFailure(self, recipient, info, cb) {
self._chanMgr.remove(recipient);
if (typeof cb !== 'function')
return;
let err;
if (info instanceof Error) {
err = info;
} else if (typeof info === 'object' && info !== null) {
err = new Error(`(SSH) Channel open failure: ${info.description}`);
err.reason = info.reason;
} else {
err = new Error(
'(SSH) Channel open failure: server closed channel unexpectedly'
);
err.reason = '';
}
cb(err);
}
function onCHANNEL_CLOSE(self, recipient, channel, err, dead) {
if (typeof channel === 'function') {
// We got CHANNEL_CLOSE instead of CHANNEL_OPEN_FAILURE when
// requesting to open a channel
onChannelOpenFailure(self, recipient, err, channel);
return;
}
if (typeof channel !== 'object' || channel === null)
return;
if (channel.incoming && channel.incoming.state === 'closed')
return;
self._chanMgr.remove(recipient);
if (channel.server && channel.constructor.name === 'Session')
return;
channel.incoming.state = 'closed';
if (channel.readable)
channel.push(null);
if (channel.server) {
if (channel.stderr.writable)
channel.stderr.end();
} else if (channel.stderr.readable) {
channel.stderr.push(null);
}
if (channel.constructor !== SFTP
&& (channel.outgoing.state === 'open'
|| channel.outgoing.state === 'eof')
&& !dead) {
channel.close();
}
if (channel.outgoing.state === 'closing')
channel.outgoing.state = 'closed';
const readState = channel._readableState;
const writeState = channel._writableState;
if (writeState && !writeState.ending && !writeState.finished && !dead)
channel.end();
// Take care of any outstanding channel requests
const chanCallbacks = channel._callbacks;
channel._callbacks = [];
for (let i = 0; i < chanCallbacks.length; ++i)
chanCallbacks[i](true);
if (channel.server) {
if (!channel.readable
|| channel.destroyed
|| (readState && readState.endEmitted)) {
channel.emit('close');
} else {
channel.once('end', () => channel.emit('close'));
}
} else {
let doClose;
switch (channel.type) {
case 'direct-streamlocal@openssh.com':
case 'direct-tcpip':
doClose = () => channel.emit('close');
break;
default: {
// Align more with node child processes, where the close event gets
// the same arguments as the exit event
const exit = channel._exit;
doClose = () => {
if (exit.code === null)
channel.emit('close', exit.code, exit.signal, exit.dump, exit.desc);
else
channel.emit('close', exit.code);
};
}
}
if (!channel.readable
|| channel.destroyed
|| (readState && readState.endEmitted)) {
doClose();
} else {
channel.once('end', doClose);
}
const errReadState = channel.stderr._readableState;
if (!channel.stderr.readable
|| channel.stderr.destroyed
|| (errReadState && errReadState.endEmitted)) {
channel.stderr.emit('close');
} else {
channel.stderr.once('end', () => channel.stderr.emit('close'));
}
}
}
class ChannelManager {
constructor(client) {
this._client = client;
this._channels = {};
this._cur = -1;
this._count = 0;
}
add(val) {
// Attempt to reserve an id
let id;
// Optimized paths
if (this._cur < MAX_CHANNEL) {
id = ++this._cur;
} else if (this._count === 0) {
// Revert and reset back to fast path once we no longer have any channels
// open
this._cur = 0;
id = 0;
} else {
// Slower lookup path
// This path is triggered we have opened at least MAX_CHANNEL channels
// while having at least one channel open at any given time, so we have
// to search for a free id.
const channels = this._channels;
for (let i = 0; i < MAX_CHANNEL; ++i) {
if (channels[i] === undefined) {
id = i;
break;
}
}
}
if (id === undefined)
return -1;
this._channels[id] = (val || true);
++this._count;
return id;
}
update(id, val) {
if (typeof id !== 'number' || id < 0 || id >= MAX_CHANNEL || !isFinite(id))
throw new Error(`Invalid channel id: ${id}`);
if (val && this._channels[id])
this._channels[id] = val;
}
get(id) {
if (typeof id !== 'number' || id < 0 || id >= MAX_CHANNEL || !isFinite(id))
throw new Error(`Invalid channel id: ${id}`);
return this._channels[id];
}
remove(id) {
if (typeof id !== 'number' || id < 0 || id >= MAX_CHANNEL || !isFinite(id))
throw new Error(`Invalid channel id: ${id}`);
if (this._channels[id]) {
delete this._channels[id];
if (this._count)
--this._count;
}
}
cleanup(err) {
const channels = this._channels;
this._channels = {};
this._cur = -1;
this._count = 0;
const chanIDs = Object.keys(channels);
const client = this._client;
for (let i = 0; i < chanIDs.length; ++i) {
const id = +chanIDs[i];
const channel = channels[id];
onCHANNEL_CLOSE(client, id, channel._channel || channel, err, true);
}
}
}
const isRegExp = (() => {
const toString = Object.prototype.toString;
return (val) => toString.call(val) === '[object RegExp]';
})();
function generateAlgorithmList(algoList, defaultList, supportedList) {
if (Array.isArray(algoList) && algoList.length > 0) {
// Exact list
for (let i = 0; i < algoList.length; ++i) {
if (supportedList.indexOf(algoList[i]) === -1)
throw new Error(`Unsupported algorithm: ${algoList[i]}`);
}
return algoList;
}
if (typeof algoList === 'object' && algoList !== null) {
// Operations based on the default list
const keys = Object.keys(algoList);
let list = defaultList;
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
let val = algoList[key];
switch (key) {
case 'append':
if (!Array.isArray(val))
val = [val];
if (Array.isArray(val)) {
for (let j = 0; j < val.length; ++j) {
const append = val[j];
if (typeof append === 'string') {
if (!append || list.indexOf(append) !== -1)
continue;
if (supportedList.indexOf(append) === -1)
throw new Error(`Unsupported algorithm: ${append}`);
if (list === defaultList)
list = list.slice();
list.push(append);
} else if (isRegExp(append)) {
for (let k = 0; k < supportedList.length; ++k) {
const algo = supportedList[k];
if (append.test(algo)) {
if (list.indexOf(algo) !== -1)
continue;
if (list === defaultList)
list = list.slice();
list.push(algo);
}
}
}
}
}
break;
case 'prepend':
if (!Array.isArray(val))
val = [val];
if (Array.isArray(val)) {
for (let j = val.length; j >= 0; --j) {
const prepend = val[j];
if (typeof prepend === 'string') {
if (!prepend || list.indexOf(prepend) !== -1)
continue;
if (supportedList.indexOf(prepend) === -1)
throw new Error(`Unsupported algorithm: ${prepend}`);
if (list === defaultList)
list = list.slice();
list.unshift(prepend);
} else if (isRegExp(prepend)) {
for (let k = supportedList.length; k >= 0; --k) {
const algo = supportedList[k];
if (prepend.test(algo)) {
if (list.indexOf(algo) !== -1)
continue;
if (list === defaultList)
list = list.slice();
list.unshift(algo);
}
}
}
}
}
break;
case 'remove':
if (!Array.isArray(val))
val = [val];
if (Array.isArray(val)) {
for (let j = 0; j < val.length; ++j) {
const search = val[j];
if (typeof search === 'string') {
if (!search)
continue;
const idx = list.indexOf(search);
if (idx === -1)
continue;
if (list === defaultList)
list = list.slice();
list.splice(idx, 1);
} else if (isRegExp(search)) {
for (let k = 0; k < list.length; ++k) {
if (search.test(list[k])) {
if (list === defaultList)
list = list.slice();
list.splice(k, 1);
--k;
}
}
}
}
}
break;
}
}
return list;
}
return defaultList;
}
module.exports = {
ChannelManager,
generateAlgorithmList,
onChannelOpenFailure,
onCHANNEL_CLOSE,
isWritable: (stream) => {
// XXX: hack to workaround regression in node
// See: https://github.com/nodejs/node/issues/36029
return (stream
&& stream.writable
&& stream._readableState
&& stream._readableState.ended === false);
},
};