utils.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. 'use strict';
  2. const { SFTP } = require('./protocol/SFTP.js');
  3. const MAX_CHANNEL = 2 ** 32 - 1;
  4. function onChannelOpenFailure(self, recipient, info, cb) {
  5. self._chanMgr.remove(recipient);
  6. if (typeof cb !== 'function')
  7. return;
  8. let err;
  9. if (info instanceof Error) {
  10. err = info;
  11. } else if (typeof info === 'object' && info !== null) {
  12. err = new Error(`(SSH) Channel open failure: ${info.description}`);
  13. err.reason = info.reason;
  14. } else {
  15. err = new Error(
  16. '(SSH) Channel open failure: server closed channel unexpectedly'
  17. );
  18. err.reason = '';
  19. }
  20. cb(err);
  21. }
  22. function onCHANNEL_CLOSE(self, recipient, channel, err, dead) {
  23. if (typeof channel === 'function') {
  24. // We got CHANNEL_CLOSE instead of CHANNEL_OPEN_FAILURE when
  25. // requesting to open a channel
  26. onChannelOpenFailure(self, recipient, err, channel);
  27. return;
  28. }
  29. if (typeof channel !== 'object'
  30. || channel === null
  31. || channel.incoming.state === 'closed') {
  32. return;
  33. }
  34. channel.incoming.state = 'closed';
  35. if (channel.readable)
  36. channel.push(null);
  37. if (channel.server) {
  38. if (channel.stderr.writable)
  39. channel.stderr.end();
  40. } else if (channel.stderr.readable) {
  41. channel.stderr.push(null);
  42. }
  43. if (channel.constructor !== SFTP
  44. && (channel.outgoing.state === 'open'
  45. || channel.outgoing.state === 'eof')
  46. && !dead) {
  47. channel.close();
  48. }
  49. if (channel.outgoing.state === 'closing')
  50. channel.outgoing.state = 'closed';
  51. self._chanMgr.remove(recipient);
  52. const readState = channel._readableState;
  53. const writeState = channel._writableState;
  54. if (writeState && !writeState.ending && !writeState.finished && !dead)
  55. channel.end();
  56. // Take care of any outstanding channel requests
  57. const chanCallbacks = channel._callbacks;
  58. channel._callbacks = [];
  59. for (let i = 0; i < chanCallbacks.length; ++i)
  60. chanCallbacks[i](true);
  61. if (channel.server) {
  62. if (!channel.readable
  63. || channel.destroyed
  64. || (readState && readState.endEmitted)) {
  65. channel.emit('close');
  66. } else {
  67. channel.once('end', () => channel.emit('close'));
  68. }
  69. } else {
  70. let doClose;
  71. switch (channel.type) {
  72. case 'direct-streamlocal@openssh.com':
  73. case 'direct-tcpip':
  74. doClose = () => channel.emit('close');
  75. break;
  76. default: {
  77. // Align more with node child processes, where the close event gets
  78. // the same arguments as the exit event
  79. const exit = channel._exit;
  80. doClose = () => {
  81. if (exit.code === null)
  82. channel.emit('close', exit.code, exit.signal, exit.dump, exit.desc);
  83. else
  84. channel.emit('close', exit.code);
  85. };
  86. }
  87. }
  88. if (!channel.readable
  89. || channel.destroyed
  90. || (readState && readState.endEmitted)) {
  91. doClose();
  92. } else {
  93. channel.once('end', doClose);
  94. }
  95. const errReadState = channel.stderr._readableState;
  96. if (!channel.stderr.readable
  97. || channel.stderr.destroyed
  98. || (errReadState && errReadState.endEmitted)) {
  99. channel.stderr.emit('close');
  100. } else {
  101. channel.stderr.once('end', () => channel.stderr.emit('close'));
  102. }
  103. }
  104. }
  105. class ChannelManager {
  106. constructor(client) {
  107. this._client = client;
  108. this._channels = {};
  109. this._cur = -1;
  110. this._count = 0;
  111. }
  112. add(val) {
  113. // Attempt to reserve an id
  114. let id;
  115. // Optimized paths
  116. if (this._cur < MAX_CHANNEL) {
  117. id = ++this._cur;
  118. } else if (this._count === 0) {
  119. // Revert and reset back to fast path once we no longer have any channels
  120. // open
  121. this._cur = 0;
  122. id = 0;
  123. } else {
  124. // Slower lookup path
  125. // This path is triggered we have opened at least MAX_CHANNEL channels
  126. // while having at least one channel open at any given time, so we have
  127. // to search for a free id.
  128. const channels = this._channels;
  129. for (let i = 0; i < MAX_CHANNEL; ++i) {
  130. if (channels[i] === undefined) {
  131. id = i;
  132. break;
  133. }
  134. }
  135. }
  136. if (id === undefined)
  137. return -1;
  138. this._channels[id] = (val || true);
  139. ++this._count;
  140. return id;
  141. }
  142. update(id, val) {
  143. if (typeof id !== 'number' || id < 0 || id >= MAX_CHANNEL || !isFinite(id))
  144. throw new Error(`Invalid channel id: ${id}`);
  145. if (val && this._channels[id])
  146. this._channels[id] = val;
  147. }
  148. get(id) {
  149. if (typeof id !== 'number' || id < 0 || id >= MAX_CHANNEL || !isFinite(id))
  150. throw new Error(`Invalid channel id: ${id}`);
  151. return this._channels[id];
  152. }
  153. remove(id) {
  154. if (typeof id !== 'number' || id < 0 || id >= MAX_CHANNEL || !isFinite(id))
  155. throw new Error(`Invalid channel id: ${id}`);
  156. if (this._channels[id]) {
  157. delete this._channels[id];
  158. if (this._count)
  159. --this._count;
  160. }
  161. }
  162. cleanup(err) {
  163. const channels = this._channels;
  164. this._channels = {};
  165. this._cur = -1;
  166. this._count = 0;
  167. const chanIDs = Object.keys(channels);
  168. const client = this._client;
  169. for (let i = 0; i < chanIDs.length; ++i) {
  170. const id = +chanIDs[i];
  171. const channel = channels[id];
  172. onCHANNEL_CLOSE(client, id, channel._channel || channel, err, true);
  173. }
  174. }
  175. }
  176. const isRegExp = (() => {
  177. const toString = Object.prototype.toString;
  178. return (val) => toString.call(val) === '[object RegExp]';
  179. })();
  180. function generateAlgorithmList(algoList, defaultList, supportedList) {
  181. if (Array.isArray(algoList) && algoList.length > 0) {
  182. // Exact list
  183. for (let i = 0; i < algoList.length; ++i) {
  184. if (supportedList.indexOf(algoList[i]) === -1)
  185. throw new Error(`Unsupported algorithm: ${algoList[i]}`);
  186. }
  187. return algoList;
  188. }
  189. if (typeof algoList === 'object' && algoList !== null) {
  190. // Operations based on the default list
  191. const keys = Object.keys(algoList);
  192. let list = defaultList;
  193. for (let i = 0; i < keys.length; ++i) {
  194. const key = keys[i];
  195. let val = algoList[key];
  196. switch (key) {
  197. case 'append':
  198. if (!Array.isArray(val))
  199. val = [val];
  200. if (Array.isArray(val)) {
  201. for (let j = 0; j < val.length; ++j) {
  202. const append = val[j];
  203. if (typeof append === 'string') {
  204. if (!append || list.indexOf(append) !== -1)
  205. continue;
  206. if (supportedList.indexOf(append) === -1)
  207. throw new Error(`Unsupported algorithm: ${append}`);
  208. if (list === defaultList)
  209. list = list.slice();
  210. list.push(append);
  211. } else if (isRegExp(append)) {
  212. for (let k = 0; k < supportedList.length; ++k) {
  213. const algo = supportedList[k];
  214. if (append.test(algo)) {
  215. if (list.indexOf(algo) !== -1)
  216. continue;
  217. if (list === defaultList)
  218. list = list.slice();
  219. list.push(algo);
  220. }
  221. }
  222. }
  223. }
  224. }
  225. break;
  226. case 'prepend':
  227. if (!Array.isArray(val))
  228. val = [val];
  229. if (Array.isArray(val)) {
  230. for (let j = val.length; j >= 0; --j) {
  231. const prepend = val[j];
  232. if (typeof prepend === 'string') {
  233. if (!prepend || list.indexOf(prepend) !== -1)
  234. continue;
  235. if (supportedList.indexOf(prepend) === -1)
  236. throw new Error(`Unsupported algorithm: ${prepend}`);
  237. if (list === defaultList)
  238. list = list.slice();
  239. list.unshift(prepend);
  240. } else if (isRegExp(prepend)) {
  241. for (let k = supportedList.length; k >= 0; --k) {
  242. const algo = supportedList[k];
  243. if (prepend.test(algo)) {
  244. if (list.indexOf(algo) !== -1)
  245. continue;
  246. if (list === defaultList)
  247. list = list.slice();
  248. list.unshift(algo);
  249. }
  250. }
  251. }
  252. }
  253. }
  254. break;
  255. case 'remove':
  256. if (!Array.isArray(val))
  257. val = [val];
  258. if (Array.isArray(val)) {
  259. for (let j = 0; j < val.length; ++j) {
  260. const search = val[j];
  261. if (typeof search === 'string') {
  262. if (!search)
  263. continue;
  264. const idx = list.indexOf(search);
  265. if (idx === -1)
  266. continue;
  267. if (list === defaultList)
  268. list = list.slice();
  269. list.splice(idx, 1);
  270. } else if (isRegExp(search)) {
  271. for (let k = 0; k < list.length; ++k) {
  272. if (search.test(list[k])) {
  273. if (list === defaultList)
  274. list = list.slice();
  275. list.splice(k, 1);
  276. --k;
  277. }
  278. }
  279. }
  280. }
  281. }
  282. break;
  283. }
  284. }
  285. return list;
  286. }
  287. return defaultList;
  288. }
  289. module.exports = {
  290. ChannelManager,
  291. generateAlgorithmList,
  292. onChannelOpenFailure,
  293. onCHANNEL_CLOSE,
  294. isWritable: (stream) => {
  295. // XXX: hack to workaround regression in node
  296. // See: https://github.com/nodejs/node/issues/36029
  297. return (stream
  298. && stream.writable
  299. && stream._readableState
  300. && stream._readableState.ended === false);
  301. },
  302. };