305 lines
9.0 KiB
JavaScript
305 lines
9.0 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 '[email protected]':
|
|
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
|
|
)
|
|
}
|
|
}
|