UNPKG

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