337 lines
9.5 KiB
JavaScript
337 lines
9.5 KiB
JavaScript
'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);
|
|
},
|
|
};
|