@electerm/ssh2 0.8.11 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/server.js CHANGED
@@ -1,1160 +1,1367 @@
1
- var net = require('net');
2
- var EventEmitter = require('events').EventEmitter;
3
- var listenerCount = EventEmitter.listenerCount;
4
- var inherits = require('util').inherits;
5
-
6
- var ssh2_streams = require('ssh2-streams');
7
- var parseKey = ssh2_streams.utils.parseKey;
8
- var SSH2Stream = ssh2_streams.SSH2Stream;
9
- var SFTPStream = ssh2_streams.SFTPStream;
10
- var consts = ssh2_streams.constants;
11
- var DISCONNECT_REASON = consts.DISCONNECT_REASON;
12
- var CHANNEL_OPEN_FAILURE = consts.CHANNEL_OPEN_FAILURE;
13
- var ALGORITHMS = consts.ALGORITHMS;
14
-
15
- var Channel = require('./Channel');
16
- var KeepaliveManager = require('./keepalivemgr');
17
- var writeUInt32BE = require('./buffer-helpers').writeUInt32BE;
18
-
19
- var MAX_CHANNEL = Math.pow(2, 32) - 1;
20
- var MAX_PENDING_AUTHS = 10;
21
-
22
- var kaMgr;
23
-
24
- function Server(cfg, listener) {
25
- if (!(this instanceof Server))
26
- return new Server(cfg, listener);
27
-
28
- var hostKeys = {
29
- 'ssh-rsa': null,
30
- 'ssh-dss': null,
31
- 'ssh-ed25519': null,
32
- 'ecdsa-sha2-nistp256': null,
33
- 'ecdsa-sha2-nistp384': null,
34
- 'ecdsa-sha2-nistp521': null
35
- };
1
+ // TODO:
2
+ // * convert listenerCount() usage to emit() return value checking?
3
+ // * emit error when connection severed early (e.g. before handshake)
4
+ // * add '.connected' or similar property to connection objects to allow
5
+ // immediate connection status checking
6
+ 'use strict';
7
+
8
+ const { Server: netServer } = require('net');
9
+ const EventEmitter = require('events');
10
+ const { listenerCount } = EventEmitter;
11
+
12
+ const {
13
+ CHANNEL_OPEN_FAILURE,
14
+ DEFAULT_CIPHER,
15
+ DEFAULT_COMPRESSION,
16
+ DEFAULT_KEX,
17
+ DEFAULT_MAC,
18
+ DEFAULT_SERVER_HOST_KEY,
19
+ DISCONNECT_REASON,
20
+ DISCONNECT_REASON_BY_VALUE,
21
+ SUPPORTED_CIPHER,
22
+ SUPPORTED_COMPRESSION,
23
+ SUPPORTED_KEX,
24
+ SUPPORTED_MAC,
25
+ SUPPORTED_SERVER_HOST_KEY,
26
+ } = require('./protocol/constants.js');
27
+ const { init: cryptoInit } = require('./protocol/crypto.js');
28
+ const { KexInit } = require('./protocol/kex.js');
29
+ const { parseKey } = require('./protocol/keyParser.js');
30
+ const Protocol = require('./protocol/Protocol.js');
31
+ const { SFTP } = require('./protocol/SFTP.js');
32
+ const { writeUInt32BE } = require('./protocol/utils.js');
33
+
34
+ const {
35
+ Channel,
36
+ MAX_WINDOW,
37
+ PACKET_SIZE,
38
+ windowAdjust,
39
+ WINDOW_THRESHOLD,
40
+ } = require('./Channel.js');
41
+
42
+ const {
43
+ ChannelManager,
44
+ generateAlgorithmList,
45
+ isWritable,
46
+ onChannelOpenFailure,
47
+ onCHANNEL_CLOSE,
48
+ } = require('./utils.js');
49
+
50
+ const MAX_PENDING_AUTHS = 10;
51
+
52
+ class AuthContext extends EventEmitter {
53
+ constructor(protocol, username, service, method, cb) {
54
+ super();
55
+
56
+ this.username = this.user = username;
57
+ this.service = service;
58
+ this.method = method;
59
+ this._initialResponse = false;
60
+ this._finalResponse = false;
61
+ this._multistep = false;
62
+ this._cbfinal = (allowed, methodsLeft, isPartial) => {
63
+ if (!this._finalResponse) {
64
+ this._finalResponse = true;
65
+ cb(this, allowed, methodsLeft, isPartial);
66
+ }
67
+ };
68
+ this._protocol = protocol;
69
+ }
36
70
 
37
- var hostKeys_ = cfg.hostKeys;
38
- if (!Array.isArray(hostKeys_))
39
- throw new Error('hostKeys must be an array');
40
-
41
- var i;
42
- for (i = 0; i < hostKeys_.length; ++i) {
43
- var privateKey;
44
- if (Buffer.isBuffer(hostKeys_[i]) || typeof hostKeys_[i] === 'string')
45
- privateKey = parseKey(hostKeys_[i]);
46
- else
47
- privateKey = parseKey(hostKeys_[i].key, hostKeys_[i].passphrase);
48
- if (privateKey instanceof Error)
49
- throw new Error('Cannot parse privateKey: ' + privateKey.message);
50
- if (Array.isArray(privateKey))
51
- privateKey = privateKey[0]; // OpenSSH's newer format only stores 1 key for now
52
- if (privateKey.getPrivatePEM() === null)
53
- throw new Error('privateKey value contains an invalid private key');
54
- if (hostKeys[privateKey.type])
55
- continue;
56
- hostKeys[privateKey.type] = privateKey;
71
+ accept() {
72
+ this._cleanup && this._cleanup();
73
+ this._initialResponse = true;
74
+ this._cbfinal(true);
57
75
  }
76
+ reject(methodsLeft, isPartial) {
77
+ this._cleanup && this._cleanup();
78
+ this._initialResponse = true;
79
+ this._cbfinal(false, methodsLeft, isPartial);
80
+ }
81
+ }
58
82
 
59
- var algorithms = {
60
- kex: undefined,
61
- kexBuf: undefined,
62
- cipher: undefined,
63
- cipherBuf: undefined,
64
- serverHostKey: undefined,
65
- serverHostKeyBuf: undefined,
66
- hmac: undefined,
67
- hmacBuf: undefined,
68
- compress: undefined,
69
- compressBuf: undefined
70
- };
71
- if (typeof cfg.algorithms === 'object' && cfg.algorithms !== null) {
72
- var algosSupported;
73
- var algoList;
74
-
75
- algoList = cfg.algorithms.kex;
76
- if (Array.isArray(algoList) && algoList.length > 0) {
77
- algosSupported = ALGORITHMS.SUPPORTED_KEX;
78
- for (i = 0; i < algoList.length; ++i) {
79
- if (algosSupported.indexOf(algoList[i]) === -1)
80
- throw new Error('Unsupported key exchange algorithm: ' + algoList[i]);
81
- }
82
- algorithms.kex = algoList;
83
- }
84
83
 
85
- algoList = cfg.algorithms.cipher;
86
- if (Array.isArray(algoList) && algoList.length > 0) {
87
- algosSupported = ALGORITHMS.SUPPORTED_CIPHER;
88
- for (i = 0; i < algoList.length; ++i) {
89
- if (algosSupported.indexOf(algoList[i]) === -1)
90
- throw new Error('Unsupported cipher algorithm: ' + algoList[i]);
91
- }
92
- algorithms.cipher = algoList;
93
- }
84
+ class KeyboardAuthContext extends AuthContext {
85
+ constructor(protocol, username, service, method, submethods, cb) {
86
+ super(protocol, username, service, method, cb);
94
87
 
95
- algoList = cfg.algorithms.serverHostKey;
96
- var copied = false;
97
- if (Array.isArray(algoList) && algoList.length > 0) {
98
- algosSupported = ALGORITHMS.SUPPORTED_SERVER_HOST_KEY;
99
- for (i = algoList.length - 1; i >= 0; --i) {
100
- if (algosSupported.indexOf(algoList[i]) === -1) {
101
- throw new Error('Unsupported server host key algorithm: '
102
- + algoList[i]);
103
- }
104
- if (!hostKeys[algoList[i]]) {
105
- // Silently discard for now
106
- if (!copied) {
107
- algoList = algoList.slice();
108
- copied = true;
109
- }
110
- algoList.splice(i, 1);
111
- }
112
- }
113
- if (algoList.length > 0)
114
- algorithms.serverHostKey = algoList;
115
- }
88
+ this._multistep = true;
116
89
 
117
- algoList = cfg.algorithms.hmac;
118
- if (Array.isArray(algoList) && algoList.length > 0) {
119
- algosSupported = ALGORITHMS.SUPPORTED_HMAC;
120
- for (i = 0; i < algoList.length; ++i) {
121
- if (algosSupported.indexOf(algoList[i]) === -1)
122
- throw new Error('Unsupported HMAC algorithm: ' + algoList[i]);
90
+ this._cb = undefined;
91
+ this._onInfoResponse = (responses) => {
92
+ const callback = this._cb;
93
+ if (callback) {
94
+ this._cb = undefined;
95
+ callback(responses);
123
96
  }
124
- algorithms.hmac = algoList;
97
+ };
98
+ this.submethods = submethods;
99
+ this.on('abort', () => {
100
+ this._cb && this._cb(new Error('Authentication request aborted'));
101
+ });
102
+ }
103
+
104
+ prompt(prompts, title, instructions, cb) {
105
+ if (!Array.isArray(prompts))
106
+ prompts = [ prompts ];
107
+
108
+ if (typeof title === 'function') {
109
+ cb = title;
110
+ title = instructions = undefined;
111
+ } else if (typeof instructions === 'function') {
112
+ cb = instructions;
113
+ instructions = undefined;
114
+ } else if (typeof cb !== 'function') {
115
+ cb = undefined;
125
116
  }
126
117
 
127
- algoList = cfg.algorithms.compress;
128
- if (Array.isArray(algoList) && algoList.length > 0) {
129
- algosSupported = ALGORITHMS.SUPPORTED_COMPRESS;
130
- for (i = 0; i < algoList.length; ++i) {
131
- if (algosSupported.indexOf(algoList[i]) === -1)
132
- throw new Error('Unsupported compression algorithm: ' + algoList[i]);
118
+ for (let i = 0; i < prompts.length; ++i) {
119
+ if (typeof prompts[i] === 'string') {
120
+ prompts[i] = {
121
+ prompt: prompts[i],
122
+ echo: true
123
+ };
133
124
  }
134
- algorithms.compress = algoList;
135
125
  }
126
+
127
+ this._cb = cb;
128
+ this._initialResponse = true;
129
+
130
+ this._protocol.authInfoReq(title, instructions, prompts);
136
131
  }
132
+ }
137
133
 
138
- // Make sure we at least have some kind of valid list of support key
139
- // formats
140
- if (algorithms.serverHostKey === undefined) {
141
- var hostKeyAlgos = Object.keys(hostKeys);
142
- for (i = hostKeyAlgos.length - 1; i >= 0; --i) {
143
- if (!hostKeys[hostKeyAlgos[i]])
144
- hostKeyAlgos.splice(i, 1);
145
- }
146
- algorithms.serverHostKey = hostKeyAlgos;
134
+ class PKAuthContext extends AuthContext {
135
+ constructor(protocol, username, service, method, pkInfo, cb) {
136
+ super(protocol, username, service, method, cb);
137
+
138
+ this.key = { algo: pkInfo.keyAlgo, data: pkInfo.key };
139
+ this.signature = pkInfo.signature;
140
+ this.blob = pkInfo.blob;
147
141
  }
148
142
 
149
- if (!kaMgr
150
- && Server.KEEPALIVE_INTERVAL > 0
151
- && Server.KEEPALIVE_CLIENT_INTERVAL > 0
152
- && Server.KEEPALIVE_CLIENT_COUNT_MAX >= 0) {
153
- kaMgr = new KeepaliveManager(Server.KEEPALIVE_INTERVAL,
154
- Server.KEEPALIVE_CLIENT_INTERVAL,
155
- Server.KEEPALIVE_CLIENT_COUNT_MAX);
143
+ accept() {
144
+ if (!this.signature) {
145
+ this._initialResponse = true;
146
+ this._protocol.authPKOK(this.key.algo, this.key.data);
147
+ } else {
148
+ AuthContext.prototype.accept.call(this);
149
+ }
156
150
  }
151
+ }
157
152
 
158
- var self = this;
153
+ class HostbasedAuthContext extends AuthContext {
154
+ constructor(protocol, username, service, method, pkInfo, cb) {
155
+ super(protocol, username, service, method, cb);
159
156
 
160
- EventEmitter.call(this);
157
+ this.key = { algo: pkInfo.keyAlgo, data: pkInfo.key };
158
+ this.signature = pkInfo.signature;
159
+ this.blob = pkInfo.blob;
160
+ this.localHostname = pkInfo.localHostname;
161
+ this.localUsername = pkInfo.localUsername;
162
+ }
163
+ }
161
164
 
162
- if (typeof listener === 'function')
163
- self.on('connection', listener);
165
+ class PwdAuthContext extends AuthContext {
166
+ constructor(protocol, username, service, method, password, cb) {
167
+ super(protocol, username, service, method, cb);
164
168
 
165
- var streamcfg = {
166
- algorithms: algorithms,
167
- hostKeys: hostKeys,
168
- server: true
169
- };
170
- var keys;
171
- var len;
172
- for (i = 0, keys = Object.keys(cfg), len = keys.length; i < len; ++i) {
173
- var key = keys[i];
174
- if (key === 'privateKey'
175
- || key === 'publicKey'
176
- || key === 'passphrase'
177
- || key === 'algorithms'
178
- || key === 'hostKeys'
179
- || key === 'server') {
180
- continue;
181
- }
182
- streamcfg[key] = cfg[key];
169
+ this.password = password;
170
+ this._changeCb = undefined;
183
171
  }
184
172
 
185
- if (typeof streamcfg.debug === 'function') {
186
- var oldDebug = streamcfg.debug;
187
- var cfgKeys = Object.keys(streamcfg);
173
+ requestChange(prompt, cb) {
174
+ if (this._changeCb)
175
+ throw new Error('Change request already in progress');
176
+ if (typeof prompt !== 'string')
177
+ throw new Error('prompt argument must be a string');
178
+ if (typeof cb !== 'function')
179
+ throw new Error('Callback argument must be a function');
180
+ this._changeCb = cb;
181
+ this._protocol.authPasswdChg(prompt);
188
182
  }
183
+ }
189
184
 
190
- this._srv = new net.Server(function(socket) {
191
- if (self._connections >= self.maxConnections) {
192
- socket.destroy();
193
- return;
194
- }
195
- ++self._connections;
196
- socket.once('close', function(had_err) {
197
- --self._connections;
198
-
199
- // since joyent/node#993bb93e0a, we have to "read past EOF" in order to
200
- // get an `end` event on streams. thankfully adding this does not
201
- // negatively affect node versions pre-joyent/node#993bb93e0a.
202
- sshstream.read();
203
- }).on('error', function(err) {
204
- sshstream.reset();
205
- sshstream.emit('error', err);
206
- });
207
185
 
208
- var conncfg = streamcfg;
186
+ class Session extends EventEmitter {
187
+ constructor(client, info, localChan) {
188
+ super();
209
189
 
210
- // prepend debug output with a unique identifier in case there are multiple
211
- // clients connected at the same time
212
- if (oldDebug) {
213
- conncfg = {};
214
- for (var i = 0, key; i < cfgKeys.length; ++i) {
215
- key = cfgKeys[i];
216
- conncfg[key] = streamcfg[key];
190
+ this.type = 'session';
191
+ this.subtype = undefined;
192
+ this.server = true;
193
+ this._ending = false;
194
+ this._channel = undefined;
195
+ this._chanInfo = {
196
+ type: 'session',
197
+ incoming: {
198
+ id: localChan,
199
+ window: MAX_WINDOW,
200
+ packetSize: PACKET_SIZE,
201
+ state: 'open'
202
+ },
203
+ outgoing: {
204
+ id: info.sender,
205
+ window: info.window,
206
+ packetSize: info.packetSize,
207
+ state: 'open'
217
208
  }
218
- var debugPrefix = '[' + process.hrtime().join('.') + '] ';
219
- conncfg.debug = function(msg) {
220
- oldDebug(debugPrefix + msg);
221
- };
222
- }
223
-
224
- var sshstream = new SSH2Stream(conncfg);
225
- var client = new Client(sshstream, socket);
226
-
227
- socket.pipe(sshstream).pipe(socket);
209
+ };
210
+ }
211
+ }
228
212
 
229
- // silence pre-header errors
230
- function onClientPreHeaderError(err) {}
231
- client.on('error', onClientPreHeaderError);
232
213
 
233
- sshstream.once('header', function(header) {
234
- if (sshstream._readableState.ended) {
235
- // already disconnected internally in SSH2Stream due to incompatible
236
- // protocol version
237
- return;
238
- } else if (!listenerCount(self, 'connection')) {
239
- // auto reject
240
- return sshstream.disconnect(DISCONNECT_REASON.BY_APPLICATION);
214
+ class Server extends EventEmitter {
215
+ constructor(cfg, listener) {
216
+ super();
217
+
218
+ if (typeof cfg !== 'object' || cfg === null)
219
+ throw new Error('Missing configuration object');
220
+
221
+ const hostKeys = Object.create(null);
222
+ const hostKeyAlgoOrder = [];
223
+
224
+ const hostKeys_ = cfg.hostKeys;
225
+ if (!Array.isArray(hostKeys_))
226
+ throw new Error('hostKeys must be an array');
227
+
228
+ const cfgAlgos = (
229
+ typeof cfg.algorithms === 'object' && cfg.algorithms !== null
230
+ ? cfg.algorithms
231
+ : {}
232
+ );
233
+
234
+ const hostKeyAlgos = generateAlgorithmList(
235
+ cfgAlgos.serverHostKey,
236
+ DEFAULT_SERVER_HOST_KEY,
237
+ SUPPORTED_SERVER_HOST_KEY
238
+ );
239
+ for (let i = 0; i < hostKeys_.length; ++i) {
240
+ let privateKey;
241
+ if (Buffer.isBuffer(hostKeys_[i]) || typeof hostKeys_[i] === 'string')
242
+ privateKey = parseKey(hostKeys_[i]);
243
+ else
244
+ privateKey = parseKey(hostKeys_[i].key, hostKeys_[i].passphrase);
245
+
246
+ if (privateKey instanceof Error)
247
+ throw new Error(`Cannot parse privateKey: ${privateKey.message}`);
248
+
249
+ if (Array.isArray(privateKey)) {
250
+ // OpenSSH's newer format only stores 1 key for now
251
+ privateKey = privateKey[0];
241
252
  }
242
253
 
243
- client.removeListener('error', onClientPreHeaderError);
254
+ if (privateKey.getPrivatePEM() === null)
255
+ throw new Error('privateKey value contains an invalid private key');
256
+
257
+ // Discard key if we already found a key of the same type
258
+ if (hostKeyAlgoOrder.includes(privateKey.type))
259
+ continue;
260
+
261
+ if (privateKey.type === 'ssh-rsa') {
262
+ // SSH supports multiple signature hashing algorithms for RSA, so we add
263
+ // the algorithms in the desired order
264
+ let sha1Pos = hostKeyAlgos.indexOf('ssh-rsa');
265
+ const sha256Pos = hostKeyAlgos.indexOf('rsa-sha2-256');
266
+ const sha512Pos = hostKeyAlgos.indexOf('rsa-sha2-512');
267
+ if (sha1Pos === -1) {
268
+ // Fall back to giving SHA1 the lowest priority
269
+ sha1Pos = Infinity;
270
+ }
271
+ [sha1Pos, sha256Pos, sha512Pos].sort(compareNumbers).forEach((pos) => {
272
+ if (pos === -1)
273
+ return;
244
274
 
245
- self.emit('connection',
246
- client,
247
- { ip: socket.remoteAddress,
248
- family: socket.remoteFamily,
249
- port: socket.remotePort,
250
- header: header });
251
- });
252
- }).on('error', function(err) {
253
- self.emit('error', err);
254
- }).on('listening', function() {
255
- self.emit('listening');
256
- }).on('close', function() {
257
- self.emit('close');
258
- });
259
- this._connections = 0;
260
- this.maxConnections = Infinity;
261
- }
262
- inherits(Server, EventEmitter);
263
-
264
- Server.prototype.listen = function() {
265
- this._srv.listen.apply(this._srv, arguments);
266
- return this;
267
- };
268
-
269
- Server.prototype.address = function() {
270
- return this._srv.address();
271
- };
272
-
273
- Server.prototype.getConnections = function(cb) {
274
- this._srv.getConnections(cb);
275
- };
276
-
277
- Server.prototype.close = function(cb) {
278
- this._srv.close(cb);
279
- return this;
280
- };
281
-
282
- Server.prototype.ref = function() {
283
- this._srv.ref();
284
- };
285
-
286
- Server.prototype.unref = function() {
287
- this._srv.unref();
288
- };
289
-
290
-
291
- function Client(stream, socket) {
292
- EventEmitter.call(this);
293
-
294
- var self = this;
295
-
296
- this._sshstream = stream;
297
- var channels = this._channels = {};
298
- this._curChan = -1;
299
- this._sock = socket;
300
- this.noMoreSessions = false;
301
- this.authenticated = false;
302
-
303
- stream.on('end', function() {
304
- socket.resume();
305
- self.emit('end');
306
- }).on('close', function(hasErr) {
307
- self.emit('close', hasErr);
308
- }).on('error', function(err) {
309
- self.emit('error', err);
310
- }).on('drain', function() {
311
- self.emit('drain');
312
- }).on('continue', function() {
313
- self.emit('continue');
314
- });
315
-
316
- var exchanges = 0;
317
- var acceptedAuthSvc = false;
318
- var pendingAuths = [];
319
- var authCtx;
320
-
321
- // begin service/auth-related ================================================
322
- stream.on('SERVICE_REQUEST', function(service) {
323
- if (exchanges === 0
324
- || acceptedAuthSvc
325
- || self.authenticated
326
- || service !== 'ssh-userauth')
327
- return stream.disconnect(DISCONNECT_REASON.SERVICE_NOT_AVAILABLE);
328
-
329
- acceptedAuthSvc = true;
330
- stream.serviceAccept(service);
331
- }).on('USERAUTH_REQUEST', onUSERAUTH_REQUEST);
332
- function onUSERAUTH_REQUEST(username, service, method, methodData) {
333
- if (exchanges === 0
334
- || (authCtx
335
- && (authCtx.username !== username || authCtx.service !== service))
336
- // TODO: support hostbased auth
337
- || (method !== 'password'
338
- && method !== 'publickey'
339
- && method !== 'hostbased'
340
- && method !== 'keyboard-interactive'
341
- && method !== 'none')
342
- || pendingAuths.length === MAX_PENDING_AUTHS)
343
- return stream.disconnect(DISCONNECT_REASON.PROTOCOL_ERROR);
344
- else if (service !== 'ssh-connection')
345
- return stream.disconnect(DISCONNECT_REASON.SERVICE_NOT_AVAILABLE);
346
-
347
- // XXX: this really shouldn't be reaching into private state ...
348
- stream._state.authMethod = method;
349
-
350
- var ctx;
351
- if (method === 'keyboard-interactive') {
352
- ctx = new KeyboardAuthContext(stream, username, service, method,
353
- methodData, onAuthDecide);
354
- } else if (method === 'publickey') {
355
- ctx = new PKAuthContext(stream, username, service, method, methodData,
356
- onAuthDecide);
357
- } else if (method === 'hostbased') {
358
- ctx = new HostbasedAuthContext(stream, username, service, method,
359
- methodData, onAuthDecide);
360
- } else if (method === 'password') {
361
- ctx = new PwdAuthContext(stream, username, service, method, methodData,
362
- onAuthDecide);
363
- } else if (method === 'none')
364
- ctx = new AuthContext(stream, username, service, method, onAuthDecide);
365
-
366
- if (authCtx) {
367
- if (!authCtx._initialResponse)
368
- return pendingAuths.push(ctx);
369
- else if (authCtx._multistep && !this._finalResponse) {
370
- // RFC 4252 says to silently abort the current auth request if a new
371
- // auth request comes in before the final response from an auth method
372
- // that requires additional request/response exchanges -- this means
373
- // keyboard-interactive for now ...
374
- authCtx._cleanup && authCtx._cleanup();
375
- authCtx.emit('abort');
376
- }
377
- }
275
+ let type;
276
+ switch (pos) {
277
+ case sha1Pos: type = 'ssh-rsa'; break;
278
+ case sha256Pos: type = 'rsa-sha2-256'; break;
279
+ case sha512Pos: type = 'rsa-sha2-512'; break;
280
+ default: return;
281
+ }
378
282
 
379
- authCtx = ctx;
283
+ // Store same RSA key under each hash algorithm name for convenience
284
+ hostKeys[type] = privateKey;
380
285
 
381
- if (listenerCount(self, 'authentication'))
382
- self.emit('authentication', authCtx);
383
- else
384
- authCtx.reject();
385
- }
386
- function onAuthDecide(ctx, allowed, methodsLeft, isPartial) {
387
- if (authCtx === ctx && !self.authenticated) {
388
- if (allowed) {
389
- stream.removeListener('USERAUTH_REQUEST', onUSERAUTH_REQUEST);
390
- authCtx = undefined;
391
- self.authenticated = true;
392
- stream.authSuccess();
393
- pendingAuths = [];
394
- self.emit('ready');
286
+ hostKeyAlgoOrder.push(type);
287
+ });
395
288
  } else {
396
- stream.authFailure(methodsLeft, isPartial);
397
- if (pendingAuths.length) {
398
- authCtx = pendingAuths.pop();
399
- if (listenerCount(self, 'authentication'))
400
- self.emit('authentication', authCtx);
401
- else
402
- authCtx.reject();
403
- }
289
+ hostKeys[privateKey.type] = privateKey;
290
+ hostKeyAlgoOrder.push(privateKey.type);
404
291
  }
405
292
  }
406
- }
407
- // end service/auth-related ==================================================
408
-
409
- var unsentGlobalRequestsReplies = [];
410
-
411
- function sendReplies() {
412
- var reply;
413
- while (unsentGlobalRequestsReplies.length > 0
414
- && unsentGlobalRequestsReplies[0].type) {
415
- reply = unsentGlobalRequestsReplies.shift();
416
- if (reply.type === 'SUCCESS')
417
- stream.requestSuccess(reply.buf);
418
- if (reply.type === 'FAILURE')
419
- stream.requestFailure();
420
- }
421
- }
422
293
 
423
- stream.on('GLOBAL_REQUEST', function(name, wantReply, data) {
424
- var reply = {
425
- type: null,
426
- buf: null
294
+ const algorithms = {
295
+ kex: generateAlgorithmList(cfgAlgos.kex, DEFAULT_KEX, SUPPORTED_KEX),
296
+ serverHostKey: hostKeyAlgoOrder,
297
+ cs: {
298
+ cipher: generateAlgorithmList(
299
+ cfgAlgos.cipher,
300
+ DEFAULT_CIPHER,
301
+ SUPPORTED_CIPHER
302
+ ),
303
+ mac: generateAlgorithmList(cfgAlgos.hmac, DEFAULT_MAC, SUPPORTED_MAC),
304
+ compress: generateAlgorithmList(
305
+ cfgAlgos.compress,
306
+ DEFAULT_COMPRESSION,
307
+ SUPPORTED_COMPRESSION
308
+ ),
309
+ lang: [],
310
+ },
311
+ sc: undefined,
427
312
  };
313
+ algorithms.sc = algorithms.cs;
428
314
 
429
- function setReply(type, buf) {
430
- reply.type = type;
431
- reply.buf = buf;
432
- sendReplies();
433
- }
315
+ if (typeof listener === 'function')
316
+ this.on('connection', listener);
434
317
 
435
- if (wantReply)
436
- unsentGlobalRequestsReplies.push(reply);
437
-
438
- if ((name === 'tcpip-forward'
439
- || name === 'cancel-tcpip-forward'
440
- || name === 'no-more-sessions@openssh.com'
441
- || name === 'streamlocal-forward@openssh.com'
442
- || name === 'cancel-streamlocal-forward@openssh.com')
443
- && listenerCount(self, 'request')
444
- && self.authenticated) {
445
- var accept;
446
- var reject;
447
-
448
- if (wantReply) {
449
- var replied = false;
450
- accept = function(chosenPort) {
451
- if (replied)
452
- return;
453
- replied = true;
454
- var bufPort;
455
- if (name === 'tcpip-forward'
456
- && data.bindPort === 0
457
- && typeof chosenPort === 'number') {
458
- bufPort = Buffer.allocUnsafe(4);
459
- writeUInt32BE(bufPort, chosenPort, 0);
460
- }
461
- setReply('SUCCESS', bufPort);
462
- };
463
- reject = function() {
464
- if (replied)
465
- return;
466
- replied = true;
467
- setReply('FAILURE');
468
- };
469
- }
318
+ const origDebug = (typeof cfg.debug === 'function' ? cfg.debug : undefined);
319
+ const ident = (cfg.ident ? Buffer.from(cfg.ident) : undefined);
320
+ const offer = new KexInit(algorithms);
470
321
 
471
- if (name === 'no-more-sessions@openssh.com') {
472
- self.noMoreSessions = true;
473
- accept && accept();
322
+ this._srv = new netServer((socket) => {
323
+ if (this._connections >= this.maxConnections) {
324
+ socket.destroy();
474
325
  return;
475
326
  }
327
+ ++this._connections;
328
+ socket.once('close', () => {
329
+ --this._connections;
330
+ });
476
331
 
477
- self.emit('request', accept, reject, name, data);
478
- } else if (wantReply)
479
- setReply('FAILURE');
480
- });
481
-
482
- stream.on('CHANNEL_OPEN', function(info) {
483
- // do early reject in some cases to prevent wasteful channel allocation
484
- if ((info.type === 'session' && self.noMoreSessions)
485
- || !self.authenticated) {
486
- var reasonCode = CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED;
487
- return stream.channelOpenFail(info.sender, reasonCode);
488
- }
332
+ let debug;
333
+ if (origDebug) {
334
+ // Prepend debug output with a unique identifier in case there are
335
+ // multiple clients connected at the same time
336
+ const debugPrefix = `[${process.hrtime().join('.')}] `;
337
+ debug = (msg) => {
338
+ origDebug(`${debugPrefix}${msg}`);
339
+ };
340
+ }
489
341
 
490
- var localChan = nextChannel(self);
491
- var accept;
492
- var reject;
493
- var replied = false;
494
- if (localChan === false) {
495
- // auto-reject due to no channels available
496
- return stream.channelOpenFail(info.sender,
497
- CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE);
498
- }
342
+ // eslint-disable-next-line no-use-before-define
343
+ new Client(socket, hostKeys, ident, offer, debug, this, cfg);
344
+ }).on('error', (err) => {
345
+ this.emit('error', err);
346
+ }).on('listening', () => {
347
+ this.emit('listening');
348
+ }).on('close', () => {
349
+ this.emit('close');
350
+ });
351
+ this._connections = 0;
352
+ this.maxConnections = Infinity;
353
+ }
499
354
 
500
- // be optimistic, reserve channel to prevent another request from trying to
501
- // take the same channel
502
- channels[localChan] = true;
355
+ injectSocket(socket) {
356
+ this._srv.emit('connection', socket);
357
+ }
503
358
 
504
- reject = function() {
505
- if (replied)
506
- return;
359
+ listen(...args) {
360
+ this._srv.listen(...args);
361
+ return this;
362
+ }
507
363
 
508
- replied = true;
364
+ address() {
365
+ return this._srv.address();
366
+ }
509
367
 
510
- delete channels[localChan];
368
+ getConnections(cb) {
369
+ this._srv.getConnections(cb);
370
+ return this;
371
+ }
511
372
 
512
- var reasonCode = CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED;
513
- return stream.channelOpenFail(info.sender, reasonCode);
514
- };
373
+ close(cb) {
374
+ this._srv.close(cb);
375
+ return this;
376
+ }
515
377
 
516
- switch (info.type) {
517
- case 'session':
518
- if (listenerCount(self, 'session')) {
519
- accept = function() {
520
- if (replied)
521
- return;
378
+ ref() {
379
+ this._srv.ref();
380
+ return this;
381
+ }
522
382
 
523
- replied = true;
383
+ unref() {
384
+ this._srv.unref();
385
+ return this;
386
+ }
387
+ }
388
+ Server.KEEPALIVE_CLIENT_INTERVAL = 15000;
389
+ Server.KEEPALIVE_CLIENT_COUNT_MAX = 3;
524
390
 
525
- stream.channelOpenConfirm(info.sender,
526
- localChan,
527
- Channel.MAX_WINDOW,
528
- Channel.PACKET_SIZE);
529
391
 
530
- return new Session(self, info, localChan);
531
- };
392
+ class Client extends EventEmitter {
393
+ constructor(socket, hostKeys, ident, offer, debug, server, srvCfg) {
394
+ super();
395
+
396
+ let exchanges = 0;
397
+ let acceptedAuthSvc = false;
398
+ let pendingAuths = [];
399
+ let authCtx;
400
+ let kaTimer;
401
+ let onPacket;
402
+ const unsentGlobalRequestsReplies = [];
403
+ this._sock = socket;
404
+ this._chanMgr = new ChannelManager(this);
405
+ this._debug = debug;
406
+ this.noMoreSessions = false;
407
+ this.authenticated = false;
408
+
409
+ // Silence pre-header errors
410
+ function onClientPreHeaderError(err) {}
411
+ this.on('error', onClientPreHeaderError);
532
412
 
533
- self.emit('session', accept, reject);
534
- } else
535
- reject();
536
- break;
537
- case 'direct-tcpip':
538
- if (listenerCount(self, 'tcpip')) {
539
- accept = function() {
540
- if (replied)
541
- return;
413
+ const DEBUG_HANDLER = (!debug ? undefined : (p, display, msg) => {
414
+ debug(`Debug output from client: ${JSON.stringify(msg)}`);
415
+ });
542
416
 
543
- replied = true;
417
+ const kaIntvl = (
418
+ typeof srvCfg.keepaliveInterval === 'number'
419
+ && isFinite(srvCfg.keepaliveInterval)
420
+ && srvCfg.keepaliveInterval > 0
421
+ ? srvCfg.keepaliveInterval
422
+ : (
423
+ typeof Server.KEEPALIVE_CLIENT_INTERVAL === 'number'
424
+ && isFinite(Server.KEEPALIVE_CLIENT_INTERVAL)
425
+ && Server.KEEPALIVE_CLIENT_INTERVAL > 0
426
+ ? Server.KEEPALIVE_CLIENT_INTERVAL
427
+ : -1
428
+ )
429
+ );
430
+ const kaCountMax = (
431
+ typeof srvCfg.keepaliveCountMax === 'number'
432
+ && isFinite(srvCfg.keepaliveCountMax)
433
+ && srvCfg.keepaliveCountMax >= 0
434
+ ? srvCfg.keepaliveCountMax
435
+ : (
436
+ typeof Server.KEEPALIVE_CLIENT_COUNT_MAX === 'number'
437
+ && isFinite(Server.KEEPALIVE_CLIENT_COUNT_MAX)
438
+ && Server.KEEPALIVE_CLIENT_COUNT_MAX >= 0
439
+ ? Server.KEEPALIVE_CLIENT_COUNT_MAX
440
+ : -1
441
+ )
442
+ );
443
+ let kaCurCount = 0;
444
+ if (kaIntvl !== -1 && kaCountMax !== -1) {
445
+ this.once('ready', () => {
446
+ const onClose = () => {
447
+ clearInterval(kaTimer);
448
+ };
449
+ this.on('close', onClose).on('end', onClose);
450
+ kaTimer = setInterval(() => {
451
+ if (++kaCurCount > kaCountMax) {
452
+ clearInterval(kaTimer);
453
+ const err = new Error('Keepalive timeout');
454
+ err.level = 'client-timeout';
455
+ this.emit('error', err);
456
+ this.end();
457
+ } else {
458
+ // XXX: if the server ever starts sending real global requests to
459
+ // the client, we will need to add a dummy callback here to
460
+ // keep the correct reply order
461
+ proto.ping();
462
+ }
463
+ }, kaIntvl);
464
+ });
465
+ // TODO: re-verify keepalive behavior with OpenSSH
466
+ onPacket = () => {
467
+ kaTimer && kaTimer.refresh();
468
+ kaCurCount = 0;
469
+ };
470
+ }
544
471
 
545
- stream.channelOpenConfirm(info.sender,
546
- localChan,
547
- Channel.MAX_WINDOW,
548
- Channel.PACKET_SIZE);
549
-
550
- var chaninfo = {
551
- type: undefined,
552
- incoming: {
553
- id: localChan,
554
- window: Channel.MAX_WINDOW,
555
- packetSize: Channel.PACKET_SIZE,
556
- state: 'open'
557
- },
558
- outgoing: {
559
- id: info.sender,
560
- window: info.window,
561
- packetSize: info.packetSize,
562
- state: 'open'
563
- }
564
- };
472
+ const proto = this._protocol = new Protocol({
473
+ server: true,
474
+ hostKeys,
475
+ ident,
476
+ offer,
477
+ onPacket,
478
+ greeting: srvCfg.greeting,
479
+ banner: srvCfg.banner,
480
+ onWrite: (data) => {
481
+ if (isWritable(socket))
482
+ socket.write(data);
483
+ },
484
+ onError: (err) => {
485
+ if (!proto._destruct)
486
+ socket.removeAllListeners('data');
487
+ this.emit('error', err);
488
+ try {
489
+ socket.end();
490
+ } catch {}
491
+ },
492
+ onHeader: (header) => {
493
+ this.removeListener('error', onClientPreHeaderError);
494
+
495
+ const info = {
496
+ ip: socket.remoteAddress,
497
+ family: socket.remoteFamily,
498
+ port: socket.remotePort,
499
+ header,
500
+ };
501
+ if (!server.emit('connection', this, info)) {
502
+ // auto reject
503
+ proto.disconnect(DISCONNECT_REASON.BY_APPLICATION);
504
+ socket.end();
505
+ return;
506
+ }
565
507
 
566
- return new Channel(chaninfo, self);
567
- };
508
+ if (header.greeting)
509
+ this.emit('greeting', header.greeting);
510
+ },
511
+ onHandshakeComplete: (negotiated) => {
512
+ if (++exchanges > 1)
513
+ this.emit('rekey');
514
+ this.emit('handshake', negotiated);
515
+ },
516
+ debug,
517
+ messageHandlers: {
518
+ DEBUG: DEBUG_HANDLER,
519
+ DISCONNECT: (p, reason, desc) => {
520
+ if (reason !== DISCONNECT_REASON.BY_APPLICATION) {
521
+ if (!desc) {
522
+ desc = DISCONNECT_REASON_BY_VALUE[reason];
523
+ if (desc === undefined)
524
+ desc = `Unexpected disconnection reason: ${reason}`;
525
+ }
526
+ const err = new Error(desc);
527
+ err.code = reason;
528
+ this.emit('error', err);
529
+ }
530
+ socket.end();
531
+ },
532
+ CHANNEL_OPEN: (p, info) => {
533
+ // Handle incoming requests from client
534
+
535
+ // Do early reject in some cases to prevent wasteful channel
536
+ // allocation
537
+ if ((info.type === 'session' && this.noMoreSessions)
538
+ || !this.authenticated) {
539
+ const reasonCode = CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED;
540
+ return proto.channelOpenFail(info.sender, reasonCode);
541
+ }
568
542
 
569
- self.emit('tcpip', accept, reject, info.data);
570
- } else
571
- reject();
572
- break;
573
- case 'direct-streamlocal@openssh.com':
574
- if (listenerCount(self, 'openssh.streamlocal')) {
575
- accept = function() {
543
+ let localChan = -1;
544
+ let reason;
545
+ let replied = false;
546
+
547
+ let accept;
548
+ const reject = () => {
576
549
  if (replied)
577
550
  return;
578
-
579
551
  replied = true;
580
552
 
581
- stream.channelOpenConfirm(info.sender,
582
- localChan,
583
- Channel.MAX_WINDOW,
584
- Channel.PACKET_SIZE);
585
-
586
- var chaninfo = {
587
- type: undefined,
588
- incoming: {
589
- id: localChan,
590
- window: Channel.MAX_WINDOW,
591
- packetSize: Channel.PACKET_SIZE,
592
- state: 'open'
593
- },
594
- outgoing: {
595
- id: info.sender,
596
- window: info.window,
597
- packetSize: info.packetSize,
598
- state: 'open'
553
+ if (reason === undefined) {
554
+ if (localChan === -1)
555
+ reason = CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE;
556
+ else
557
+ reason = CHANNEL_OPEN_FAILURE.CONNECT_FAILED;
558
+ }
559
+
560
+ if (localChan !== -1)
561
+ this._chanMgr.remove(localChan);
562
+ proto.channelOpenFail(info.sender, reason, '');
563
+ };
564
+ const reserveChannel = () => {
565
+ localChan = this._chanMgr.add();
566
+
567
+ if (localChan === -1) {
568
+ reason = CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE;
569
+ if (debug) {
570
+ debug('Automatic rejection of incoming channel open: '
571
+ + 'no channels available');
599
572
  }
600
- };
573
+ }
601
574
 
602
- return new Channel(chaninfo, self);
575
+ return (localChan !== -1);
603
576
  };
604
577
 
605
- self.emit('openssh.streamlocal', accept, reject, info.data);
606
- } else
607
- reject();
608
- break;
609
- default:
610
- // auto-reject unsupported channel types
611
- reject();
612
- }
613
- });
578
+ const data = info.data;
579
+ switch (info.type) {
580
+ case 'session':
581
+ if (listenerCount(this, 'session') && reserveChannel()) {
582
+ accept = () => {
583
+ if (replied)
584
+ return;
585
+ replied = true;
614
586
 
615
- stream.on('NEWKEYS', function() {
616
- if (++exchanges > 1)
617
- self.emit('rekey');
618
- });
587
+ const instance = new Session(this, info, localChan);
588
+ this._chanMgr.update(localChan, instance);
619
589
 
620
- if (kaMgr) {
621
- this.once('ready', function() {
622
- kaMgr.add(stream);
623
- });
624
- }
625
- }
626
- inherits(Client, EventEmitter);
590
+ proto.channelOpenConfirm(info.sender,
591
+ localChan,
592
+ MAX_WINDOW,
593
+ PACKET_SIZE);
627
594
 
628
- Client.prototype.end = function() {
629
- return this._sshstream.disconnect(DISCONNECT_REASON.BY_APPLICATION);
630
- };
595
+ return instance;
596
+ };
631
597
 
632
- Client.prototype.x11 = function(originAddr, originPort, cb) {
633
- var opts = {
634
- originAddr: originAddr,
635
- originPort: originPort
636
- };
637
- return openChannel(this, 'x11', opts, cb);
638
- };
639
-
640
- Client.prototype.forwardOut = function(boundAddr, boundPort, remoteAddr,
641
- remotePort, cb) {
642
- var opts = {
643
- boundAddr: boundAddr,
644
- boundPort: boundPort,
645
- remoteAddr: remoteAddr,
646
- remotePort: remotePort
647
- };
648
- return openChannel(this, 'forwarded-tcpip', opts, cb);
649
- };
598
+ this.emit('session', accept, reject);
599
+ return;
600
+ }
601
+ break;
602
+ case 'direct-tcpip':
603
+ if (listenerCount(this, 'tcpip') && reserveChannel()) {
604
+ accept = () => {
605
+ if (replied)
606
+ return;
607
+ replied = true;
608
+
609
+ const chanInfo = {
610
+ type: undefined,
611
+ incoming: {
612
+ id: localChan,
613
+ window: MAX_WINDOW,
614
+ packetSize: PACKET_SIZE,
615
+ state: 'open'
616
+ },
617
+ outgoing: {
618
+ id: info.sender,
619
+ window: info.window,
620
+ packetSize: info.packetSize,
621
+ state: 'open'
622
+ }
623
+ };
624
+
625
+ const stream = new Channel(this, chanInfo, { server: true });
626
+ this._chanMgr.update(localChan, stream);
627
+
628
+ proto.channelOpenConfirm(info.sender,
629
+ localChan,
630
+ MAX_WINDOW,
631
+ PACKET_SIZE);
632
+
633
+ return stream;
634
+ };
635
+
636
+ this.emit('tcpip', accept, reject, data);
637
+ return;
638
+ }
639
+ break;
640
+ case 'direct-streamlocal@openssh.com':
641
+ if (listenerCount(this, 'openssh.streamlocal')
642
+ && reserveChannel()) {
643
+ accept = () => {
644
+ if (replied)
645
+ return;
646
+ replied = true;
647
+
648
+ const chanInfo = {
649
+ type: undefined,
650
+ incoming: {
651
+ id: localChan,
652
+ window: MAX_WINDOW,
653
+ packetSize: PACKET_SIZE,
654
+ state: 'open'
655
+ },
656
+ outgoing: {
657
+ id: info.sender,
658
+ window: info.window,
659
+ packetSize: info.packetSize,
660
+ state: 'open'
661
+ }
662
+ };
663
+
664
+ const stream = new Channel(this, chanInfo, { server: true });
665
+ this._chanMgr.update(localChan, stream);
666
+
667
+ proto.channelOpenConfirm(info.sender,
668
+ localChan,
669
+ MAX_WINDOW,
670
+ PACKET_SIZE);
671
+
672
+ return stream;
673
+ };
674
+
675
+ this.emit('openssh.streamlocal', accept, reject, data);
676
+ return;
677
+ }
678
+ break;
679
+ default:
680
+ // Automatically reject any unsupported channel open requests
681
+ reason = CHANNEL_OPEN_FAILURE.UNKNOWN_CHANNEL_TYPE;
682
+ if (debug) {
683
+ debug('Automatic rejection of unsupported incoming channel open'
684
+ + ` type: ${info.type}`);
685
+ }
686
+ }
650
687
 
651
- Client.prototype.openssh_forwardOutStreamLocal = function(socketPath, cb) {
652
- var opts = {
653
- socketPath: socketPath
654
- };
655
- return openChannel(this, 'forwarded-streamlocal@openssh.com', opts, cb);
656
- };
657
-
658
- Client.prototype.rekey = function(cb) {
659
- var stream = this._sshstream;
660
- var ret = true;
661
- var error;
662
-
663
- try {
664
- ret = stream.rekey();
665
- } catch (ex) {
666
- error = ex;
667
- }
688
+ if (reason === undefined) {
689
+ reason = CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED;
690
+ if (debug) {
691
+ debug('Automatic rejection of unexpected incoming channel open'
692
+ + ` for: ${info.type}`);
693
+ }
694
+ }
668
695
 
669
- // TODO: re-throw error if no callback?
696
+ reject();
697
+ },
698
+ CHANNEL_OPEN_CONFIRMATION: (p, info) => {
699
+ const channel = this._chanMgr.get(info.recipient);
700
+ if (typeof channel !== 'function')
701
+ return;
670
702
 
671
- if (typeof cb === 'function') {
672
- if (error) {
673
- process.nextTick(function() {
674
- cb(error);
675
- });
676
- } else
677
- this.once('rekey', cb);
678
- }
703
+ const chanInfo = {
704
+ type: channel.type,
705
+ incoming: {
706
+ id: info.recipient,
707
+ window: MAX_WINDOW,
708
+ packetSize: PACKET_SIZE,
709
+ state: 'open'
710
+ },
711
+ outgoing: {
712
+ id: info.sender,
713
+ window: info.window,
714
+ packetSize: info.packetSize,
715
+ state: 'open'
716
+ }
717
+ };
679
718
 
680
- return ret;
681
- };
682
-
683
- function Session(client, info, localChan) {
684
- this.subtype = undefined;
685
-
686
- var ending = false;
687
- var self = this;
688
- var outgoingId = info.sender;
689
- var channel;
690
-
691
- var chaninfo = {
692
- type: 'session',
693
- incoming: {
694
- id: localChan,
695
- window: Channel.MAX_WINDOW,
696
- packetSize: Channel.PACKET_SIZE,
697
- state: 'open'
698
- },
699
- outgoing: {
700
- id: info.sender,
701
- window: info.window,
702
- packetSize: info.packetSize,
703
- state: 'open'
704
- }
705
- };
719
+ const instance = new Channel(this, chanInfo, { server: true });
720
+ this._chanMgr.update(info.recipient, instance);
721
+ channel(undefined, instance);
722
+ },
723
+ CHANNEL_OPEN_FAILURE: (p, recipient, reason, description) => {
724
+ const channel = this._chanMgr.get(recipient);
725
+ if (typeof channel !== 'function')
726
+ return;
706
727
 
707
- function onREQUEST(info) {
708
- var replied = false;
709
- var accept;
710
- var reject;
711
-
712
- if (info.wantReply) {
713
- // "real session" requests will have custom accept behaviors
714
- if (info.request !== 'shell'
715
- && info.request !== 'exec'
716
- && info.request !== 'subsystem') {
717
- accept = function() {
718
- if (replied || ending || channel)
728
+ const info = { reason, description };
729
+ onChannelOpenFailure(this, recipient, info, channel);
730
+ },
731
+ CHANNEL_DATA: (p, recipient, data) => {
732
+ let channel = this._chanMgr.get(recipient);
733
+ if (typeof channel !== 'object' || channel === null)
719
734
  return;
720
735
 
721
- replied = true;
736
+ if (channel.constructor === Session) {
737
+ channel = channel._channel;
738
+ if (!channel)
739
+ return;
740
+ }
722
741
 
723
- return client._sshstream.channelSuccess(outgoingId);
724
- };
725
- }
742
+ // The remote party should not be sending us data if there is no
743
+ // window space available ...
744
+ // TODO: raise error on data with not enough window?
745
+ if (channel.incoming.window === 0)
746
+ return;
726
747
 
727
- reject = function() {
728
- if (replied || ending || channel)
729
- return;
748
+ channel.incoming.window -= data.length;
730
749
 
731
- replied = true;
750
+ if (channel.push(data) === false) {
751
+ channel._waitChanDrain = true;
752
+ return;
753
+ }
732
754
 
733
- return client._sshstream.channelFailure(outgoingId);
734
- };
735
- }
755
+ if (channel.incoming.window <= WINDOW_THRESHOLD)
756
+ windowAdjust(channel);
757
+ },
758
+ CHANNEL_EXTENDED_DATA: (p, recipient, data, type) => {
759
+ // NOOP -- should not be sent by client
760
+ },
761
+ CHANNEL_WINDOW_ADJUST: (p, recipient, amount) => {
762
+ let channel = this._chanMgr.get(recipient);
763
+ if (typeof channel !== 'object' || channel === null)
764
+ return;
736
765
 
737
- if (ending) {
738
- reject && reject();
739
- return;
740
- }
766
+ if (channel.constructor === Session) {
767
+ channel = channel._channel;
768
+ if (!channel)
769
+ return;
770
+ }
741
771
 
742
- switch (info.request) {
743
- // "pre-real session start" requests
744
- case 'env':
745
- if (listenerCount(self, 'env')) {
746
- self.emit('env', accept, reject, {
747
- key: info.key,
748
- val: info.val
749
- });
750
- } else
751
- reject && reject();
752
- break;
753
- case 'pty-req':
754
- if (listenerCount(self, 'pty')) {
755
- self.emit('pty', accept, reject, {
756
- cols: info.cols,
757
- rows: info.rows,
758
- width: info.width,
759
- height: info.height,
760
- term: info.term,
761
- modes: info.modes,
762
- });
763
- } else
764
- reject && reject();
765
- break;
766
- case 'window-change':
767
- if (listenerCount(self, 'window-change')) {
768
- self.emit('window-change', accept, reject, {
769
- cols: info.cols,
770
- rows: info.rows,
771
- width: info.width,
772
- height: info.height
773
- });
774
- } else
775
- reject && reject();
776
- break;
777
- case 'x11-req':
778
- if (listenerCount(self, 'x11')) {
779
- self.emit('x11', accept, reject, {
780
- single: info.single,
781
- protocol: info.protocol,
782
- cookie: info.cookie,
783
- screen: info.screen
784
- });
785
- } else
786
- reject && reject();
787
- break;
788
- // "post-real session start" requests
789
- case 'signal':
790
- if (listenerCount(self, 'signal')) {
791
- self.emit('signal', accept, reject, {
792
- name: info.signal
793
- });
794
- } else
795
- reject && reject();
796
- break;
797
- // XXX: is `auth-agent-req@openssh.com` really "post-real session start"?
798
- case 'auth-agent-req@openssh.com':
799
- if (listenerCount(self, 'auth-agent'))
800
- self.emit('auth-agent', accept, reject);
801
- else
802
- reject && reject();
803
- break;
804
- // "real session start" requests
805
- case 'shell':
806
- if (listenerCount(self, 'shell')) {
807
- accept = function() {
808
- if (replied || ending || channel)
772
+ // The other side is allowing us to send `amount` more bytes of data
773
+ channel.outgoing.window += amount;
774
+
775
+ if (channel._waitWindow) {
776
+ channel._waitWindow = false;
777
+
778
+ if (channel._chunk) {
779
+ channel._write(channel._chunk, null, channel._chunkcb);
780
+ } else if (channel._chunkcb) {
781
+ channel._chunkcb();
782
+ } else if (channel._chunkErr) {
783
+ channel.stderr._write(channel._chunkErr,
784
+ null,
785
+ channel._chunkcbErr);
786
+ } else if (channel._chunkcbErr) {
787
+ channel._chunkcbErr();
788
+ }
789
+ }
790
+ },
791
+ CHANNEL_SUCCESS: (p, recipient) => {
792
+ let channel = this._chanMgr.get(recipient);
793
+ if (typeof channel !== 'object' || channel === null)
794
+ return;
795
+
796
+ if (channel.constructor === Session) {
797
+ channel = channel._channel;
798
+ if (!channel)
809
799
  return;
800
+ }
810
801
 
811
- replied = true;
802
+ if (channel._callbacks.length)
803
+ channel._callbacks.shift()(false);
804
+ },
805
+ CHANNEL_FAILURE: (p, recipient) => {
806
+ let channel = this._chanMgr.get(recipient);
807
+ if (typeof channel !== 'object' || channel === null)
808
+ return;
812
809
 
813
- if (info.wantReply)
814
- client._sshstream.channelSuccess(outgoingId);
810
+ if (channel.constructor === Session) {
811
+ channel = channel._channel;
812
+ if (!channel)
813
+ return;
814
+ }
815
815
 
816
- channel = new Channel(chaninfo, client, { server: true });
816
+ if (channel._callbacks.length)
817
+ channel._callbacks.shift()(true);
818
+ },
819
+ CHANNEL_REQUEST: (p, recipient, type, wantReply, data) => {
820
+ const session = this._chanMgr.get(recipient);
821
+ if (typeof session !== 'object' || session === null)
822
+ return;
817
823
 
818
- channel.subtype = self.subtype = info.request;
824
+ let replied = false;
825
+ let accept;
826
+ let reject;
819
827
 
820
- return channel;
821
- };
828
+ if (session.constructor !== Session) {
829
+ // normal Channel instance
830
+ if (wantReply)
831
+ proto.channelFailure(session.outgoing.id);
832
+ return;
833
+ }
822
834
 
823
- self.emit('shell', accept, reject);
824
- } else
825
- reject && reject();
826
- break;
827
- case 'exec':
828
- if (listenerCount(self, 'exec')) {
829
- accept = function() {
830
- if (replied || ending || channel)
831
- return;
835
+ if (wantReply) {
836
+ // "real session" requests will have custom accept behaviors
837
+ if (type !== 'shell'
838
+ && type !== 'exec'
839
+ && type !== 'subsystem') {
840
+ accept = () => {
841
+ if (replied || session._ending || session._channel)
842
+ return;
843
+ replied = true;
844
+
845
+ proto.channelSuccess(session._chanInfo.outgoing.id);
846
+ };
847
+ }
848
+
849
+ reject = () => {
850
+ if (replied || session._ending || session._channel)
851
+ return;
852
+ replied = true;
853
+
854
+ proto.channelFailure(session._chanInfo.outgoing.id);
855
+ };
856
+ }
832
857
 
833
- replied = true;
858
+ if (session._ending) {
859
+ reject && reject();
860
+ return;
861
+ }
834
862
 
835
- if (info.wantReply)
836
- client._sshstream.channelSuccess(outgoingId);
863
+ switch (type) {
864
+ // "pre-real session start" requests
865
+ case 'env':
866
+ if (listenerCount(session, 'env')) {
867
+ session.emit('env', accept, reject, {
868
+ key: data.name,
869
+ val: data.value
870
+ });
871
+ return;
872
+ }
873
+ break;
874
+ case 'pty-req':
875
+ if (listenerCount(session, 'pty')) {
876
+ session.emit('pty', accept, reject, data);
877
+ return;
878
+ }
879
+ break;
880
+ case 'window-change':
881
+ if (listenerCount(session, 'window-change'))
882
+ session.emit('window-change', accept, reject, data);
883
+ else
884
+ reject && reject();
885
+ break;
886
+ case 'x11-req':
887
+ if (listenerCount(session, 'x11')) {
888
+ session.emit('x11', accept, reject, data);
889
+ return;
890
+ }
891
+ break;
892
+ // "post-real session start" requests
893
+ case 'signal':
894
+ if (listenerCount(session, 'signal')) {
895
+ session.emit('signal', accept, reject, {
896
+ name: data
897
+ });
898
+ return;
899
+ }
900
+ break;
901
+ // XXX: is `auth-agent-req@openssh.com` really "post-real session
902
+ // start"?
903
+ case 'auth-agent-req@openssh.com':
904
+ if (listenerCount(session, 'auth-agent')) {
905
+ session.emit('auth-agent', accept, reject);
906
+ return;
907
+ }
908
+ break;
909
+ // "real session start" requests
910
+ case 'shell':
911
+ if (listenerCount(session, 'shell')) {
912
+ accept = () => {
913
+ if (replied || session._ending || session._channel)
914
+ return;
915
+ replied = true;
916
+
917
+ if (wantReply)
918
+ proto.channelSuccess(session._chanInfo.outgoing.id);
919
+
920
+ const channel = new Channel(
921
+ this, session._chanInfo, { server: true }
922
+ );
923
+
924
+ channel.subtype = session.subtype = type;
925
+ session._channel = channel;
926
+
927
+ return channel;
928
+ };
929
+
930
+ session.emit('shell', accept, reject);
931
+ return;
932
+ }
933
+ break;
934
+ case 'exec':
935
+ if (listenerCount(session, 'exec')) {
936
+ accept = () => {
937
+ if (replied || session._ending || session._channel)
938
+ return;
939
+ replied = true;
940
+
941
+ if (wantReply)
942
+ proto.channelSuccess(session._chanInfo.outgoing.id);
943
+
944
+ const channel = new Channel(
945
+ this, session._chanInfo, { server: true }
946
+ );
947
+
948
+ channel.subtype = session.subtype = type;
949
+ session._channel = channel;
950
+
951
+ return channel;
952
+ };
953
+
954
+ session.emit('exec', accept, reject, {
955
+ command: data
956
+ });
957
+ return;
958
+ }
959
+ break;
960
+ case 'subsystem': {
961
+ let useSFTP = (data === 'sftp');
962
+ accept = () => {
963
+ if (replied || session._ending || session._channel)
964
+ return;
965
+ replied = true;
966
+
967
+ if (wantReply)
968
+ proto.channelSuccess(session._chanInfo.outgoing.id);
969
+
970
+ let instance;
971
+ if (useSFTP) {
972
+ instance = new SFTP(this, session._chanInfo, {
973
+ server: true,
974
+ debug,
975
+ });
976
+ } else {
977
+ instance = new Channel(
978
+ this, session._chanInfo, { server: true }
979
+ );
980
+ instance.subtype =
981
+ session.subtype = `${type}:${data}`;
982
+ }
983
+ session._channel = instance;
984
+
985
+ return instance;
986
+ };
987
+
988
+ if (data === 'sftp') {
989
+ if (listenerCount(session, 'sftp')) {
990
+ session.emit('sftp', accept, reject);
991
+ return;
992
+ }
993
+ useSFTP = false;
994
+ }
995
+ if (listenerCount(session, 'subsystem')) {
996
+ session.emit('subsystem', accept, reject, {
997
+ name: data
998
+ });
999
+ return;
1000
+ }
1001
+ break;
1002
+ }
1003
+ }
1004
+ debug && debug(
1005
+ `Automatic rejection of incoming channel request: ${type}`
1006
+ );
1007
+ reject && reject();
1008
+ },
1009
+ CHANNEL_EOF: (p, recipient) => {
1010
+ let channel = this._chanMgr.get(recipient);
1011
+ if (typeof channel !== 'object' || channel === null)
1012
+ return;
837
1013
 
838
- channel = new Channel(chaninfo, client, { server: true });
1014
+ if (channel.constructor === Session) {
1015
+ if (!channel._ending) {
1016
+ channel._ending = true;
1017
+ channel.emit('eof');
1018
+ channel.emit('end');
1019
+ }
1020
+ channel = channel._channel;
1021
+ if (!channel)
1022
+ return;
1023
+ }
839
1024
 
840
- channel.subtype = self.subtype = info.request;
1025
+ if (channel.incoming.state !== 'open')
1026
+ return;
1027
+ channel.incoming.state = 'eof';
1028
+
1029
+ if (channel.readable)
1030
+ channel.push(null);
1031
+ },
1032
+ CHANNEL_CLOSE: (p, recipient) => {
1033
+ let channel = this._chanMgr.get(recipient);
1034
+ if (typeof channel !== 'object' || channel === null)
1035
+ return;
841
1036
 
842
- return channel;
843
- };
1037
+ if (channel.constructor === Session) {
1038
+ channel._ending = true;
1039
+ channel.emit('close');
1040
+ channel = channel._channel;
1041
+ if (!channel)
1042
+ return;
1043
+ }
844
1044
 
845
- self.emit('exec', accept, reject, {
846
- command: info.command
847
- });
848
- } else
849
- reject && reject();
850
- break;
851
- case 'subsystem':
852
- accept = function() {
853
- if (replied || ending || channel)
1045
+ onCHANNEL_CLOSE(this, recipient, channel);
1046
+ },
1047
+ // Begin service/auth-related ==========================================
1048
+ SERVICE_REQUEST: (p, service) => {
1049
+ if (exchanges === 0
1050
+ || acceptedAuthSvc
1051
+ || this.authenticated
1052
+ || service !== 'ssh-userauth') {
1053
+ proto.disconnect(DISCONNECT_REASON.SERVICE_NOT_AVAILABLE);
1054
+ socket.end();
854
1055
  return;
1056
+ }
855
1057
 
856
- replied = true;
1058
+ acceptedAuthSvc = true;
1059
+ proto.serviceAccept(service);
1060
+ },
1061
+ USERAUTH_REQUEST: (p, username, service, method, methodData) => {
1062
+ if (exchanges === 0
1063
+ || this.authenticated
1064
+ || (authCtx
1065
+ && (authCtx.username !== username
1066
+ || authCtx.service !== service))
1067
+ // TODO: support hostbased auth
1068
+ || (method !== 'password'
1069
+ && method !== 'publickey'
1070
+ && method !== 'hostbased'
1071
+ && method !== 'keyboard-interactive'
1072
+ && method !== 'none')
1073
+ || pendingAuths.length === MAX_PENDING_AUTHS) {
1074
+ proto.disconnect(DISCONNECT_REASON.PROTOCOL_ERROR);
1075
+ socket.end();
1076
+ return;
1077
+ } else if (service !== 'ssh-connection') {
1078
+ proto.disconnect(DISCONNECT_REASON.SERVICE_NOT_AVAILABLE);
1079
+ socket.end();
1080
+ return;
1081
+ }
857
1082
 
858
- if (info.wantReply)
859
- client._sshstream.channelSuccess(outgoingId);
1083
+ let ctx;
1084
+ switch (method) {
1085
+ case 'keyboard-interactive':
1086
+ ctx = new KeyboardAuthContext(proto, username, service, method,
1087
+ methodData, onAuthDecide);
1088
+ break;
1089
+ case 'publickey':
1090
+ ctx = new PKAuthContext(proto, username, service, method,
1091
+ methodData, onAuthDecide);
1092
+ break;
1093
+ case 'hostbased':
1094
+ ctx = new HostbasedAuthContext(proto, username, service, method,
1095
+ methodData, onAuthDecide);
1096
+ break;
1097
+ case 'password':
1098
+ if (authCtx
1099
+ && authCtx instanceof PwdAuthContext
1100
+ && authCtx._changeCb) {
1101
+ const cb = authCtx._changeCb;
1102
+ authCtx._changeCb = undefined;
1103
+ cb(methodData.newPassword);
1104
+ return;
1105
+ }
1106
+ ctx = new PwdAuthContext(proto, username, service, method,
1107
+ methodData, onAuthDecide);
1108
+ break;
1109
+ case 'none':
1110
+ ctx = new AuthContext(proto, username, service, method,
1111
+ onAuthDecide);
1112
+ break;
1113
+ }
860
1114
 
861
- channel = new Channel(chaninfo, client, { server: true });
1115
+ if (authCtx) {
1116
+ if (!authCtx._initialResponse) {
1117
+ return pendingAuths.push(ctx);
1118
+ } else if (authCtx._multistep && !authCtx._finalResponse) {
1119
+ // RFC 4252 says to silently abort the current auth request if a
1120
+ // new auth request comes in before the final response from an
1121
+ // auth method that requires additional request/response exchanges
1122
+ // -- this means keyboard-interactive for now ...
1123
+ authCtx._cleanup && authCtx._cleanup();
1124
+ authCtx.emit('abort');
1125
+ }
1126
+ }
862
1127
 
863
- channel.subtype = self.subtype = (info.request + ':' + info.subsystem);
1128
+ authCtx = ctx;
864
1129
 
865
- if (info.subsystem === 'sftp') {
866
- var sftp = new SFTPStream({
867
- server: true,
868
- debug: client._sshstream.debug
869
- });
870
- channel.pipe(sftp).pipe(channel);
1130
+ if (listenerCount(this, 'authentication'))
1131
+ this.emit('authentication', authCtx);
1132
+ else
1133
+ authCtx.reject();
1134
+ },
1135
+ USERAUTH_INFO_RESPONSE: (p, responses) => {
1136
+ if (authCtx && authCtx instanceof KeyboardAuthContext)
1137
+ authCtx._onInfoResponse(responses);
1138
+ },
1139
+ // End service/auth-related ============================================
1140
+ GLOBAL_REQUEST: (p, name, wantReply, data) => {
1141
+ const reply = {
1142
+ type: null,
1143
+ buf: null
1144
+ };
871
1145
 
872
- return sftp;
873
- } else
874
- return channel;
875
- };
1146
+ function setReply(type, buf) {
1147
+ reply.type = type;
1148
+ reply.buf = buf;
1149
+ sendReplies();
1150
+ }
876
1151
 
877
- if (info.subsystem === 'sftp' && listenerCount(self, 'sftp'))
878
- self.emit('sftp', accept, reject);
879
- else if (info.subsystem !== 'sftp' && listenerCount(self, 'subsystem')) {
880
- self.emit('subsystem', accept, reject, {
881
- name: info.subsystem
882
- });
883
- } else
884
- reject && reject();
885
- break;
886
- default:
887
- reject && reject();
888
- }
889
- }
890
- function onEOF() {
891
- ending = true;
892
- self.emit('eof');
893
- self.emit('end');
894
- }
895
- function onCLOSE() {
896
- ending = true;
897
- self.emit('close');
898
- }
899
- client._sshstream
900
- .on('CHANNEL_REQUEST:' + localChan, onREQUEST)
901
- .once('CHANNEL_EOF:' + localChan, onEOF)
902
- .once('CHANNEL_CLOSE:' + localChan, onCLOSE);
903
- }
904
- inherits(Session, EventEmitter);
1152
+ if (wantReply)
1153
+ unsentGlobalRequestsReplies.push(reply);
1154
+
1155
+ if ((name === 'tcpip-forward'
1156
+ || name === 'cancel-tcpip-forward'
1157
+ || name === 'no-more-sessions@openssh.com'
1158
+ || name === 'streamlocal-forward@openssh.com'
1159
+ || name === 'cancel-streamlocal-forward@openssh.com')
1160
+ && listenerCount(this, 'request')
1161
+ && this.authenticated) {
1162
+ let accept;
1163
+ let reject;
1164
+
1165
+ if (wantReply) {
1166
+ let replied = false;
1167
+ accept = (chosenPort) => {
1168
+ if (replied)
1169
+ return;
1170
+ replied = true;
1171
+ let bufPort;
1172
+ if (name === 'tcpip-forward'
1173
+ && data.bindPort === 0
1174
+ && typeof chosenPort === 'number') {
1175
+ bufPort = Buffer.allocUnsafe(4);
1176
+ writeUInt32BE(bufPort, chosenPort, 0);
1177
+ }
1178
+ setReply('SUCCESS', bufPort);
1179
+ };
1180
+ reject = () => {
1181
+ if (replied)
1182
+ return;
1183
+ replied = true;
1184
+ setReply('FAILURE');
1185
+ };
1186
+ }
1187
+
1188
+ if (name === 'no-more-sessions@openssh.com') {
1189
+ this.noMoreSessions = true;
1190
+ accept && accept();
1191
+ return;
1192
+ }
905
1193
 
1194
+ this.emit('request', accept, reject, name, data);
1195
+ } else if (wantReply) {
1196
+ setReply('FAILURE');
1197
+ }
1198
+ },
1199
+ },
1200
+ });
906
1201
 
907
- function AuthContext(stream, username, service, method, cb) {
908
- EventEmitter.call(this);
1202
+ socket.pause();
1203
+ cryptoInit.then(() => {
1204
+ proto.start();
1205
+ socket.on('data', (data) => {
1206
+ try {
1207
+ proto.parse(data, 0, data.length);
1208
+ } catch (ex) {
1209
+ this.emit('error', ex);
1210
+ try {
1211
+ if (isWritable(socket))
1212
+ socket.end();
1213
+ } catch {}
1214
+ }
1215
+ });
1216
+ socket.resume();
1217
+ }).catch((err) => {
1218
+ this.emit('error', err);
1219
+ try {
1220
+ if (isWritable(socket))
1221
+ socket.end();
1222
+ } catch {}
1223
+ });
1224
+ socket.on('error', (err) => {
1225
+ err.level = 'socket';
1226
+ this.emit('error', err);
1227
+ }).once('end', () => {
1228
+ debug && debug('Socket ended');
1229
+ proto.cleanup();
1230
+ this.emit('end');
1231
+ }).once('close', () => {
1232
+ debug && debug('Socket closed');
1233
+ proto.cleanup();
1234
+ this.emit('close');
1235
+
1236
+ const err = new Error('No response from server');
1237
+
1238
+ // Simulate error for pending channels and close any open channels
1239
+ this._chanMgr.cleanup(err);
1240
+ });
909
1241
 
910
- var self = this;
1242
+ const onAuthDecide = (ctx, allowed, methodsLeft, isPartial) => {
1243
+ if (authCtx === ctx && !this.authenticated) {
1244
+ if (allowed) {
1245
+ authCtx = undefined;
1246
+ this.authenticated = true;
1247
+ proto.authSuccess();
1248
+ pendingAuths = [];
1249
+ this.emit('ready');
1250
+ } else {
1251
+ proto.authFailure(methodsLeft, isPartial);
1252
+ if (pendingAuths.length) {
1253
+ authCtx = pendingAuths.pop();
1254
+ if (listenerCount(this, 'authentication'))
1255
+ this.emit('authentication', authCtx);
1256
+ else
1257
+ authCtx.reject();
1258
+ }
1259
+ }
1260
+ }
1261
+ };
911
1262
 
912
- this.username = this.user = username;
913
- this.service = service;
914
- this.method = method;
915
- this._initialResponse = false;
916
- this._finalResponse = false;
917
- this._multistep = false;
918
- this._cbfinal = function(allowed, methodsLeft, isPartial) {
919
- if (!self._finalResponse) {
920
- self._finalResponse = true;
921
- cb(self, allowed, methodsLeft, isPartial);
922
- }
923
- };
924
- this._stream = stream;
925
- }
926
- inherits(AuthContext, EventEmitter);
927
- AuthContext.prototype.accept = function() {
928
- this._cleanup && this._cleanup();
929
- this._initialResponse = true;
930
- this._cbfinal(true);
931
- };
932
- AuthContext.prototype.reject = function(methodsLeft, isPartial) {
933
- this._cleanup && this._cleanup();
934
- this._initialResponse = true;
935
- this._cbfinal(false, methodsLeft, isPartial);
936
- };
937
-
938
- var RE_KBINT_SUBMETHODS = /[ \t\r\n]*,[ \t\r\n]*/g;
939
- function KeyboardAuthContext(stream, username, service, method, submethods, cb) {
940
- AuthContext.call(this, stream, username, service, method, cb);
941
- this._multistep = true;
942
-
943
- var self = this;
944
-
945
- this._cb = undefined;
946
- this._onInfoResponse = function(responses) {
947
- if (self._cb) {
948
- var callback = self._cb;
949
- self._cb = undefined;
950
- callback(responses);
1263
+ function sendReplies() {
1264
+ while (unsentGlobalRequestsReplies.length > 0
1265
+ && unsentGlobalRequestsReplies[0].type) {
1266
+ const reply = unsentGlobalRequestsReplies.shift();
1267
+ if (reply.type === 'SUCCESS')
1268
+ proto.requestSuccess(reply.buf);
1269
+ if (reply.type === 'FAILURE')
1270
+ proto.requestFailure();
1271
+ }
951
1272
  }
952
- };
953
- this.submethods = submethods.split(RE_KBINT_SUBMETHODS);
954
- this.on('abort', function() {
955
- self._cb && self._cb(new Error('Authentication request aborted'));
956
- });
957
- }
958
- inherits(KeyboardAuthContext, AuthContext);
959
- KeyboardAuthContext.prototype._cleanup = function() {
960
- this._stream.removeListener('USERAUTH_INFO_RESPONSE', this._onInfoResponse);
961
- };
962
- KeyboardAuthContext.prototype.prompt = function(prompts, title, instructions,
963
- cb) {
964
- if (!Array.isArray(prompts))
965
- prompts = [ prompts ];
966
-
967
- if (typeof title === 'function') {
968
- cb = title;
969
- title = instructions = undefined;
970
- } else if (typeof instructions === 'function') {
971
- cb = instructions;
972
- instructions = undefined;
973
1273
  }
974
1274
 
975
- for (var i = 0; i < prompts.length; ++i) {
976
- if (typeof prompts[i] === 'string') {
977
- prompts[i] = {
978
- prompt: prompts[i],
979
- echo: true
980
- };
1275
+ end() {
1276
+ if (this._sock && isWritable(this._sock)) {
1277
+ this._protocol.disconnect(DISCONNECT_REASON.BY_APPLICATION);
1278
+ this._sock.end();
981
1279
  }
1280
+ return this;
982
1281
  }
983
1282
 
984
- this._cb = cb;
985
- this._initialResponse = true;
986
- this._stream.once('USERAUTH_INFO_RESPONSE', this._onInfoResponse);
987
-
988
- return this._stream.authInfoReq(title, instructions, prompts);
989
- };
990
-
991
- function PKAuthContext(stream, username, service, method, pkInfo, cb) {
992
- AuthContext.call(this, stream, username, service, method, cb);
993
-
994
- this.key = { algo: pkInfo.keyAlgo, data: pkInfo.key };
995
- this.signature = pkInfo.signature;
996
- var sigAlgo;
997
- if (this.signature) {
998
- // TODO: move key type checking logic to ssh2-streams
999
- switch (pkInfo.keyAlgo) {
1000
- case 'ssh-rsa':
1001
- case 'ssh-dss':
1002
- sigAlgo = 'sha1';
1003
- break;
1004
- case 'ssh-ed25519':
1005
- sigAlgo = null;
1006
- break;
1007
- case 'ecdsa-sha2-nistp256':
1008
- sigAlgo = 'sha256';
1009
- break;
1010
- case 'ecdsa-sha2-nistp384':
1011
- sigAlgo = 'sha384';
1012
- break;
1013
- case 'ecdsa-sha2-nistp521':
1014
- sigAlgo = 'sha512';
1015
- break;
1016
- }
1283
+ x11(originAddr, originPort, cb) {
1284
+ const opts = { originAddr, originPort };
1285
+ openChannel(this, 'x11', opts, cb);
1286
+ return this;
1017
1287
  }
1018
- this.sigAlgo = sigAlgo;
1019
- this.blob = pkInfo.blob;
1020
- }
1021
- inherits(PKAuthContext, AuthContext);
1022
- PKAuthContext.prototype.accept = function() {
1023
- if (!this.signature) {
1024
- this._initialResponse = true;
1025
- this._stream.authPKOK(this.key.algo, this.key.data);
1026
- } else {
1027
- AuthContext.prototype.accept.call(this);
1288
+
1289
+ forwardOut(boundAddr, boundPort, remoteAddr, remotePort, cb) {
1290
+ const opts = { boundAddr, boundPort, remoteAddr, remotePort };
1291
+ openChannel(this, 'forwarded-tcpip', opts, cb);
1292
+ return this;
1028
1293
  }
1029
- };
1030
-
1031
- function HostbasedAuthContext(stream, username, service, method, pkInfo, cb) {
1032
- AuthContext.call(this, stream, username, service, method, cb);
1033
-
1034
- this.key = { algo: pkInfo.keyAlgo, data: pkInfo.key };
1035
- this.signature = pkInfo.signature;
1036
- var sigAlgo;
1037
- if (this.signature) {
1038
- // TODO: move key type checking logic to ssh2-streams
1039
- switch (pkInfo.keyAlgo) {
1040
- case 'ssh-rsa':
1041
- case 'ssh-dss':
1042
- sigAlgo = 'sha1';
1043
- break;
1044
- case 'ssh-ed25519':
1045
- sigAlgo = null;
1046
- break;
1047
- case 'ecdsa-sha2-nistp256':
1048
- sigAlgo = 'sha256';
1049
- break;
1050
- case 'ecdsa-sha2-nistp384':
1051
- sigAlgo = 'sha384';
1052
- break;
1053
- case 'ecdsa-sha2-nistp521':
1054
- sigAlgo = 'sha512';
1055
- break;
1056
- }
1294
+
1295
+ openssh_forwardOutStreamLocal(socketPath, cb) {
1296
+ const opts = { socketPath };
1297
+ openChannel(this, 'forwarded-streamlocal@openssh.com', opts, cb);
1298
+ return this;
1057
1299
  }
1058
- this.sigAlgo = sigAlgo;
1059
- this.blob = pkInfo.blob;
1060
- this.localHostname = pkInfo.localHostname;
1061
- this.localUsername = pkInfo.localUsername;
1062
- }
1063
- inherits(HostbasedAuthContext, AuthContext);
1064
1300
 
1065
- function PwdAuthContext(stream, username, service, method, password, cb) {
1066
- AuthContext.call(this, stream, username, service, method, cb);
1301
+ rekey(cb) {
1302
+ let error;
1303
+
1304
+ try {
1305
+ this._protocol.rekey();
1306
+ } catch (ex) {
1307
+ error = ex;
1308
+ }
1067
1309
 
1068
- this.password = password;
1310
+ // TODO: re-throw error if no callback?
1311
+
1312
+ if (typeof cb === 'function') {
1313
+ if (error)
1314
+ process.nextTick(cb, error);
1315
+ else
1316
+ this.once('rekey', cb);
1317
+ }
1318
+ }
1069
1319
  }
1070
- inherits(PwdAuthContext, AuthContext);
1071
1320
 
1072
1321
 
1073
1322
  function openChannel(self, type, opts, cb) {
1074
- // ask the client to open a channel for some purpose
1075
- // (e.g. a forwarded TCP connection)
1076
- var localChan = nextChannel(self);
1077
- var initWindow = Channel.MAX_WINDOW;
1078
- var maxPacket = Channel.PACKET_SIZE;
1079
- var ret = true;
1080
-
1081
- if (localChan === false)
1082
- return cb(new Error('No free channels available'));
1323
+ // Ask the client to open a channel for some purpose (e.g. a forwarded TCP
1324
+ // connection)
1325
+ const initWindow = MAX_WINDOW;
1326
+ const maxPacket = PACKET_SIZE;
1083
1327
 
1084
1328
  if (typeof opts === 'function') {
1085
1329
  cb = opts;
1086
1330
  opts = {};
1087
1331
  }
1088
1332
 
1089
- self._channels[localChan] = true;
1333
+ const wrapper = (err, stream) => {
1334
+ cb(err, stream);
1335
+ };
1336
+ wrapper.type = type;
1090
1337
 
1091
- var sshstream = self._sshstream;
1092
- sshstream.once('CHANNEL_OPEN_CONFIRMATION:' + localChan, function(info) {
1093
- sshstream.removeAllListeners('CHANNEL_OPEN_FAILURE:' + localChan);
1338
+ const localChan = self._chanMgr.add(wrapper);
1094
1339
 
1095
- var chaninfo = {
1096
- type: type,
1097
- incoming: {
1098
- id: localChan,
1099
- window: initWindow,
1100
- packetSize: maxPacket,
1101
- state: 'open'
1102
- },
1103
- outgoing: {
1104
- id: info.sender,
1105
- window: info.window,
1106
- packetSize: info.packetSize,
1107
- state: 'open'
1108
- }
1109
- };
1110
- cb(undefined, new Channel(chaninfo, self, { server: true }));
1111
- }).once('CHANNEL_OPEN_FAILURE:' + localChan, function(info) {
1112
- sshstream.removeAllListeners('CHANNEL_OPEN_CONFIRMATION:' + localChan);
1113
-
1114
- delete self._channels[localChan];
1115
-
1116
- var err = new Error('(SSH) Channel open failure: ' + info.description);
1117
- err.reason = info.reason;
1118
- err.lang = info.lang;
1119
- cb(err);
1120
- });
1121
-
1122
- if (type === 'forwarded-tcpip')
1123
- ret = sshstream.forwardedTcpip(localChan, initWindow, maxPacket, opts);
1124
- else if (type === 'x11')
1125
- ret = sshstream.x11(localChan, initWindow, maxPacket, opts);
1126
- else if (type === 'forwarded-streamlocal@openssh.com') {
1127
- ret = sshstream.openssh_forwardedStreamLocal(localChan,
1128
- initWindow,
1129
- maxPacket,
1130
- opts);
1340
+ if (localChan === -1) {
1341
+ cb(new Error('No free channels available'));
1342
+ return;
1131
1343
  }
1132
1344
 
1133
- return ret;
1345
+ switch (type) {
1346
+ case 'forwarded-tcpip':
1347
+ self._protocol.forwardedTcpip(localChan, initWindow, maxPacket, opts);
1348
+ break;
1349
+ case 'x11':
1350
+ self._protocol.x11(localChan, initWindow, maxPacket, opts);
1351
+ break;
1352
+ case 'forwarded-streamlocal@openssh.com':
1353
+ self._protocol.openssh_forwardedStreamLocal(
1354
+ localChan, initWindow, maxPacket, opts
1355
+ );
1356
+ break;
1357
+ default:
1358
+ throw new Error(`Unsupported channel type: ${type}`);
1359
+ }
1134
1360
  }
1135
1361
 
1136
- function nextChannel(self) {
1137
- // get the next available channel number
1138
-
1139
- // fast path
1140
- if (self._curChan < MAX_CHANNEL)
1141
- return ++self._curChan;
1142
-
1143
- // slower lookup path
1144
- for (var i = 0, channels = self._channels; i < MAX_CHANNEL; ++i)
1145
- if (!channels[i])
1146
- return i;
1147
-
1148
- return false;
1362
+ function compareNumbers(a, b) {
1363
+ return a - b;
1149
1364
  }
1150
1365
 
1151
-
1152
- Server.createServer = function(cfg, listener) {
1153
- return new Server(cfg, listener);
1154
- };
1155
- Server.KEEPALIVE_INTERVAL = 1000;
1156
- Server.KEEPALIVE_CLIENT_INTERVAL = 15000;
1157
- Server.KEEPALIVE_CLIENT_COUNT_MAX = 3;
1158
-
1159
1366
  module.exports = Server;
1160
1367
  module.exports.IncomingClient = Client;