@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/client.js CHANGED
@@ -1,1240 +1,1565 @@
1
- var crypto = require('crypto');
2
- var Socket = require('net').Socket;
3
- var dnsLookup = require('dns').lookup;
4
- var EventEmitter = require('events').EventEmitter;
5
- var inherits = require('util').inherits;
6
- var HASHES = crypto.getHashes();
7
-
8
- var ssh2_streams = require('ssh2-streams');
9
- var SSH2Stream = ssh2_streams.SSH2Stream;
10
- var SFTPStream = ssh2_streams.SFTPStream;
11
- var consts = ssh2_streams.constants;
12
- var BUGS = consts.BUGS;
13
- var ALGORITHMS = consts.ALGORITHMS;
14
- var EDDSA_SUPPORTED = consts.EDDSA_SUPPORTED;
15
- var parseKey = ssh2_streams.utils.parseKey;
16
-
17
- var HTTPAgents = require('./http-agents');
18
- var Channel = require('./Channel');
19
- var agentQuery = require('./agent');
20
- var SFTPWrapper = require('./SFTPWrapper');
21
- var readUInt32BE = require('./buffer-helpers').readUInt32BE;
22
-
23
- var MAX_CHANNEL = Math.pow(2, 32) - 1;
24
- var RE_OPENSSH = /^OpenSSH_(?:(?![0-4])\d)|(?:\d{2,})/;
25
- var DEBUG_NOOP = function(msg) {};
26
-
27
- function Client() {
28
- if (!(this instanceof Client))
29
- return new Client();
30
-
31
- EventEmitter.call(this);
32
-
33
- this.config = {
34
- host: undefined,
35
- port: undefined,
36
- localAddress: undefined,
37
- localPort: undefined,
38
- forceIPv4: undefined,
39
- forceIPv6: undefined,
40
- keepaliveCountMax: undefined,
41
- keepaliveInterval: undefined,
42
- readyTimeout: undefined,
43
-
44
- username: undefined,
45
- password: undefined,
46
- privateKey: undefined,
47
- tryKeyboard: undefined,
48
- agent: undefined,
49
- allowAgentFwd: undefined,
50
- authHandler: undefined,
51
-
52
- hostHashAlgo: undefined,
53
- hostHashCb: undefined,
54
- strictVendor: undefined,
55
- debug: undefined
56
- };
57
-
58
- this._readyTimeout = undefined;
59
- this._channels = undefined;
60
- this._callbacks = undefined;
61
- this._forwarding = undefined;
62
- this._forwardingUnix = undefined;
63
- this._acceptX11 = undefined;
64
- this._agentFwdEnabled = undefined;
65
- this._curChan = undefined;
66
- this._remoteVer = undefined;
67
-
68
- this._sshstream = undefined;
69
- this._sock = undefined;
70
- this._resetKA = undefined;
71
- }
72
- inherits(Client, EventEmitter);
73
-
74
- Client.prototype.connect = function(cfg) {
75
- var self = this;
1
+ // TODO:
2
+ // * add `.connected` or similar property to allow immediate connection
3
+ // status checking
4
+ // * add/improve debug output during user authentication phase
5
+ 'use strict';
6
+
7
+ const {
8
+ createHash,
9
+ getHashes,
10
+ randomFillSync,
11
+ } = require('crypto');
12
+ const { Socket } = require('net');
13
+ const { lookup: dnsLookup } = require('dns');
14
+ const EventEmitter = require('events');
15
+ const HASHES = getHashes();
16
+
17
+ const {
18
+ COMPAT,
19
+ CHANNEL_EXTENDED_DATATYPE: { STDERR },
20
+ CHANNEL_OPEN_FAILURE,
21
+ DEFAULT_CIPHER,
22
+ DEFAULT_COMPRESSION,
23
+ DEFAULT_KEX,
24
+ DEFAULT_MAC,
25
+ DEFAULT_SERVER_HOST_KEY,
26
+ DISCONNECT_REASON,
27
+ DISCONNECT_REASON_BY_VALUE,
28
+ SUPPORTED_CIPHER,
29
+ SUPPORTED_COMPRESSION,
30
+ SUPPORTED_KEX,
31
+ SUPPORTED_MAC,
32
+ SUPPORTED_SERVER_HOST_KEY,
33
+ } = require('./protocol/constants.js');
34
+ const { init: cryptoInit } = require('./protocol/crypto.js');
35
+ const Protocol = require('./protocol/Protocol.js');
36
+ const { parseKey } = require('./protocol/keyParser.js');
37
+ const { SFTP } = require('./protocol/SFTP.js');
38
+ const {
39
+ bufferCopy,
40
+ makeBufferParser,
41
+ makeError,
42
+ readUInt32BE,
43
+ sigSSHToASN1,
44
+ writeUInt32BE,
45
+ } = require('./protocol/utils.js');
46
+
47
+ const { AgentContext, createAgent, isAgent } = require('./agent.js');
48
+ const {
49
+ Channel,
50
+ MAX_WINDOW,
51
+ PACKET_SIZE,
52
+ windowAdjust,
53
+ WINDOW_THRESHOLD,
54
+ } = require('./Channel.js');
55
+ const {
56
+ ChannelManager,
57
+ generateAlgorithmList,
58
+ isWritable,
59
+ onChannelOpenFailure,
60
+ onCHANNEL_CLOSE,
61
+ } = require('./utils.js');
62
+
63
+ const bufferParser = makeBufferParser();
64
+ const sigParser = makeBufferParser();
65
+ const RE_OPENSSH = /^OpenSSH_(?:(?![0-4])\d)|(?:\d{2,})/;
66
+ const noop = (err) => {};
67
+
68
+ class Client extends EventEmitter {
69
+ constructor() {
70
+ super();
71
+
72
+ this.config = {
73
+ host: undefined,
74
+ port: undefined,
75
+ localAddress: undefined,
76
+ localPort: undefined,
77
+ forceIPv4: undefined,
78
+ forceIPv6: undefined,
79
+ keepaliveCountMax: undefined,
80
+ keepaliveInterval: undefined,
81
+ readyTimeout: undefined,
82
+ ident: undefined,
83
+
84
+ username: undefined,
85
+ password: undefined,
86
+ privateKey: undefined,
87
+ tryKeyboard: undefined,
88
+ agent: undefined,
89
+ allowAgentFwd: undefined,
90
+ authHandler: undefined,
91
+
92
+ hostHashAlgo: undefined,
93
+ hostHashCb: undefined,
94
+ strictVendor: undefined,
95
+ debug: undefined
96
+ };
76
97
 
77
- if (this._sock && this._sock.writable) {
78
- this.once('close', function() {
79
- self.connect(cfg);
80
- });
81
- this.end();
82
- return;
98
+ this._agent = undefined;
99
+ this._readyTimeout = undefined;
100
+ this._chanMgr = undefined;
101
+ this._callbacks = undefined;
102
+ this._forwarding = undefined;
103
+ this._forwardingUnix = undefined;
104
+ this._acceptX11 = undefined;
105
+ this._agentFwdEnabled = undefined;
106
+ this._remoteVer = undefined;
107
+
108
+ this._protocol = undefined;
109
+ this._sock = undefined;
110
+ this._resetKA = undefined;
83
111
  }
84
112
 
85
- this.config.host = cfg.hostname || cfg.host || 'localhost';
86
- this.config.port = cfg.port || 22;
87
- this.config.localAddress = (typeof cfg.localAddress === 'string'
88
- ? cfg.localAddress
89
- : undefined);
90
- this.config.localPort = (typeof cfg.localPort === 'string'
91
- || typeof cfg.localPort === 'number'
92
- ? cfg.localPort
93
- : undefined);
94
- this.config.forceIPv4 = cfg.forceIPv4 || false;
95
- this.config.forceIPv6 = cfg.forceIPv6 || false;
96
- this.config.keepaliveCountMax = (typeof cfg.keepaliveCountMax === 'number'
97
- && cfg.keepaliveCountMax >= 0
98
- ? cfg.keepaliveCountMax
99
- : 3);
100
- this.config.keepaliveInterval = (typeof cfg.keepaliveInterval === 'number'
101
- && cfg.keepaliveInterval > 0
102
- ? cfg.keepaliveInterval
103
- : 0);
104
- this.config.readyTimeout = (typeof cfg.readyTimeout === 'number'
105
- && cfg.readyTimeout >= 0
106
- ? cfg.readyTimeout
107
- : 20000);
108
-
109
- var algorithms = {
110
- kex: undefined,
111
- kexBuf: undefined,
112
- cipher: undefined,
113
- cipherBuf: undefined,
114
- serverHostKey: undefined,
115
- serverHostKeyBuf: undefined,
116
- hmac: undefined,
117
- hmacBuf: undefined,
118
- compress: undefined,
119
- compressBuf: undefined
120
- };
121
- var i;
122
- if (typeof cfg.algorithms === 'object' && cfg.algorithms !== null) {
123
- var algosSupported;
124
- var algoList;
125
-
126
- algoList = cfg.algorithms.kex;
127
- if (Array.isArray(algoList) && algoList.length > 0) {
128
- algosSupported = ALGORITHMS.SUPPORTED_KEX;
129
- for (i = 0; i < algoList.length; ++i) {
130
- if (algosSupported.indexOf(algoList[i]) === -1)
131
- throw new Error('Unsupported key exchange algorithm: ' + algoList[i]);
132
- }
133
- algorithms.kex = algoList;
113
+ connect(cfg) {
114
+ if (this._sock && isWritable(this._sock)) {
115
+ this.once('close', () => {
116
+ this.connect(cfg);
117
+ });
118
+ this.end();
119
+ return this;
134
120
  }
135
121
 
136
- algoList = cfg.algorithms.cipher;
137
- if (Array.isArray(algoList) && algoList.length > 0) {
138
- algosSupported = ALGORITHMS.SUPPORTED_CIPHER;
139
- for (i = 0; i < algoList.length; ++i) {
140
- if (algosSupported.indexOf(algoList[i]) === -1)
141
- throw new Error('Unsupported cipher algorithm: ' + algoList[i]);
142
- }
143
- algorithms.cipher = algoList;
122
+ this.config.host = cfg.hostname || cfg.host || 'localhost';
123
+ this.config.port = cfg.port || 22;
124
+ this.config.localAddress = (typeof cfg.localAddress === 'string'
125
+ ? cfg.localAddress
126
+ : undefined);
127
+ this.config.localPort = (typeof cfg.localPort === 'string'
128
+ || typeof cfg.localPort === 'number'
129
+ ? cfg.localPort
130
+ : undefined);
131
+ this.config.forceIPv4 = cfg.forceIPv4 || false;
132
+ this.config.forceIPv6 = cfg.forceIPv6 || false;
133
+ this.config.keepaliveCountMax = (typeof cfg.keepaliveCountMax === 'number'
134
+ && cfg.keepaliveCountMax >= 0
135
+ ? cfg.keepaliveCountMax
136
+ : 3);
137
+ this.config.keepaliveInterval = (typeof cfg.keepaliveInterval === 'number'
138
+ && cfg.keepaliveInterval > 0
139
+ ? cfg.keepaliveInterval
140
+ : 0);
141
+ this.config.readyTimeout = (typeof cfg.readyTimeout === 'number'
142
+ && cfg.readyTimeout >= 0
143
+ ? cfg.readyTimeout
144
+ : 20000);
145
+ this.config.ident = (typeof cfg.ident === 'string'
146
+ || Buffer.isBuffer(cfg.ident)
147
+ ? cfg.ident
148
+ : undefined);
149
+
150
+ const algorithms = {
151
+ kex: undefined,
152
+ serverHostKey: undefined,
153
+ cs: {
154
+ cipher: undefined,
155
+ mac: undefined,
156
+ compress: undefined,
157
+ lang: [],
158
+ },
159
+ sc: undefined,
160
+ };
161
+ let allOfferDefaults = true;
162
+ if (typeof cfg.algorithms === 'object' && cfg.algorithms !== null) {
163
+ algorithms.kex = generateAlgorithmList(cfg.algorithms.kex,
164
+ DEFAULT_KEX,
165
+ SUPPORTED_KEX);
166
+ if (algorithms.kex !== DEFAULT_KEX)
167
+ allOfferDefaults = false;
168
+
169
+ algorithms.serverHostKey =
170
+ generateAlgorithmList(cfg.algorithms.serverHostKey,
171
+ DEFAULT_SERVER_HOST_KEY,
172
+ SUPPORTED_SERVER_HOST_KEY);
173
+ if (algorithms.serverHostKey !== DEFAULT_SERVER_HOST_KEY)
174
+ allOfferDefaults = false;
175
+
176
+ algorithms.cs.cipher = generateAlgorithmList(cfg.algorithms.cipher,
177
+ DEFAULT_CIPHER,
178
+ SUPPORTED_CIPHER);
179
+ if (algorithms.cs.cipher !== DEFAULT_CIPHER)
180
+ allOfferDefaults = false;
181
+
182
+ algorithms.cs.mac = generateAlgorithmList(cfg.algorithms.hmac,
183
+ DEFAULT_MAC,
184
+ SUPPORTED_MAC);
185
+ if (algorithms.cs.mac !== DEFAULT_MAC)
186
+ allOfferDefaults = false;
187
+
188
+ algorithms.cs.compress = generateAlgorithmList(cfg.algorithms.compress,
189
+ DEFAULT_COMPRESSION,
190
+ SUPPORTED_COMPRESSION);
191
+ if (algorithms.cs.compress !== DEFAULT_COMPRESSION)
192
+ allOfferDefaults = false;
193
+
194
+ if (!allOfferDefaults)
195
+ algorithms.sc = algorithms.cs;
144
196
  }
145
197
 
146
- algoList = cfg.algorithms.serverHostKey;
147
- if (Array.isArray(algoList) && algoList.length > 0) {
148
- algosSupported = ALGORITHMS.SUPPORTED_SERVER_HOST_KEY;
149
- for (i = 0; i < algoList.length; ++i) {
150
- if (algosSupported.indexOf(algoList[i]) === -1) {
151
- throw new Error('Unsupported server host key algorithm: '
152
- + algoList[i]);
153
- }
154
- }
155
- algorithms.serverHostKey = algoList;
156
- }
198
+ if (typeof cfg.username === 'string')
199
+ this.config.username = cfg.username;
200
+ else if (typeof cfg.user === 'string')
201
+ this.config.username = cfg.user;
202
+ else
203
+ throw new Error('Invalid username');
157
204
 
158
- algoList = cfg.algorithms.hmac;
159
- if (Array.isArray(algoList) && algoList.length > 0) {
160
- algosSupported = ALGORITHMS.SUPPORTED_HMAC;
161
- for (i = 0; i < algoList.length; ++i) {
162
- if (algosSupported.indexOf(algoList[i]) === -1)
163
- throw new Error('Unsupported HMAC algorithm: ' + algoList[i]);
164
- }
165
- algorithms.hmac = algoList;
166
- }
205
+ this.config.password = (typeof cfg.password === 'string'
206
+ ? cfg.password
207
+ : undefined);
208
+ this.config.privateKey = (typeof cfg.privateKey === 'string'
209
+ || Buffer.isBuffer(cfg.privateKey)
210
+ ? cfg.privateKey
211
+ : undefined);
212
+ this.config.localHostname = (typeof cfg.localHostname === 'string'
213
+ ? cfg.localHostname
214
+ : undefined);
215
+ this.config.localUsername = (typeof cfg.localUsername === 'string'
216
+ ? cfg.localUsername
217
+ : undefined);
218
+ this.config.tryKeyboard = (cfg.tryKeyboard === true);
219
+ if (typeof cfg.agent === 'string' && cfg.agent.length)
220
+ this.config.agent = createAgent(cfg.agent);
221
+ else if (isAgent(cfg.agent))
222
+ this.config.agent = cfg.agent;
223
+ else
224
+ this.config.agent = undefined;
225
+ this.config.allowAgentFwd = (cfg.agentForward === true
226
+ && this.config.agent !== undefined);
227
+ let authHandler = this.config.authHandler = (
228
+ typeof cfg.authHandler === 'function'
229
+ || Array.isArray(cfg.authHandler)
230
+ ? cfg.authHandler
231
+ : undefined
232
+ );
167
233
 
168
- algoList = cfg.algorithms.compress;
169
- if (Array.isArray(algoList) && algoList.length > 0) {
170
- algosSupported = ALGORITHMS.SUPPORTED_COMPRESS;
171
- for (i = 0; i < algoList.length; ++i) {
172
- if (algosSupported.indexOf(algoList[i]) === -1)
173
- throw new Error('Unsupported compression algorithm: ' + algoList[i]);
174
- }
175
- algorithms.compress = algoList;
176
- }
177
- }
178
- if (algorithms.compress === undefined) {
179
- if (cfg.compress) {
180
- algorithms.compress = ['zlib@openssh.com', 'zlib'];
181
- if (cfg.compress !== 'force')
182
- algorithms.compress.push('none');
183
- } else if (cfg.compress === false)
184
- algorithms.compress = ['none'];
185
- }
234
+ this.config.strictVendor = (typeof cfg.strictVendor === 'boolean'
235
+ ? cfg.strictVendor
236
+ : true);
186
237
 
187
- if (typeof cfg.username === 'string')
188
- this.config.username = cfg.username;
189
- else if (typeof cfg.user === 'string')
190
- this.config.username = cfg.user;
191
- else
192
- throw new Error('Invalid username');
193
-
194
- this.config.password = (typeof cfg.password === 'string'
195
- ? cfg.password
196
- : undefined);
197
- this.config.privateKey = (typeof cfg.privateKey === 'string'
198
- || Buffer.isBuffer(cfg.privateKey)
199
- ? cfg.privateKey
200
- : undefined);
201
- this.config.localHostname = (typeof cfg.localHostname === 'string'
202
- && cfg.localHostname.length
203
- ? cfg.localHostname
204
- : undefined);
205
- this.config.localUsername = (typeof cfg.localUsername === 'string'
206
- && cfg.localUsername.length
207
- ? cfg.localUsername
208
- : undefined);
209
- this.config.tryKeyboard = (cfg.tryKeyboard === true);
210
- this.config.agent = (typeof cfg.agent === 'string' && cfg.agent.length
211
- ? cfg.agent
212
- : undefined);
213
- this.config.allowAgentFwd = (cfg.agentForward === true
214
- && this.config.agent !== undefined);
215
- var authHandler = this.config.authHandler = (
216
- typeof cfg.authHandler === 'function' ? cfg.authHandler : undefined
217
- );
238
+ const debug = this.config.debug = (typeof cfg.debug === 'function'
239
+ ? cfg.debug
240
+ : undefined);
218
241
 
219
- this.config.strictVendor = (typeof cfg.strictVendor === 'boolean'
220
- ? cfg.strictVendor
221
- : true);
222
-
223
- var debug = this.config.debug = (typeof cfg.debug === 'function'
224
- ? cfg.debug
225
- : DEBUG_NOOP);
226
-
227
- if (cfg.agentForward === true && !this.config.allowAgentFwd)
228
- throw new Error('You must set a valid agent path to allow agent forwarding');
229
-
230
- var callbacks = this._callbacks = [];
231
- this._channels = {};
232
- this._forwarding = {};
233
- this._forwardingUnix = {};
234
- this._acceptX11 = 0;
235
- this._agentFwdEnabled = false;
236
- this._curChan = -1;
237
- this._remoteVer = undefined;
238
- var privateKey;
239
-
240
- if (this.config.privateKey) {
241
- privateKey = parseKey(this.config.privateKey, cfg.passphrase);
242
- if (privateKey instanceof Error)
243
- throw new Error('Cannot parse privateKey: ' + privateKey.message);
244
- if (Array.isArray(privateKey))
245
- privateKey = privateKey[0]; // OpenSSH's newer format only stores 1 key for now
246
- if (privateKey.getPrivatePEM() === null)
247
- throw new Error('privateKey value does not contain a (valid) private key');
248
- }
242
+ if (cfg.agentForward === true && !this.config.allowAgentFwd) {
243
+ throw new Error(
244
+ 'You must set a valid agent path to allow agent forwarding'
245
+ );
246
+ }
249
247
 
250
- var stream = this._sshstream = new SSH2Stream({
251
- algorithms: algorithms,
252
- debug: (debug === DEBUG_NOOP ? undefined : debug)
253
- });
254
- var sock = this._sock = (cfg.sock || new Socket());
255
-
256
- // drain stderr if we are connection hopping using an exec stream
257
- if (this._sock.stderr && typeof this._sock.stderr.resume === 'function')
258
- this._sock.stderr.resume();
259
-
260
- // keepalive-related
261
- var kainterval = this.config.keepaliveInterval;
262
- var kacountmax = this.config.keepaliveCountMax;
263
- var kacount = 0;
264
- var katimer;
265
- function sendKA() {
266
- if (++kacount > kacountmax) {
267
- clearInterval(katimer);
268
- if (sock.readable) {
269
- var err = new Error('Keepalive timeout');
270
- err.level = 'client-timeout';
271
- self.emit('error', err);
272
- sock.destroy();
248
+ let callbacks = this._callbacks = [];
249
+ this._chanMgr = new ChannelManager(this);
250
+ this._forwarding = {};
251
+ this._forwardingUnix = {};
252
+ this._acceptX11 = 0;
253
+ this._agentFwdEnabled = false;
254
+ this._agent = (this.config.agent ? this.config.agent : undefined);
255
+ this._remoteVer = undefined;
256
+ let privateKey;
257
+
258
+ if (this.config.privateKey) {
259
+ privateKey = parseKey(this.config.privateKey, cfg.passphrase);
260
+ if (privateKey instanceof Error)
261
+ throw new Error(`Cannot parse privateKey: ${privateKey.message}`);
262
+ if (Array.isArray(privateKey)) {
263
+ // OpenSSH's newer format only stores 1 key for now
264
+ privateKey = privateKey[0];
265
+ }
266
+ if (privateKey.getPrivatePEM() === null) {
267
+ throw new Error(
268
+ 'privateKey value does not contain a (valid) private key'
269
+ );
273
270
  }
274
- return;
275
- }
276
- if (sock.writable) {
277
- // append dummy callback to keep correct callback order
278
- callbacks.push(resetKA);
279
- stream.ping();
280
- } else
281
- clearInterval(katimer);
282
- }
283
- function resetKA() {
284
- if (kainterval > 0) {
285
- kacount = 0;
286
- clearInterval(katimer);
287
- if (sock.writable)
288
- katimer = setInterval(sendKA, kainterval);
289
271
  }
290
- }
291
- this._resetKA = resetKA;
292
-
293
- stream.on('USERAUTH_BANNER', function(msg) {
294
- self.emit('banner', msg);
295
- });
296
272
 
297
- sock.on('connect', function() {
298
- debug('DEBUG: Client: Connected');
299
- self.emit('connect');
300
- if (!cfg.sock)
301
- stream.pipe(sock).pipe(stream);
302
- }).on('timeout', function() {
303
- self.emit('timeout');
304
- }).on('error', function(err) {
305
- clearTimeout(self._readyTimeout);
306
- err.level = 'client-socket';
307
- self.emit('error', err);
308
- }).on('end', function() {
309
- stream.unpipe(sock);
310
- clearTimeout(self._readyTimeout);
311
- clearInterval(katimer);
312
- self.emit('end');
313
- }).on('close', function() {
314
- stream.unpipe(sock);
315
- clearTimeout(self._readyTimeout);
316
- clearInterval(katimer);
317
- self.emit('close');
318
-
319
- // notify outstanding channel requests of disconnection ...
320
- var callbacks_ = callbacks;
321
- var err = new Error('No response from server');
322
- callbacks = self._callbacks = [];
323
- for (i = 0; i < callbacks_.length; ++i)
324
- callbacks_[i](err);
325
-
326
- // simulate error for any channels waiting to be opened. this is safe
327
- // against successfully opened channels because the success and failure
328
- // event handlers are automatically removed when a success/failure response
329
- // is received
330
- var channels = self._channels;
331
- var chanNos = Object.keys(channels);
332
- self._channels = {};
333
- for (i = 0; i < chanNos.length; ++i) {
334
- var ev1 = stream.emit('CHANNEL_OPEN_FAILURE:' + chanNos[i], err);
335
- // emitting CHANNEL_CLOSE should be safe too and should help for any
336
- // special channels which might otherwise keep the process alive, such
337
- // as agent forwarding channels which have open unix sockets ...
338
- var ev2 = stream.emit('CHANNEL_CLOSE:' + chanNos[i]);
339
- var earlyCb;
340
- if (!ev1 && !ev2 && (earlyCb = channels[chanNos[i]])
341
- && typeof earlyCb === 'function') {
342
- earlyCb(err);
273
+ let hostVerifier;
274
+ if (typeof cfg.hostVerifier === 'function') {
275
+ const hashCb = cfg.hostVerifier;
276
+ let hasher;
277
+ if (HASHES.indexOf(cfg.hostHash) !== -1) {
278
+ // Default to old behavior of hashing on user's behalf
279
+ hasher = createHash(cfg.hostHash);
343
280
  }
281
+ hostVerifier = (key, verify) => {
282
+ if (hasher) {
283
+ hasher.update(key);
284
+ key = hasher.digest('hex');
285
+ }
286
+ const ret = hashCb(key, verify);
287
+ if (ret !== undefined)
288
+ verify(ret);
289
+ };
344
290
  }
345
- });
346
- stream.on('drain', function() {
347
- self.emit('drain');
348
- }).once('header', function(header) {
349
- self._remoteVer = header.versions.software;
350
- if (header.greeting)
351
- self.emit('greeting', header.greeting);
352
- }).on('continue', function() {
353
- self.emit('continue');
354
- }).on('error', function(err) {
355
- if (err.level === undefined)
356
- err.level = 'protocol';
357
- else if (err.level === 'handshake')
358
- clearTimeout(self._readyTimeout);
359
- self.emit('error', err);
360
- }).on('end', function() {
361
- sock.resume();
362
- });
363
291
 
364
- if (typeof cfg.hostVerifier === 'function') {
365
- if (HASHES.indexOf(cfg.hostHash) === -1)
366
- throw new Error('Invalid host hash algorithm: ' + cfg.hostHash);
367
- var hashCb = cfg.hostVerifier;
368
- var hasher = crypto.createHash(cfg.hostHash);
369
- stream.once('fingerprint', function(key, verify) {
370
- hasher.update(key);
371
- var ret = hashCb(hasher.digest('hex'), verify);
372
- if (ret !== undefined)
373
- verify(ret);
292
+ const sock = this._sock = (cfg.sock || new Socket());
293
+ let ready = false;
294
+ let sawHeader = false;
295
+ if (this._protocol)
296
+ this._protocol.cleanup();
297
+ const DEBUG_HANDLER = (!debug ? undefined : (p, display, msg) => {
298
+ debug(`Debug output from server: ${JSON.stringify(msg)}`);
374
299
  });
375
- }
376
-
377
- // begin authentication handling =============================================
378
- var curAuth;
379
- var curPartial = null;
380
- var curAuthsLeft = null;
381
- var agentKeys;
382
- var agentKeyPos = 0;
383
- var authsAllowed = ['none'];
384
- if (this.config.password !== undefined)
385
- authsAllowed.push('password');
386
- if (privateKey !== undefined)
387
- authsAllowed.push('publickey');
388
- if (this.config.agent !== undefined)
389
- authsAllowed.push('agent');
390
- if (this.config.tryKeyboard)
391
- authsAllowed.push('keyboard-interactive');
392
- if (this.config.tryKeyboard && this.config.password) {
393
- authsAllowed.push('password');
394
- }
395
- if (privateKey !== undefined
396
- && this.config.localHostname !== undefined
397
- && this.config.localUsername !== undefined) {
398
- authsAllowed.push('hostbased');
399
- }
400
-
401
- if (authHandler === undefined) {
402
- var authPos = 0;
403
- authHandler = function authHandler(authsLeft, partial, cb) {
404
- if (authPos === authsAllowed.length)
405
- return false;
406
- return authsAllowed[authPos++];
407
- };
408
- }
409
-
410
- var hasSentAuth = false;
411
- function doNextAuth(authName) {
412
- hasSentAuth = true;
413
- if (authName === false) {
414
- stream.removeListener('USERAUTH_FAILURE', onUSERAUTH_FAILURE);
415
- stream.removeListener('USERAUTH_PK_OK', onUSERAUTH_PK_OK);
416
- var err = new Error('All configured authentication methods failed');
417
- err.level = 'client-authentication';
418
- self.emit('error', err);
419
- if (stream.writable)
420
- self.end();
421
- return;
422
- }
423
- if (authsAllowed.indexOf(authName) === -1)
424
- throw new Error('Authentication method not allowed: ' + authName);
425
- curAuth = authName;
426
- switch (curAuth) {
427
- case 'password':
428
- stream.authPassword(self.config.username, self.config.password);
429
- break;
430
- case 'publickey':
431
- stream.authPK(self.config.username, privateKey);
432
- stream.once('USERAUTH_PK_OK', onUSERAUTH_PK_OK);
433
- break;
434
- case 'hostbased':
435
- function hostbasedCb(buf, cb) {
436
- var signature = privateKey.sign(buf);
437
- if (signature instanceof Error) {
438
- signature.message = 'Error while signing data with privateKey: '
439
- + signature.message;
440
- signature.level = 'client-authentication';
441
- self.emit('error', signature);
442
- return tryNextAuth();
443
- }
444
-
445
- cb(signature);
300
+ const proto = this._protocol = new Protocol({
301
+ ident: this.config.ident,
302
+ offer: (allOfferDefaults ? undefined : algorithms),
303
+ onWrite: (data) => {
304
+ if (isWritable(sock))
305
+ sock.write(data);
306
+ },
307
+ onError: (err) => {
308
+ if (err.level === 'handshake')
309
+ clearTimeout(this._readyTimeout);
310
+ if (!proto._destruct)
311
+ sock.removeAllListeners('data');
312
+ this.emit('error', err);
313
+ try {
314
+ sock.end();
315
+ } catch {}
316
+ },
317
+ onHeader: (header) => {
318
+ sawHeader = true;
319
+ this._remoteVer = header.versions.software;
320
+ if (header.greeting)
321
+ this.emit('greeting', header.greeting);
322
+ },
323
+ onHandshakeComplete: (negotiated) => {
324
+ this.emit('handshake', negotiated);
325
+ if (!ready) {
326
+ ready = true;
327
+ proto.service('ssh-userauth');
446
328
  }
447
- stream.authHostbased(self.config.username,
448
- privateKey,
449
- self.config.localHostname,
450
- self.config.localUsername,
451
- hostbasedCb);
452
- break;
453
- case 'agent':
454
- agentQuery(self.config.agent, function(err, keys) {
455
- if (err) {
456
- err.level = 'agent';
457
- self.emit('error', err);
458
- agentKeys = undefined;
459
- return tryNextAuth();
460
- } else if (keys.length === 0) {
461
- debug('DEBUG: Agent: No keys stored in agent');
462
- agentKeys = undefined;
463
- return tryNextAuth();
329
+ },
330
+ debug,
331
+ hostVerifier,
332
+ messageHandlers: {
333
+ DEBUG: DEBUG_HANDLER,
334
+ DISCONNECT: (p, reason, desc) => {
335
+ if (reason !== DISCONNECT_REASON.BY_APPLICATION) {
336
+ if (!desc) {
337
+ desc = DISCONNECT_REASON_BY_VALUE[reason];
338
+ if (desc === undefined)
339
+ desc = `Unexpected disconnection reason: ${reason}`;
340
+ }
341
+ const err = new Error(desc);
342
+ err.code = reason;
343
+ this.emit('error', err);
344
+ }
345
+ sock.end();
346
+ },
347
+ SERVICE_ACCEPT: (p, name) => {
348
+ if (name === 'ssh-userauth')
349
+ tryNextAuth();
350
+ },
351
+ USERAUTH_BANNER: (p, msg) => {
352
+ this.emit('banner', msg);
353
+ },
354
+ USERAUTH_SUCCESS: (p) => {
355
+ // Start keepalive mechanism
356
+ resetKA();
357
+
358
+ clearTimeout(this._readyTimeout);
359
+
360
+ this.emit('ready');
361
+ },
362
+ USERAUTH_FAILURE: (p, authMethods, partialSuccess) => {
363
+ if (curAuth.type === 'agent') {
364
+ const pos = curAuth.agentCtx.pos();
365
+ debug && debug(`Client: Agent key #${pos + 1} failed`);
366
+ return tryNextAgentKey();
464
367
  }
465
368
 
466
- agentKeys = keys;
467
- agentKeyPos = 0;
468
-
469
- stream.authPK(self.config.username, keys[0]);
470
- stream.once('USERAUTH_PK_OK', onUSERAUTH_PK_OK);
471
- });
472
- break;
473
- case 'keyboard-interactive':
474
- stream.authKeyboard(self.config.username);
475
- stream.on('USERAUTH_INFO_REQUEST', onUSERAUTH_INFO_REQUEST);
476
- break;
477
- case 'none':
478
- stream.authNone(self.config.username);
479
- break;
480
- }
481
- }
482
- function tryNextAuth() {
483
- hasSentAuth = false;
484
- var auth = authHandler(curAuthsLeft, curPartial, doNextAuth);
485
- if (hasSentAuth || auth === undefined)
486
- return;
487
- doNextAuth(auth);
488
- }
489
- function tryNextAgentKey() {
490
- if (curAuth === 'agent') {
491
- if (agentKeyPos >= agentKeys.length)
492
- return;
493
- if (++agentKeyPos >= agentKeys.length) {
494
- debug('DEBUG: Agent: No more keys left to try');
495
- debug('DEBUG: Client: agent auth failed');
496
- agentKeys = undefined;
497
- tryNextAuth();
498
- } else {
499
- debug('DEBUG: Agent: Trying key #' + (agentKeyPos + 1));
500
- stream.authPK(self.config.username, agentKeys[agentKeyPos]);
501
- stream.once('USERAUTH_PK_OK', onUSERAUTH_PK_OK);
502
- }
503
- }
504
- }
505
- function onUSERAUTH_INFO_REQUEST(name, instructions, lang, prompts) {
506
- var nprompts = (Array.isArray(prompts) ? prompts.length : 0);
507
- if (nprompts === 0) {
508
- debug('DEBUG: Client: Sending automatic USERAUTH_INFO_RESPONSE');
509
- return stream.authInfoRes();
510
- }
511
- // we sent a keyboard-interactive user authentication request and now the
512
- // server is sending us the prompts we need to present to the user
513
- self.emit('keyboard-interactive',
369
+ debug && debug(`Client: ${curAuth.type} auth failed`);
370
+
371
+ curPartial = partialSuccess;
372
+ curAuthsLeft = authMethods;
373
+ tryNextAuth();
374
+ },
375
+ USERAUTH_PASSWD_CHANGEREQ: (p, prompt) => {
376
+ if (curAuth.type === 'password') {
377
+ // TODO: support a `changePrompt()` on `curAuth` that defaults to
378
+ // emitting 'change password' as before
379
+ this.emit('change password', prompt, (newPassword) => {
380
+ proto.authPassword(
381
+ this.config.username,
382
+ this.config.password,
383
+ newPassword
384
+ );
385
+ });
386
+ }
387
+ },
388
+ USERAUTH_PK_OK: (p) => {
389
+ if (curAuth.type === 'agent') {
390
+ const key = curAuth.agentCtx.currentKey();
391
+ proto.authPK(curAuth.username, key, (buf, cb) => {
392
+ curAuth.agentCtx.sign(key, buf, {}, (err, signed) => {
393
+ if (err) {
394
+ err.level = 'agent';
395
+ this.emit('error', err);
396
+ } else {
397
+ return cb(signed);
398
+ }
399
+
400
+ tryNextAgentKey();
401
+ });
402
+ });
403
+ } else if (curAuth.type === 'publickey') {
404
+ proto.authPK(curAuth.username, curAuth.key, (buf, cb) => {
405
+ const signature = curAuth.key.sign(buf);
406
+ if (signature instanceof Error) {
407
+ signature.message =
408
+ `Error signing data with key: ${signature.message}`;
409
+ signature.level = 'client-authentication';
410
+ this.emit('error', signature);
411
+ return tryNextAuth();
412
+ }
413
+ cb(signature);
414
+ });
415
+ }
416
+ },
417
+ USERAUTH_INFO_REQUEST: (p, name, instructions, prompts) => {
418
+ if (curAuth.type === 'keyboard-interactive') {
419
+ const nprompts = (Array.isArray(prompts) ? prompts.length : 0);
420
+ if (nprompts === 0) {
421
+ debug && debug(
422
+ 'Client: Sending automatic USERAUTH_INFO_RESPONSE'
423
+ );
424
+ proto.authInfoRes();
425
+ return;
426
+ }
427
+ // We sent a keyboard-interactive user authentication request and
428
+ // now the server is sending us the prompts we need to present to
429
+ // the user
430
+ curAuth.prompt(
514
431
  name,
515
432
  instructions,
516
- lang,
433
+ '',
517
434
  prompts,
518
- function(answers) {
519
- stream.authInfoRes(answers);
520
- }
521
- );
522
- }
523
- function onUSERAUTH_PK_OK() {
524
- if (curAuth === 'agent') {
525
- var agentKey = agentKeys[agentKeyPos];
526
- var keyLen = readUInt32BE(agentKey, 0);
527
- var pubKeyFullType = agentKey.toString('ascii', 4, 4 + keyLen);
528
- var pubKeyType = pubKeyFullType.slice(4);
529
- // Check that we support the key type first
530
- // TODO: move key type checking logic to ssh2-streams
531
- switch (pubKeyFullType) {
532
- case 'ssh-rsa':
533
- case 'ssh-dss':
534
- case 'ecdsa-sha2-nistp256':
535
- case 'ecdsa-sha2-nistp384':
536
- case 'ecdsa-sha2-nistp521':
537
- break;
538
- default:
539
- if (EDDSA_SUPPORTED && pubKeyFullType === 'ssh-ed25519')
540
- break;
541
- debug('DEBUG: Agent: Skipping unsupported key type: '
542
- + pubKeyFullType);
543
- return tryNextAgentKey();
544
- }
545
- stream.authPK(self.config.username,
546
- agentKey,
547
- function(buf, cb) {
548
- agentQuery(self.config.agent,
549
- agentKey,
550
- pubKeyType,
551
- buf,
552
- function(err, signed) {
553
- if (err) {
554
- err.level = 'agent';
555
- self.emit('error', err);
556
- } else {
557
- var sigFullTypeLen = readUInt32BE(signed, 0);
558
- if (4 + sigFullTypeLen + 4 < signed.length) {
559
- var sigFullType = signed.toString('ascii', 4, 4 + sigFullTypeLen);
560
- if (sigFullType !== pubKeyFullType) {
561
- err = new Error('Agent key/signature type mismatch');
562
- err.level = 'agent';
563
- self.emit('error', err);
564
- } else {
565
- // skip algoLen + algo + sigLen
566
- return cb(signed.slice(4 + sigFullTypeLen + 4));
435
+ (answers) => {
436
+ proto.authInfoRes(answers);
567
437
  }
438
+ );
439
+ }
440
+ },
441
+ REQUEST_SUCCESS: (p, data) => {
442
+ if (callbacks.length)
443
+ callbacks.shift()(false, data);
444
+ },
445
+ REQUEST_FAILURE: (p) => {
446
+ if (callbacks.length)
447
+ callbacks.shift()(true);
448
+ },
449
+ GLOBAL_REQUEST: (p, name, wantReply, data) => {
450
+ switch (name) {
451
+ case 'hostkeys-00@openssh.com':
452
+ // Automatically verify keys before passing to end user
453
+ hostKeysProve(this, data, (err, keys) => {
454
+ if (err)
455
+ return;
456
+ this.emit('hostkeys', keys);
457
+ });
458
+ if (wantReply)
459
+ proto.requestSuccess();
460
+ break;
461
+ default:
462
+ // Auto-reject all other global requests, this can be especially
463
+ // useful if the server is sending us dummy keepalive global
464
+ // requests
465
+ if (wantReply)
466
+ proto.requestFailure();
467
+ }
468
+ },
469
+ CHANNEL_OPEN: (p, info) => {
470
+ // Handle incoming requests from server, typically a forwarded TCP or
471
+ // X11 connection
472
+ onCHANNEL_OPEN(this, info);
473
+ },
474
+ CHANNEL_OPEN_CONFIRMATION: (p, info) => {
475
+ const channel = this._chanMgr.get(info.recipient);
476
+ if (typeof channel !== 'function')
477
+ return;
478
+
479
+ const isSFTP = (channel.type === 'sftp');
480
+ const type = (isSFTP ? 'session' : channel.type);
481
+ const chanInfo = {
482
+ type,
483
+ incoming: {
484
+ id: info.recipient,
485
+ window: MAX_WINDOW,
486
+ packetSize: PACKET_SIZE,
487
+ state: 'open'
488
+ },
489
+ outgoing: {
490
+ id: info.sender,
491
+ window: info.window,
492
+ packetSize: info.packetSize,
493
+ state: 'open'
568
494
  }
495
+ };
496
+ const instance = (
497
+ isSFTP
498
+ ? new SFTP(this, chanInfo, { debug })
499
+ : new Channel(this, chanInfo)
500
+ );
501
+ this._chanMgr.update(info.recipient, instance);
502
+ channel(undefined, instance);
503
+ },
504
+ CHANNEL_OPEN_FAILURE: (p, recipient, reason, description) => {
505
+ const channel = this._chanMgr.get(recipient);
506
+ if (typeof channel !== 'function')
507
+ return;
508
+
509
+ const info = { reason, description };
510
+ onChannelOpenFailure(this, recipient, info, channel);
511
+ },
512
+ CHANNEL_DATA: (p, recipient, data) => {
513
+ const channel = this._chanMgr.get(recipient);
514
+ if (typeof channel !== 'object' || channel === null)
515
+ return;
516
+
517
+ // The remote party should not be sending us data if there is no
518
+ // window space available ...
519
+ // TODO: raise error on data with not enough window?
520
+ if (channel.incoming.window === 0)
521
+ return;
522
+
523
+ channel.incoming.window -= data.length;
524
+
525
+ if (channel.push(data) === false) {
526
+ channel._waitChanDrain = true;
527
+ return;
569
528
  }
570
529
 
571
- tryNextAgentKey();
572
- });
573
- });
574
- } else if (curAuth === 'publickey') {
575
- stream.authPK(self.config.username, privateKey, function(buf, cb) {
576
- var signature = privateKey.sign(buf);
577
- if (signature instanceof Error) {
578
- signature.message = 'Error while signing data with privateKey: '
579
- + signature.message;
580
- signature.level = 'client-authentication';
581
- self.emit('error', signature);
582
- return tryNextAuth();
583
- }
584
- cb(signature);
585
- });
586
- }
587
- }
588
- function onUSERAUTH_FAILURE(authsLeft, partial) {
589
- stream.removeListener('USERAUTH_PK_OK', onUSERAUTH_PK_OK);
590
- stream.removeListener('USERAUTH_INFO_REQUEST', onUSERAUTH_INFO_REQUEST);
591
- if (curAuth === 'agent') {
592
- debug('DEBUG: Client: Agent key #' + (agentKeyPos + 1) + ' failed');
593
- return tryNextAgentKey();
594
- } else {
595
- debug('DEBUG: Client: ' + curAuth + ' auth failed');
596
- }
530
+ if (channel.incoming.window <= WINDOW_THRESHOLD)
531
+ windowAdjust(channel);
532
+ },
533
+ CHANNEL_EXTENDED_DATA: (p, recipient, data, type) => {
534
+ if (type !== STDERR)
535
+ return;
597
536
 
598
- curPartial = partial;
599
- curAuthsLeft = authsLeft;
600
- tryNextAuth();
601
- }
602
- stream.once('USERAUTH_SUCCESS', function() {
603
- stream.removeListener('USERAUTH_FAILURE', onUSERAUTH_FAILURE);
604
- stream.removeListener('USERAUTH_INFO_REQUEST', onUSERAUTH_INFO_REQUEST);
537
+ const channel = this._chanMgr.get(recipient);
538
+ if (typeof channel !== 'object' || channel === null)
539
+ return;
605
540
 
606
- // start keepalive mechanism
607
- resetKA();
541
+ // The remote party should not be sending us data if there is no
542
+ // window space available ...
543
+ // TODO: raise error on data with not enough window?
544
+ if (channel.incoming.window === 0)
545
+ return;
608
546
 
609
- clearTimeout(self._readyTimeout);
547
+ channel.incoming.window -= data.length;
548
+
549
+ if (!channel.stderr.push(data)) {
550
+ channel._waitChanDrain = true;
551
+ return;
552
+ }
610
553
 
611
- self.emit('ready');
612
- }).on('USERAUTH_FAILURE', onUSERAUTH_FAILURE);
613
- // end authentication handling ===============================================
554
+ if (channel.incoming.window <= WINDOW_THRESHOLD)
555
+ windowAdjust(channel);
556
+ },
557
+ CHANNEL_WINDOW_ADJUST: (p, recipient, amount) => {
558
+ const channel = this._chanMgr.get(recipient);
559
+ if (typeof channel !== 'object' || channel === null)
560
+ return;
561
+
562
+ // The other side is allowing us to send `amount` more bytes of data
563
+ channel.outgoing.window += amount;
564
+
565
+ if (channel._waitWindow) {
566
+ channel._waitWindow = false;
567
+
568
+ if (channel._chunk) {
569
+ channel._write(channel._chunk, null, channel._chunkcb);
570
+ } else if (channel._chunkcb) {
571
+ channel._chunkcb();
572
+ } else if (channel._chunkErr) {
573
+ channel.stderr._write(channel._chunkErr,
574
+ null,
575
+ channel._chunkcbErr);
576
+ } else if (channel._chunkcbErr) {
577
+ channel._chunkcbErr();
578
+ }
579
+ }
580
+ },
581
+ CHANNEL_SUCCESS: (p, recipient) => {
582
+ const channel = this._chanMgr.get(recipient);
583
+ if (typeof channel !== 'object' || channel === null)
584
+ return;
585
+
586
+ this._resetKA();
587
+
588
+ if (channel._callbacks.length)
589
+ channel._callbacks.shift()(false);
590
+ },
591
+ CHANNEL_FAILURE: (p, recipient) => {
592
+ const channel = this._chanMgr.get(recipient);
593
+ if (typeof channel !== 'object' || channel === null)
594
+ return;
595
+
596
+ this._resetKA();
597
+
598
+ if (channel._callbacks.length)
599
+ channel._callbacks.shift()(true);
600
+ },
601
+ CHANNEL_REQUEST: (p, recipient, type, wantReply, data) => {
602
+ const channel = this._chanMgr.get(recipient);
603
+ if (typeof channel !== 'object' || channel === null)
604
+ return;
605
+
606
+ const exit = channel._exit;
607
+ if (exit.code !== undefined)
608
+ return;
609
+ switch (type) {
610
+ case 'exit-status':
611
+ channel.emit('exit', exit.code = data);
612
+ return;
613
+ case 'exit-signal':
614
+ channel.emit('exit',
615
+ exit.code = null,
616
+ exit.signal = `SIG${data.signal}`,
617
+ exit.dump = data.coreDumped,
618
+ exit.desc = data.errorMessage);
619
+ return;
620
+ }
614
621
 
615
- // handle initial handshake completion
616
- stream.once('ready', function() {
617
- stream.service('ssh-userauth');
618
- stream.once('SERVICE_ACCEPT', function(svcName) {
619
- if (svcName === 'ssh-userauth')
620
- tryNextAuth();
622
+ // Keepalive request? OpenSSH will send one as a channel request if
623
+ // there is a channel open
624
+
625
+ if (wantReply)
626
+ p.channelFailure(channel.outgoing.id);
627
+ },
628
+ CHANNEL_EOF: (p, recipient) => {
629
+ const channel = this._chanMgr.get(recipient);
630
+ if (typeof channel !== 'object' || channel === null)
631
+ return;
632
+
633
+ if (channel.incoming.state !== 'open')
634
+ return;
635
+ channel.incoming.state = 'eof';
636
+
637
+ if (channel.readable)
638
+ channel.push(null);
639
+ if (channel.stderr.readable)
640
+ channel.stderr.push(null);
641
+ },
642
+ CHANNEL_CLOSE: (p, recipient) => {
643
+ onCHANNEL_CLOSE(this, recipient, this._chanMgr.get(recipient));
644
+ },
645
+ },
621
646
  });
622
- });
623
647
 
624
- // handle incoming requests from server, typically a forwarded TCP or X11
625
- // connection
626
- stream.on('CHANNEL_OPEN', function(info) {
627
- onCHANNEL_OPEN(self, info);
628
- });
648
+ sock.pause();
649
+
650
+ // TODO: check keepalive implementation
651
+ // Keepalive-related
652
+ const kainterval = this.config.keepaliveInterval;
653
+ const kacountmax = this.config.keepaliveCountMax;
654
+ let kacount = 0;
655
+ let katimer;
656
+ const sendKA = () => {
657
+ if (++kacount > kacountmax) {
658
+ clearInterval(katimer);
659
+ if (sock.readable) {
660
+ const err = new Error('Keepalive timeout');
661
+ err.level = 'client-timeout';
662
+ this.emit('error', err);
663
+ sock.destroy();
664
+ }
665
+ return;
666
+ }
667
+ if (isWritable(sock)) {
668
+ // Append dummy callback to keep correct callback order
669
+ callbacks.push(resetKA);
670
+ proto.ping();
671
+ } else {
672
+ clearInterval(katimer);
673
+ }
674
+ };
675
+ function resetKA() {
676
+ if (kainterval > 0) {
677
+ kacount = 0;
678
+ clearInterval(katimer);
679
+ if (isWritable(sock))
680
+ katimer = setInterval(sendKA, kainterval);
681
+ }
682
+ }
683
+ this._resetKA = resetKA;
629
684
 
630
- // handle responses for tcpip-forward and other global requests
631
- stream.on('REQUEST_SUCCESS', function(data) {
632
- if (callbacks.length)
633
- callbacks.shift()(false, data);
634
- }).on('REQUEST_FAILURE', function() {
635
- if (callbacks.length)
636
- callbacks.shift()(true);
637
- });
685
+ const onDone = (() => {
686
+ let called = false;
687
+ return () => {
688
+ if (called)
689
+ return;
690
+ called = true;
691
+ if (wasConnected && !sawHeader) {
692
+ const err =
693
+ makeError('Connection lost before handshake', 'protocol', true);
694
+ this.emit('error', err);
695
+ }
696
+ };
697
+ })();
698
+ const onConnect = (() => {
699
+ let called = false;
700
+ return () => {
701
+ if (called)
702
+ return;
703
+ called = true;
704
+
705
+ wasConnected = true;
706
+ debug && debug('Socket connected');
707
+ this.emit('connect');
708
+
709
+ cryptoInit.then(() => {
710
+ proto.start();
711
+ sock.on('data', (data) => {
712
+ try {
713
+ proto.parse(data, 0, data.length);
714
+ } catch (ex) {
715
+ this.emit('error', ex);
716
+ try {
717
+ if (isWritable(sock))
718
+ sock.end();
719
+ } catch {}
720
+ }
721
+ });
638
722
 
639
- stream.on('GLOBAL_REQUEST', function(name, wantReply, data) {
640
- // auto-reject all global requests, this can be especially useful if the
641
- // server is sending us dummy keepalive global requests
642
- if (wantReply)
643
- stream.requestFailure();
644
- });
723
+ // Drain stderr if we are connection hopping using an exec stream
724
+ if (sock.stderr && typeof sock.stderr.resume === 'function')
725
+ sock.stderr.resume();
726
+
727
+ sock.resume();
728
+ }).catch((err) => {
729
+ this.emit('error', err);
730
+ try {
731
+ if (isWritable(sock))
732
+ sock.end();
733
+ } catch {}
734
+ });
735
+ };
736
+ })();
737
+ let wasConnected = false;
738
+ sock.on('connect', onConnect)
739
+ .on('timeout', () => {
740
+ this.emit('timeout');
741
+ }).on('error', (err) => {
742
+ debug && debug(`Socket error: ${err.message}`);
743
+ clearTimeout(this._readyTimeout);
744
+ err.level = 'client-socket';
745
+ this.emit('error', err);
746
+ }).on('end', () => {
747
+ debug && debug('Socket ended');
748
+ onDone();
749
+ proto.cleanup();
750
+ clearTimeout(this._readyTimeout);
751
+ clearInterval(katimer);
752
+ this.emit('end');
753
+ }).on('close', () => {
754
+ debug && debug('Socket closed');
755
+ onDone();
756
+ proto.cleanup();
757
+ clearTimeout(this._readyTimeout);
758
+ clearInterval(katimer);
759
+ this.emit('close');
645
760
 
646
- if (!cfg.sock) {
647
- var host = this.config.host;
648
- var forceIPv4 = this.config.forceIPv4;
649
- var forceIPv6 = this.config.forceIPv6;
761
+ // Notify outstanding channel requests of disconnection ...
762
+ const callbacks_ = callbacks;
763
+ callbacks = this._callbacks = [];
764
+ const err = new Error('No response from server');
765
+ for (let i = 0; i < callbacks_.length; ++i)
766
+ callbacks_[i](err);
650
767
 
651
- debug('DEBUG: Client: Trying '
652
- + host
653
- + ' on port '
654
- + this.config.port
655
- + ' ...');
768
+ // Simulate error for any channels waiting to be opened
769
+ this._chanMgr.cleanup(err);
770
+ });
656
771
 
657
- function doConnect() {
658
- startTimeout();
659
- self._sock.connect({
660
- host: host,
661
- port: self.config.port,
662
- localAddress: self.config.localAddress,
663
- localPort: self.config.localPort
664
- });
665
- self._sock.setNoDelay(true);
666
- self._sock.setMaxListeners(0);
667
- self._sock.setTimeout(typeof cfg.timeout === 'number' ? cfg.timeout : 0);
772
+ // Begin authentication handling ===========================================
773
+ let curAuth;
774
+ let curPartial = null;
775
+ let curAuthsLeft = null;
776
+ const authsAllowed = ['none'];
777
+ if (this.config.password !== undefined)
778
+ authsAllowed.push('password');
779
+ if (privateKey !== undefined)
780
+ authsAllowed.push('publickey');
781
+ if (this._agent !== undefined)
782
+ authsAllowed.push('agent');
783
+ if (this.config.tryKeyboard)
784
+ authsAllowed.push('keyboard-interactive');
785
+ if (privateKey !== undefined
786
+ && this.config.localHostname !== undefined
787
+ && this.config.localUsername !== undefined) {
788
+ authsAllowed.push('hostbased');
668
789
  }
669
790
 
670
- if ((!forceIPv4 && !forceIPv6) || (forceIPv4 && forceIPv6))
671
- doConnect();
672
- else {
673
- dnsLookup(host, (forceIPv4 ? 4 : 6), function(err, address, family) {
674
- if (err) {
675
- var error = new Error('Error while looking up '
676
- + (forceIPv4 ? 'IPv4' : 'IPv6')
677
- + ' address for host '
678
- + host
679
- + ': ' + err);
680
- clearTimeout(self._readyTimeout);
681
- error.level = 'client-dns';
682
- self.emit('error', error);
683
- self.emit('close');
684
- return;
685
- }
686
- host = address;
687
- doConnect();
688
- });
689
- }
690
- } else {
691
- startTimeout();
692
- stream.pipe(sock).pipe(stream);
693
- }
791
+ if (Array.isArray(authHandler))
792
+ authHandler = makeSimpleAuthHandler(authHandler);
793
+ else if (typeof authHandler !== 'function')
794
+ authHandler = makeSimpleAuthHandler(authsAllowed);
694
795
 
695
- function startTimeout() {
696
- if (self.config.readyTimeout > 0) {
697
- self._readyTimeout = setTimeout(function() {
698
- var err = new Error('Timed out while waiting for handshake');
699
- err.level = 'client-timeout';
700
- self.emit('error', err);
701
- sock.destroy();
702
- }, self.config.readyTimeout);
703
- }
704
- }
705
- };
706
-
707
- Client.prototype.end = function() {
708
- if (this._sock
709
- && this._sock.writable
710
- && this._sshstream
711
- && this._sshstream.writable)
712
- return this._sshstream.disconnect();
713
- return false;
714
- };
715
-
716
- Client.prototype.destroy = function() {
717
- this._sock && this._sock.destroy();
718
- };
719
-
720
- Client.prototype.exec = function(cmd, opts, cb) {
721
- if (!this._sock
722
- || !this._sock.writable
723
- || !this._sshstream
724
- || !this._sshstream.writable)
725
- throw new Error('Not connected');
796
+ let hasSentAuth = false;
797
+ const doNextAuth = (nextAuth) => {
798
+ if (hasSentAuth)
799
+ return;
800
+ hasSentAuth = true;
726
801
 
727
- if (typeof opts === 'function') {
728
- cb = opts;
729
- opts = {};
730
- }
802
+ if (nextAuth === false) {
803
+ const err = new Error('All configured authentication methods failed');
804
+ err.level = 'client-authentication';
805
+ this.emit('error', err);
806
+ this.end();
807
+ return;
808
+ }
731
809
 
732
- var self = this;
733
- var extraOpts = { allowHalfOpen: (opts.allowHalfOpen !== false) };
810
+ if (typeof nextAuth === 'string') {
811
+ // Remain backwards compatible with original `authHandler()` usage,
812
+ // which only supported passing names of next method to try using data
813
+ // from the `connect()` config object
734
814
 
735
- return openChannel(this, 'session', extraOpts, function(err, chan) {
736
- if (err)
737
- return cb(err);
815
+ const type = nextAuth;
816
+ if (authsAllowed.indexOf(type) === -1)
817
+ return skipAuth(`Authentication method not allowed: ${type}`);
738
818
 
739
- var todo = [];
819
+ const username = this.config.username;
820
+ switch (type) {
821
+ case 'password':
822
+ nextAuth = { type, username, password: this.config.password };
823
+ break;
824
+ case 'publickey':
825
+ nextAuth = { type, username, key: privateKey };
826
+ break;
827
+ case 'hostbased':
828
+ nextAuth = {
829
+ type,
830
+ username,
831
+ key: privateKey,
832
+ localHostname: this.config.localHostname,
833
+ localUsername: this.config.localUsername,
834
+ };
835
+ break;
836
+ case 'agent':
837
+ nextAuth = {
838
+ type,
839
+ username,
840
+ agentCtx: new AgentContext(this._agent),
841
+ };
842
+ break;
843
+ case 'keyboard-interactive':
844
+ nextAuth = {
845
+ type,
846
+ username,
847
+ prompt: (...args) => this.emit('keyboard-interactive', ...args),
848
+ };
849
+ break;
850
+ case 'none':
851
+ nextAuth = { type, username };
852
+ break;
853
+ default:
854
+ return skipAuth(
855
+ `Skipping unsupported authentication method: ${nextAuth}`
856
+ );
857
+ }
858
+ } else if (typeof nextAuth !== 'object' || nextAuth === null) {
859
+ return skipAuth(
860
+ `Skipping invalid authentication attempt: ${nextAuth}`
861
+ );
862
+ } else {
863
+ const username = nextAuth.username;
864
+ if (typeof username !== 'string') {
865
+ return skipAuth(
866
+ `Skipping invalid authentication attempt: ${nextAuth}`
867
+ );
868
+ }
869
+ const type = nextAuth.type;
870
+ switch (type) {
871
+ case 'password': {
872
+ const { password } = nextAuth;
873
+ if (typeof password !== 'string' && !Buffer.isBuffer(password))
874
+ return skipAuth('Skipping invalid password auth attempt');
875
+ nextAuth = { type, username, password };
876
+ break;
877
+ }
878
+ case 'publickey': {
879
+ const key = parseKey(nextAuth.key, nextAuth.passphrase);
880
+ if (key instanceof Error)
881
+ return skipAuth('Skipping invalid key auth attempt');
882
+ if (!key.isPrivateKey())
883
+ return skipAuth('Skipping non-private key');
884
+ nextAuth = { type, username, key };
885
+ break;
886
+ }
887
+ case 'hostbased': {
888
+ const { localHostname, localUsername } = nextAuth;
889
+ const key = parseKey(nextAuth.key, nextAuth.passphrase);
890
+ if (key instanceof Error
891
+ || typeof localHostname !== 'string'
892
+ || typeof localUsername !== 'string') {
893
+ return skipAuth('Skipping invalid hostbased auth attempt');
894
+ }
895
+ if (!key.isPrivateKey())
896
+ return skipAuth('Skipping non-private key');
897
+ nextAuth = { type, username, key, localHostname, localUsername };
898
+ break;
899
+ }
900
+ case 'agent': {
901
+ let agent = nextAuth.agent;
902
+ if (typeof agent === 'string' && agent.length) {
903
+ agent = createAgent(agent);
904
+ } else if (!isAgent(agent)) {
905
+ return skipAuth(
906
+ `Skipping invalid agent: ${nextAuth.agent}`
907
+ );
908
+ }
909
+ nextAuth = { type, username, agentCtx: new AgentContext(agent) };
910
+ break;
911
+ }
912
+ case 'keyboard-interactive': {
913
+ const { prompt } = nextAuth;
914
+ if (typeof prompt !== 'function') {
915
+ return skipAuth(
916
+ 'Skipping invalid keyboard-interactive auth attempt'
917
+ );
918
+ }
919
+ nextAuth = { type, username, prompt };
920
+ break;
921
+ }
922
+ case 'none':
923
+ nextAuth = { type, username };
924
+ break;
925
+ default:
926
+ return skipAuth(
927
+ `Skipping unsupported authentication method: ${nextAuth}`
928
+ );
929
+ }
930
+ }
931
+ curAuth = nextAuth;
932
+
933
+ // Begin authentication method's process
934
+ try {
935
+ const username = curAuth.username;
936
+ switch (curAuth.type) {
937
+ case 'password':
938
+ proto.authPassword(username, curAuth.password);
939
+ break;
940
+ case 'publickey':
941
+ proto.authPK(username, curAuth.key);
942
+ break;
943
+ case 'hostbased':
944
+ proto.authHostbased(username,
945
+ curAuth.key,
946
+ curAuth.localHostname,
947
+ curAuth.localUsername,
948
+ (buf, cb) => {
949
+ const signature = curAuth.key.sign(buf);
950
+ if (signature instanceof Error) {
951
+ signature.message =
952
+ `Error while signing with key: ${signature.message}`;
953
+ signature.level = 'client-authentication';
954
+ this.emit('error', signature);
955
+ return tryNextAuth();
956
+ }
740
957
 
741
- function reqCb(err) {
742
- if (err) {
743
- chan.close();
744
- return cb(err);
958
+ cb(signature);
959
+ });
960
+ break;
961
+ case 'agent':
962
+ curAuth.agentCtx.init((err) => {
963
+ if (err) {
964
+ err.level = 'agent';
965
+ this.emit('error', err);
966
+ return tryNextAuth();
967
+ }
968
+ tryNextAgentKey();
969
+ });
970
+ break;
971
+ case 'keyboard-interactive':
972
+ proto.authKeyboard(username);
973
+ break;
974
+ case 'none':
975
+ proto.authNone(username);
976
+ break;
977
+ }
978
+ } finally {
979
+ hasSentAuth = false;
745
980
  }
746
- if (todo.length)
747
- todo.shift()();
981
+ };
982
+
983
+ function skipAuth(msg) {
984
+ debug && debug(msg);
985
+ process.nextTick(tryNextAuth);
748
986
  }
749
987
 
750
- if (self.config.allowAgentFwd === true
751
- || (opts
752
- && opts.agentForward === true
753
- && self.config.agent !== undefined)) {
754
- todo.push(function() {
755
- reqAgentFwd(chan, reqCb);
756
- });
988
+ function tryNextAuth() {
989
+ hasSentAuth = false;
990
+ const auth = authHandler(curAuthsLeft, curPartial, doNextAuth);
991
+ if (hasSentAuth || auth === undefined)
992
+ return;
993
+ doNextAuth(auth);
757
994
  }
758
995
 
759
- if (typeof opts === 'object' && opts !== null) {
760
- if (typeof opts.env === 'object' && opts.env !== null)
761
- reqEnv(chan, opts.env);
762
- if ((typeof opts.pty === 'object' && opts.pty !== null)
763
- || opts.pty === true) {
764
- todo.push(function() { reqPty(chan, opts.pty, reqCb); });
996
+ const tryNextAgentKey = () => {
997
+ if (curAuth.type === 'agent') {
998
+ const key = curAuth.agentCtx.nextKey();
999
+ if (key === false) {
1000
+ debug && debug('Agent: No more keys left to try');
1001
+ debug && debug('Client: agent auth failed');
1002
+ tryNextAuth();
1003
+ } else {
1004
+ const pos = curAuth.agentCtx.pos();
1005
+ debug && debug(`Agent: Trying key #${pos + 1}`);
1006
+ proto.authPK(curAuth.username, key);
1007
+ }
765
1008
  }
766
- if ((typeof opts.x11 === 'object' && opts.x11 !== null)
767
- || opts.x11 === 'number'
768
- || opts.x11 === true) {
769
- todo.push(function() { reqX11(chan, opts.x11, reqCb); });
1009
+ };
1010
+
1011
+ const startTimeout = () => {
1012
+ if (this.config.readyTimeout > 0) {
1013
+ this._readyTimeout = setTimeout(() => {
1014
+ const err = new Error('Timed out while waiting for handshake');
1015
+ err.level = 'client-timeout';
1016
+ this.emit('error', err);
1017
+ sock.destroy();
1018
+ }, this.config.readyTimeout);
770
1019
  }
771
- }
1020
+ };
772
1021
 
773
- todo.push(function() { reqExec(chan, cmd, opts, cb); });
774
- todo.shift()();
775
- });
776
- };
777
-
778
- Client.prototype.shell = function(wndopts, opts, cb) {
779
- if (!this._sock
780
- || !this._sock.writable
781
- || !this._sshstream
782
- || !this._sshstream.writable)
783
- throw new Error('Not connected');
784
-
785
- // start an interactive terminal/shell session
786
- var self = this;
787
-
788
- if (typeof wndopts === 'function') {
789
- cb = wndopts;
790
- wndopts = opts = undefined;
791
- } else if (typeof opts === 'function') {
792
- cb = opts;
793
- opts = undefined;
794
- }
795
- if (wndopts && (wndopts.x11 !== undefined || wndopts.env !== undefined)) {
796
- opts = wndopts;
797
- wndopts = undefined;
798
- }
1022
+ if (!cfg.sock) {
1023
+ let host = this.config.host;
1024
+ const forceIPv4 = this.config.forceIPv4;
1025
+ const forceIPv6 = this.config.forceIPv6;
799
1026
 
800
- return openChannel(this, 'session', function(err, chan) {
801
- if (err)
802
- return cb(err);
1027
+ debug && debug(`Client: Trying ${host} on port ${this.config.port} ...`);
803
1028
 
804
- var todo = [];
1029
+ const doConnect = () => {
1030
+ startTimeout();
1031
+ sock.connect({
1032
+ host,
1033
+ port: this.config.port,
1034
+ localAddress: this.config.localAddress,
1035
+ localPort: this.config.localPort
1036
+ });
1037
+ sock.setNoDelay(true);
1038
+ sock.setMaxListeners(0);
1039
+ sock.setTimeout(typeof cfg.timeout === 'number' ? cfg.timeout : 0);
1040
+ };
805
1041
 
806
- function reqCb(err) {
807
- if (err) {
808
- chan.close();
809
- return cb(err);
1042
+ if ((!forceIPv4 && !forceIPv6) || (forceIPv4 && forceIPv6)) {
1043
+ doConnect();
1044
+ } else {
1045
+ dnsLookup(host, (forceIPv4 ? 4 : 6), (err, address, family) => {
1046
+ if (err) {
1047
+ const type = (forceIPv4 ? 'IPv4' : 'IPv6');
1048
+ const error = new Error(
1049
+ `Error while looking up ${type} address for '${host}': ${err}`
1050
+ );
1051
+ clearTimeout(this._readyTimeout);
1052
+ error.level = 'client-dns';
1053
+ this.emit('error', error);
1054
+ this.emit('close');
1055
+ return;
1056
+ }
1057
+ host = address;
1058
+ doConnect();
1059
+ });
1060
+ }
1061
+ } else {
1062
+ // Custom socket passed in
1063
+ startTimeout();
1064
+ if (typeof sock.connecting === 'boolean') {
1065
+ // net.Socket
1066
+
1067
+ if (!sock.connecting) {
1068
+ // Already connected
1069
+ onConnect();
1070
+ }
1071
+ } else {
1072
+ // Assume socket/stream is already "connected"
1073
+ onConnect();
810
1074
  }
811
- if (todo.length)
812
- todo.shift()();
813
1075
  }
814
1076
 
815
- if (self.config.allowAgentFwd === true
816
- || (opts
817
- && opts.agentForward === true
818
- && self.config.agent !== undefined)) {
819
- todo.push(function() { reqAgentFwd(chan, reqCb); });
1077
+ return this;
1078
+ }
1079
+
1080
+ end() {
1081
+ if (this._sock && isWritable(this._sock)) {
1082
+ this._protocol.disconnect(DISCONNECT_REASON.BY_APPLICATION);
1083
+ this._sock.end();
820
1084
  }
1085
+ return this;
1086
+ }
821
1087
 
822
- if (wndopts !== false)
823
- todo.push(function() { reqPty(chan, wndopts, reqCb); });
1088
+ destroy() {
1089
+ this._sock && isWritable(this._sock) && this._sock.destroy();
1090
+ return this;
1091
+ }
824
1092
 
825
- if (typeof opts === 'object' && opts !== null) {
826
- if (typeof opts.env === 'object' && opts.env !== null)
827
- reqEnv(chan, opts.env);
828
- if ((typeof opts.x11 === 'object' && opts.x11 !== null)
829
- || opts.x11 === 'number'
830
- || opts.x11 === true) {
831
- todo.push(function() { reqX11(chan, opts.x11, reqCb); });
832
- }
1093
+ exec(cmd, opts, cb) {
1094
+ if (!this._sock || !isWritable(this._sock))
1095
+ throw new Error('Not connected');
1096
+
1097
+ if (typeof opts === 'function') {
1098
+ cb = opts;
1099
+ opts = {};
833
1100
  }
834
1101
 
835
- todo.push(function() { reqShell(chan, cb); });
836
- todo.shift()();
837
- });
838
- };
839
-
840
- Client.prototype.subsys = function(name, cb) {
841
- if (!this._sock
842
- || !this._sock.writable
843
- || !this._sshstream
844
- || !this._sshstream.writable)
845
- throw new Error('Not connected');
846
-
847
- return openChannel(this, 'session', function(err, chan) {
848
- if (err)
849
- return cb(err);
850
-
851
- reqSubsystem(chan, name, function(err, stream) {
852
- if (err)
853
- return cb(err);
854
-
855
- cb(undefined, stream);
856
- });
857
- });
858
- };
859
-
860
- Client.prototype.sftp = function(cb) {
861
- if (!this._sock
862
- || !this._sock.writable
863
- || !this._sshstream
864
- || !this._sshstream.writable)
865
- throw new Error('Not connected');
866
-
867
- var self = this;
868
-
869
- // start an SFTP session
870
- return openChannel(this, 'session', function(err, chan) {
871
- if (err)
872
- return cb(err);
873
-
874
- reqSubsystem(chan, 'sftp', function(err, stream) {
875
- if (err)
876
- return cb(err);
877
-
878
- var serverIdentRaw = self._sshstream._state.incoming.identRaw;
879
- var cfg = { debug: self.config.debug };
880
- var sftp = new SFTPStream(cfg, serverIdentRaw);
881
-
882
- function onError(err) {
883
- sftp.removeListener('ready', onReady);
884
- stream.removeListener('exit', onExit);
1102
+ const extraOpts = { allowHalfOpen: (opts.allowHalfOpen !== false) };
1103
+
1104
+ openChannel(this, 'session', extraOpts, (err, chan) => {
1105
+ if (err) {
885
1106
  cb(err);
1107
+ return;
886
1108
  }
887
1109
 
888
- function onReady() {
889
- sftp.removeListener('error', onError);
890
- stream.removeListener('exit', onExit);
891
- cb(undefined, new SFTPWrapper(sftp));
892
- }
1110
+ const todo = [];
893
1111
 
894
- function onExit(code, signal) {
895
- sftp.removeListener('ready', onReady);
896
- sftp.removeListener('error', onError);
897
- var msg;
898
- if (typeof code === 'number') {
899
- msg = 'Received exit code '
900
- + code
901
- + ' while establishing SFTP session';
902
- } else {
903
- msg = 'Received signal '
904
- + signal
905
- + ' while establishing SFTP session';
1112
+ function reqCb(err) {
1113
+ if (err) {
1114
+ chan.close();
1115
+ cb(err);
1116
+ return;
906
1117
  }
907
- var err = new Error(msg);
908
- err.code = code;
909
- err.signal = signal;
910
- cb(err);
1118
+ if (todo.length)
1119
+ todo.shift()();
911
1120
  }
912
1121
 
913
- sftp.once('error', onError)
914
- .once('ready', onReady)
915
- .once('close', function() {
916
- stream.end();
917
- });
1122
+ if (this.config.allowAgentFwd === true
1123
+ || (opts
1124
+ && opts.agentForward === true
1125
+ && this._agent !== undefined)) {
1126
+ todo.push(() => reqAgentFwd(chan, reqCb));
1127
+ }
918
1128
 
919
- // OpenSSH server sends an exit-status if there was a problem spinning up
920
- // an sftp server child process, so we listen for that here in order to
921
- // properly raise an error.
922
- stream.once('exit', onExit);
1129
+ if (typeof opts === 'object' && opts !== null) {
1130
+ if (typeof opts.env === 'object' && opts.env !== null)
1131
+ reqEnv(chan, opts.env);
1132
+ if ((typeof opts.pty === 'object' && opts.pty !== null)
1133
+ || opts.pty === true) {
1134
+ todo.push(() => reqPty(chan, opts.pty, reqCb));
1135
+ }
1136
+ if ((typeof opts.x11 === 'object' && opts.x11 !== null)
1137
+ || opts.x11 === 'number'
1138
+ || opts.x11 === true) {
1139
+ todo.push(() => reqX11(chan, opts.x11, reqCb));
1140
+ }
1141
+ }
923
1142
 
924
- sftp.pipe(stream).pipe(sftp);
1143
+ todo.push(() => reqExec(chan, cmd, opts, cb));
1144
+ todo.shift()();
925
1145
  });
926
- });
927
- };
928
1146
 
929
- Client.prototype.forwardIn = function(bindAddr, bindPort, cb) {
930
- if (!this._sock
931
- || !this._sock.writable
932
- || !this._sshstream
933
- || !this._sshstream.writable)
934
- throw new Error('Not connected');
1147
+ return this;
1148
+ }
935
1149
 
936
- // send a request for the server to start forwarding TCP connections to us
937
- // on a particular address and port
1150
+ shell(wndopts, opts, cb) {
1151
+ if (!this._sock || !isWritable(this._sock))
1152
+ throw new Error('Not connected');
938
1153
 
939
- var self = this;
940
- var wantReply = (typeof cb === 'function');
1154
+ if (typeof wndopts === 'function') {
1155
+ cb = wndopts;
1156
+ wndopts = opts = undefined;
1157
+ } else if (typeof opts === 'function') {
1158
+ cb = opts;
1159
+ opts = undefined;
1160
+ }
1161
+ if (wndopts && (wndopts.x11 !== undefined || wndopts.env !== undefined)) {
1162
+ opts = wndopts;
1163
+ wndopts = undefined;
1164
+ }
941
1165
 
942
- if (wantReply) {
943
- this._callbacks.push(function(had_err, data) {
944
- if (had_err) {
945
- return cb(had_err !== true
946
- ? had_err
947
- : new Error('Unable to bind to ' + bindAddr + ':' + bindPort));
1166
+ openChannel(this, 'session', (err, chan) => {
1167
+ if (err) {
1168
+ cb(err);
1169
+ return;
948
1170
  }
949
1171
 
950
- var realPort = bindPort;
951
- if (bindPort === 0 && data && data.length >= 4) {
952
- realPort = readUInt32BE(data, 0);
953
- if (!(self._sshstream.remoteBugs & BUGS.DYN_RPORT_BUG))
954
- bindPort = realPort;
1172
+ const todo = [];
1173
+
1174
+ function reqCb(err) {
1175
+ if (err) {
1176
+ chan.close();
1177
+ cb(err);
1178
+ return;
1179
+ }
1180
+ if (todo.length)
1181
+ todo.shift()();
955
1182
  }
956
1183
 
957
- self._forwarding[bindAddr + ':' + bindPort] = realPort;
1184
+ if (this.config.allowAgentFwd === true
1185
+ || (opts
1186
+ && opts.agentForward === true
1187
+ && this._agent !== undefined)) {
1188
+ todo.push(() => reqAgentFwd(chan, reqCb));
1189
+ }
958
1190
 
959
- cb(undefined, realPort);
960
- });
961
- }
1191
+ if (wndopts !== false)
1192
+ todo.push(() => reqPty(chan, wndopts, reqCb));
962
1193
 
963
- return this._sshstream.tcpipForward(bindAddr, bindPort, wantReply);
964
- };
1194
+ if (typeof opts === 'object' && opts !== null) {
1195
+ if (typeof opts.env === 'object' && opts.env !== null)
1196
+ reqEnv(chan, opts.env);
1197
+ if ((typeof opts.x11 === 'object' && opts.x11 !== null)
1198
+ || opts.x11 === 'number'
1199
+ || opts.x11 === true) {
1200
+ todo.push(() => reqX11(chan, opts.x11, reqCb));
1201
+ }
1202
+ }
965
1203
 
966
- Client.prototype.unforwardIn = function(bindAddr, bindPort, cb) {
967
- if (!this._sock
968
- || !this._sock.writable
969
- || !this._sshstream
970
- || !this._sshstream.writable)
971
- throw new Error('Not connected');
1204
+ todo.push(() => reqShell(chan, cb));
1205
+ todo.shift()();
1206
+ });
972
1207
 
973
- // send a request to stop forwarding us new connections for a particular
974
- // address and port
1208
+ return this;
1209
+ }
975
1210
 
976
- var self = this;
977
- var wantReply = (typeof cb === 'function');
1211
+ subsys(name, cb) {
1212
+ if (!this._sock || !isWritable(this._sock))
1213
+ throw new Error('Not connected');
978
1214
 
979
- if (wantReply) {
980
- this._callbacks.push(function(had_err) {
981
- if (had_err) {
982
- return cb(had_err !== true
983
- ? had_err
984
- : new Error('Unable to unbind from '
985
- + bindAddr + ':' + bindPort));
1215
+ openChannel(this, 'session', (err, chan) => {
1216
+ if (err) {
1217
+ cb(err);
1218
+ return;
986
1219
  }
987
1220
 
988
- delete self._forwarding[bindAddr + ':' + bindPort];
1221
+ reqSubsystem(chan, name, (err, stream) => {
1222
+ if (err) {
1223
+ cb(err);
1224
+ return;
1225
+ }
989
1226
 
990
- cb();
1227
+ cb(undefined, stream);
1228
+ });
991
1229
  });
992
- }
993
-
994
- return this._sshstream.cancelTcpipForward(bindAddr, bindPort, wantReply);
995
- };
996
-
997
- Client.prototype.forwardOut = function(srcIP, srcPort, dstIP, dstPort, cb) {
998
- if (!this._sock
999
- || !this._sock.writable
1000
- || !this._sshstream
1001
- || !this._sshstream.writable)
1002
- throw new Error('Not connected');
1003
1230
 
1004
- // send a request to forward a TCP connection to the server
1005
-
1006
- var cfg = {
1007
- srcIP: srcIP,
1008
- srcPort: srcPort,
1009
- dstIP: dstIP,
1010
- dstPort: dstPort
1011
- };
1231
+ return this;
1232
+ }
1012
1233
 
1013
- return openChannel(this, 'direct-tcpip', cfg, cb);
1014
- };
1234
+ forwardIn(bindAddr, bindPort, cb) {
1235
+ if (!this._sock || !isWritable(this._sock))
1236
+ throw new Error('Not connected');
1015
1237
 
1016
- Client.prototype.openssh_noMoreSessions = function(cb) {
1017
- if (!this._sock
1018
- || !this._sock.writable
1019
- || !this._sshstream
1020
- || !this._sshstream.writable)
1021
- throw new Error('Not connected');
1238
+ // Send a request for the server to start forwarding TCP connections to us
1239
+ // on a particular address and port
1022
1240
 
1023
- var wantReply = (typeof cb === 'function');
1241
+ const wantReply = (typeof cb === 'function');
1024
1242
 
1025
- if (!this.config.strictVendor
1026
- || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) {
1027
1243
  if (wantReply) {
1028
- this._callbacks.push(function(had_err) {
1244
+ this._callbacks.push((had_err, data) => {
1029
1245
  if (had_err) {
1030
- return cb(had_err !== true
1031
- ? had_err
1032
- : new Error('Unable to disable future sessions'));
1246
+ cb(had_err !== true
1247
+ ? had_err
1248
+ : new Error(`Unable to bind to ${bindAddr}:${bindPort}`));
1249
+ return;
1033
1250
  }
1034
1251
 
1035
- cb();
1252
+ let realPort = bindPort;
1253
+ if (bindPort === 0 && data && data.length >= 4) {
1254
+ realPort = readUInt32BE(data, 0);
1255
+ if (!(this._protocol._compatFlags & COMPAT.DYN_RPORT_BUG))
1256
+ bindPort = realPort;
1257
+ }
1258
+
1259
+ this._forwarding[`${bindAddr}:${bindPort}`] = realPort;
1260
+
1261
+ cb(undefined, realPort);
1036
1262
  });
1037
1263
  }
1038
1264
 
1039
- return this._sshstream.openssh_noMoreSessions(wantReply);
1040
- } else if (wantReply) {
1041
- process.nextTick(function() {
1042
- cb(new Error('strictVendor enabled and server is not OpenSSH or compatible version'));
1043
- });
1265
+ this._protocol.tcpipForward(bindAddr, bindPort, wantReply);
1266
+
1267
+ return this;
1044
1268
  }
1045
1269
 
1046
- return true;
1047
- };
1270
+ unforwardIn(bindAddr, bindPort, cb) {
1271
+ if (!this._sock || !isWritable(this._sock))
1272
+ throw new Error('Not connected');
1048
1273
 
1049
- Client.prototype.openssh_forwardInStreamLocal = function(socketPath, cb) {
1050
- if (!this._sock
1051
- || !this._sock.writable
1052
- || !this._sshstream
1053
- || !this._sshstream.writable)
1054
- throw new Error('Not connected');
1274
+ // Send a request to stop forwarding us new connections for a particular
1275
+ // address and port
1055
1276
 
1056
- var wantReply = (typeof cb === 'function');
1057
- var self = this;
1277
+ const wantReply = (typeof cb === 'function');
1058
1278
 
1059
- if (!this.config.strictVendor
1060
- || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) {
1061
1279
  if (wantReply) {
1062
- this._callbacks.push(function(had_err) {
1280
+ this._callbacks.push((had_err) => {
1063
1281
  if (had_err) {
1064
- return cb(had_err !== true
1065
- ? had_err
1066
- : new Error('Unable to bind to ' + socketPath));
1282
+ cb(had_err !== true
1283
+ ? had_err
1284
+ : new Error(`Unable to unbind from ${bindAddr}:${bindPort}`));
1285
+ return;
1067
1286
  }
1068
- self._forwardingUnix[socketPath] = true;
1287
+
1288
+ delete this._forwarding[`${bindAddr}:${bindPort}`];
1289
+
1069
1290
  cb();
1070
1291
  });
1071
1292
  }
1072
1293
 
1073
- return this._sshstream.openssh_streamLocalForward(socketPath, wantReply);
1074
- } else if (wantReply) {
1075
- process.nextTick(function() {
1076
- cb(new Error('strictVendor enabled and server is not OpenSSH or compatible version'));
1077
- });
1294
+ this._protocol.cancelTcpipForward(bindAddr, bindPort, wantReply);
1295
+
1296
+ return this;
1078
1297
  }
1079
1298
 
1080
- return true;
1081
- };
1299
+ forwardOut(srcIP, srcPort, dstIP, dstPort, cb) {
1300
+ if (!this._sock || !isWritable(this._sock))
1301
+ throw new Error('Not connected');
1082
1302
 
1083
- Client.prototype.openssh_unforwardInStreamLocal = function(socketPath, cb) {
1084
- if (!this._sock
1085
- || !this._sock.writable
1086
- || !this._sshstream
1087
- || !this._sshstream.writable)
1088
- throw new Error('Not connected');
1303
+ // Send a request to forward a TCP connection to the server
1089
1304
 
1090
- var wantReply = (typeof cb === 'function');
1091
- var self = this;
1305
+ const cfg = {
1306
+ srcIP: srcIP,
1307
+ srcPort: srcPort,
1308
+ dstIP: dstIP,
1309
+ dstPort: dstPort
1310
+ };
1092
1311
 
1093
- if (!this.config.strictVendor
1094
- || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) {
1095
- if (wantReply) {
1096
- this._callbacks.push(function(had_err) {
1097
- if (had_err) {
1098
- return cb(had_err !== true
1099
- ? had_err
1100
- : new Error('Unable to unbind on ' + socketPath));
1101
- }
1102
- delete self._forwardingUnix[socketPath];
1103
- cb();
1104
- });
1105
- }
1312
+ if (typeof cb !== 'function')
1313
+ cb = noop;
1106
1314
 
1107
- return this._sshstream.openssh_cancelStreamLocalForward(socketPath,
1108
- wantReply);
1109
- } else if (wantReply) {
1110
- process.nextTick(function() {
1111
- cb(new Error('strictVendor enabled and server is not OpenSSH or compatible version'));
1112
- });
1113
- }
1315
+ openChannel(this, 'direct-tcpip', cfg, cb);
1114
1316
 
1115
- return true;
1116
- };
1117
-
1118
- Client.prototype.openssh_forwardOutStreamLocal = function(socketPath, cb) {
1119
- if (!this._sock
1120
- || !this._sock.writable
1121
- || !this._sshstream
1122
- || !this._sshstream.writable)
1123
- throw new Error('Not connected');
1124
-
1125
- if (!this.config.strictVendor
1126
- || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) {
1127
- var cfg = { socketPath: socketPath };
1128
- return openChannel(this, 'direct-streamlocal@openssh.com', cfg, cb);
1129
- } else {
1130
- process.nextTick(function() {
1131
- cb(new Error('strictVendor enabled and server is not OpenSSH or compatible version'));
1132
- });
1317
+ return this;
1133
1318
  }
1134
1319
 
1135
- return true;
1136
- };
1320
+ openssh_noMoreSessions(cb) {
1321
+ if (!this._sock || !isWritable(this._sock))
1322
+ throw new Error('Not connected');
1323
+
1324
+ const wantReply = (typeof cb === 'function');
1325
+
1326
+ if (!this.config.strictVendor
1327
+ || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) {
1328
+ if (wantReply) {
1329
+ this._callbacks.push((had_err) => {
1330
+ if (had_err) {
1331
+ cb(had_err !== true
1332
+ ? had_err
1333
+ : new Error('Unable to disable future sessions'));
1334
+ return;
1335
+ }
1137
1336
 
1138
- function openChannel(self, type, opts, cb) {
1139
- // ask the server to open a channel for some purpose
1140
- // (e.g. session (sftp, exec, shell), or forwarding a TCP connection
1141
- var localChan = nextChannel(self);
1142
- var initWindow = Channel.MAX_WINDOW;
1143
- var maxPacket = Channel.PACKET_SIZE;
1144
- var ret = true;
1337
+ cb();
1338
+ });
1339
+ }
1145
1340
 
1146
- if (localChan === false)
1147
- return cb(new Error('No free channels available'));
1341
+ this._protocol.openssh_noMoreSessions(wantReply);
1342
+ return this;
1343
+ }
1148
1344
 
1149
- if (typeof opts === 'function') {
1150
- cb = opts;
1151
- opts = {};
1152
- }
1345
+ if (!wantReply)
1346
+ return this;
1347
+
1348
+ process.nextTick(
1349
+ cb,
1350
+ new Error(
1351
+ 'strictVendor enabled and server is not OpenSSH or compatible version'
1352
+ )
1353
+ );
1153
1354
 
1154
- self._channels[localChan] = cb;
1155
-
1156
- var sshstream = self._sshstream;
1157
- sshstream.once('CHANNEL_OPEN_CONFIRMATION:' + localChan, onSuccess)
1158
- .once('CHANNEL_OPEN_FAILURE:' + localChan, onFailure)
1159
- .once('CHANNEL_CLOSE:' + localChan, onFailure);
1160
-
1161
- if (type === 'session')
1162
- ret = sshstream.session(localChan, initWindow, maxPacket);
1163
- else if (type === 'direct-tcpip')
1164
- ret = sshstream.directTcpip(localChan, initWindow, maxPacket, opts);
1165
- else if (type === 'direct-streamlocal@openssh.com') {
1166
- ret = sshstream.openssh_directStreamLocal(localChan,
1167
- initWindow,
1168
- maxPacket,
1169
- opts);
1355
+ return this;
1170
1356
  }
1171
1357
 
1172
- return ret;
1358
+ openssh_forwardInStreamLocal(socketPath, cb) {
1359
+ if (!this._sock || !isWritable(this._sock))
1360
+ throw new Error('Not connected');
1361
+
1362
+ const wantReply = (typeof cb === 'function');
1363
+
1364
+ if (!this.config.strictVendor
1365
+ || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) {
1366
+ if (wantReply) {
1367
+ this._callbacks.push((had_err) => {
1368
+ if (had_err) {
1369
+ cb(had_err !== true
1370
+ ? had_err
1371
+ : new Error(`Unable to bind to ${socketPath}`));
1372
+ return;
1373
+ }
1374
+ this._forwardingUnix[socketPath] = true;
1375
+ cb();
1376
+ });
1377
+ }
1173
1378
 
1174
- function onSuccess(info) {
1175
- sshstream.removeListener('CHANNEL_OPEN_FAILURE:' + localChan, onFailure);
1176
- sshstream.removeListener('CHANNEL_CLOSE:' + localChan, onFailure);
1379
+ this._protocol.openssh_streamLocalForward(socketPath, wantReply);
1380
+ return this;
1381
+ }
1177
1382
 
1178
- var chaninfo = {
1179
- type: type,
1180
- incoming: {
1181
- id: localChan,
1182
- window: initWindow,
1183
- packetSize: maxPacket,
1184
- state: 'open'
1185
- },
1186
- outgoing: {
1187
- id: info.sender,
1188
- window: info.window,
1189
- packetSize: info.packetSize,
1190
- state: 'open'
1383
+ if (!wantReply)
1384
+ return this;
1385
+
1386
+ process.nextTick(
1387
+ cb,
1388
+ new Error(
1389
+ 'strictVendor enabled and server is not OpenSSH or compatible version'
1390
+ )
1391
+ );
1392
+
1393
+ return this;
1394
+ }
1395
+
1396
+ openssh_unforwardInStreamLocal(socketPath, cb) {
1397
+ if (!this._sock || !isWritable(this._sock))
1398
+ throw new Error('Not connected');
1399
+
1400
+ const wantReply = (typeof cb === 'function');
1401
+
1402
+ if (!this.config.strictVendor
1403
+ || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) {
1404
+ if (wantReply) {
1405
+ this._callbacks.push((had_err) => {
1406
+ if (had_err) {
1407
+ cb(had_err !== true
1408
+ ? had_err
1409
+ : new Error(`Unable to unbind from ${socketPath}`));
1410
+ return;
1411
+ }
1412
+ delete this._forwardingUnix[socketPath];
1413
+ cb();
1414
+ });
1191
1415
  }
1192
- };
1193
- cb(undefined, new Channel(chaninfo, self));
1416
+
1417
+ this._protocol.openssh_cancelStreamLocalForward(socketPath, wantReply);
1418
+ return this;
1419
+ }
1420
+
1421
+ if (!wantReply)
1422
+ return this;
1423
+
1424
+ process.nextTick(
1425
+ cb,
1426
+ new Error(
1427
+ 'strictVendor enabled and server is not OpenSSH or compatible version'
1428
+ )
1429
+ );
1430
+
1431
+ return this;
1194
1432
  }
1195
1433
 
1196
- function onFailure(info) {
1197
- sshstream.removeListener('CHANNEL_OPEN_CONFIRMATION:' + localChan,
1198
- onSuccess);
1199
- sshstream.removeListener('CHANNEL_OPEN_FAILURE:' + localChan, onFailure);
1200
- sshstream.removeListener('CHANNEL_CLOSE:' + localChan, onFailure);
1201
-
1202
- delete self._channels[localChan];
1203
-
1204
- var err;
1205
- if (info instanceof Error)
1206
- err = info;
1207
- else if (typeof info === 'object' && info !== null) {
1208
- err = new Error('(SSH) Channel open failure: ' + info.description);
1209
- err.reason = info.reason;
1210
- err.lang = info.lang;
1211
- } else {
1212
- err = new Error('(SSH) Channel open failure: '
1213
- + 'server closed channel unexpectedly');
1214
- err.reason = err.lang = '';
1434
+ openssh_forwardOutStreamLocal(socketPath, cb) {
1435
+ if (!this._sock || !isWritable(this._sock))
1436
+ throw new Error('Not connected');
1437
+
1438
+ if (typeof cb !== 'function')
1439
+ cb = noop;
1440
+
1441
+ if (!this.config.strictVendor
1442
+ || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) {
1443
+ openChannel(this, 'direct-streamlocal@openssh.com', { socketPath }, cb);
1444
+ return this;
1215
1445
  }
1216
- cb(err);
1446
+ process.nextTick(
1447
+ cb,
1448
+ new Error(
1449
+ 'strictVendor enabled and server is not OpenSSH or compatible version'
1450
+ )
1451
+ );
1452
+
1453
+ return this;
1454
+ }
1455
+
1456
+ sftp(cb) {
1457
+ if (!this._sock || !isWritable(this._sock))
1458
+ throw new Error('Not connected');
1459
+
1460
+ openChannel(this, 'sftp', (err, sftp) => {
1461
+ if (err) {
1462
+ cb(err);
1463
+ return;
1464
+ }
1465
+
1466
+ reqSubsystem(sftp, 'sftp', (err, sftp_) => {
1467
+ if (err) {
1468
+ cb(err);
1469
+ return;
1470
+ }
1471
+
1472
+ function removeListeners() {
1473
+ sftp.removeListener('ready', onReady);
1474
+ sftp.removeListener('error', onError);
1475
+ sftp.removeListener('exit', onExit);
1476
+ sftp.removeListener('close', onExit);
1477
+ }
1478
+
1479
+ function onReady() {
1480
+ // TODO: do not remove exit/close in case remote end closes the
1481
+ // channel abruptly and we need to notify outstanding callbacks
1482
+ removeListeners();
1483
+ cb(undefined, sftp);
1484
+ }
1485
+
1486
+ function onError(err) {
1487
+ removeListeners();
1488
+ cb(err);
1489
+ }
1490
+
1491
+ function onExit(code, signal) {
1492
+ removeListeners();
1493
+ let msg;
1494
+ if (typeof code === 'number')
1495
+ msg = `Received exit code ${code} while establishing SFTP session`;
1496
+ else if (signal !== undefined)
1497
+ msg = `Received signal ${signal} while establishing SFTP session`;
1498
+ else
1499
+ msg = 'Received unexpected SFTP session termination';
1500
+ const err = new Error(msg);
1501
+ err.code = code;
1502
+ err.signal = signal;
1503
+ cb(err);
1504
+ }
1505
+
1506
+ sftp.on('ready', onReady)
1507
+ .on('error', onError)
1508
+ .on('exit', onExit)
1509
+ .on('close', onExit);
1510
+
1511
+ sftp._init();
1512
+ });
1513
+ });
1514
+
1515
+ return this;
1217
1516
  }
1218
1517
  }
1219
1518
 
1220
- function nextChannel(self) {
1221
- // get the next available channel number
1519
+ function openChannel(self, type, opts, cb) {
1520
+ // Ask the server to open a channel for some purpose
1521
+ // (e.g. session (sftp, exec, shell), or forwarding a TCP connection
1522
+ const initWindow = MAX_WINDOW;
1523
+ const maxPacket = PACKET_SIZE;
1222
1524
 
1223
- // optimized path
1224
- if (self._curChan < MAX_CHANNEL)
1225
- return ++self._curChan;
1525
+ if (typeof opts === 'function') {
1526
+ cb = opts;
1527
+ opts = {};
1528
+ }
1529
+
1530
+ const wrapper = (err, stream) => {
1531
+ cb(err, stream);
1532
+ };
1533
+ wrapper.type = type;
1226
1534
 
1227
- // slower lookup path
1228
- for (var i = 0, channels = self._channels; i < MAX_CHANNEL; ++i)
1229
- if (!channels[i])
1230
- return i;
1535
+ const localChan = self._chanMgr.add(wrapper);
1231
1536
 
1232
- return false;
1537
+ if (localChan === -1) {
1538
+ cb(new Error('No free channels available'));
1539
+ return;
1540
+ }
1541
+
1542
+ switch (type) {
1543
+ case 'session':
1544
+ case 'sftp':
1545
+ self._protocol.session(localChan, initWindow, maxPacket);
1546
+ break;
1547
+ case 'direct-tcpip':
1548
+ self._protocol.directTcpip(localChan, initWindow, maxPacket, opts);
1549
+ break;
1550
+ case 'direct-streamlocal@openssh.com':
1551
+ self._protocol.openssh_directStreamLocal(
1552
+ localChan, initWindow, maxPacket, opts
1553
+ );
1554
+ break;
1555
+ default:
1556
+ throw new Error(`Unsupported channel type: ${type}`);
1557
+ }
1233
1558
  }
1234
1559
 
1235
1560
  function reqX11(chan, screen, cb) {
1236
- // asks server to start sending us X11 connections
1237
- var cfg = {
1561
+ // Asks server to start sending us X11 connections
1562
+ const cfg = {
1238
1563
  single: false,
1239
1564
  protocol: 'MIT-MAGIC-COOKIE-1',
1240
1565
  cookie: undefined,
@@ -1253,29 +1578,29 @@ function reqX11(chan, screen, cb) {
1253
1578
  if (typeof screen.cookie === 'string')
1254
1579
  cfg.cookie = screen.cookie;
1255
1580
  else if (Buffer.isBuffer(screen.cookie))
1256
- cfg.cookie = screen.cookie.toString('hex');
1581
+ cfg.cookie = screen.cookie.hexSlice(0, screen.cookie.length);
1257
1582
  }
1258
1583
  if (cfg.cookie === undefined)
1259
1584
  cfg.cookie = randomCookie();
1260
1585
 
1261
- var wantReply = (typeof cb === 'function');
1586
+ const wantReply = (typeof cb === 'function');
1262
1587
 
1263
1588
  if (chan.outgoing.state !== 'open') {
1264
- wantReply && cb(new Error('Channel is not open'));
1265
- return true;
1589
+ if (wantReply)
1590
+ cb(new Error('Channel is not open'));
1591
+ return;
1266
1592
  }
1267
1593
 
1268
1594
  if (wantReply) {
1269
- chan._callbacks.push(function(had_err) {
1595
+ chan._callbacks.push((had_err) => {
1270
1596
  if (had_err) {
1271
- return cb(had_err !== true
1272
- ? had_err
1273
- : new Error('Unable to request X11'));
1597
+ cb(had_err !== true ? had_err : new Error('Unable to request X11'));
1598
+ return;
1274
1599
  }
1275
1600
 
1276
1601
  chan._hasX11 = true;
1277
1602
  ++chan._client._acceptX11;
1278
- chan.once('close', function() {
1603
+ chan.once('close', () => {
1279
1604
  if (chan._client._acceptX11)
1280
1605
  --chan._client._acceptX11;
1281
1606
  });
@@ -1284,20 +1609,20 @@ function reqX11(chan, screen, cb) {
1284
1609
  });
1285
1610
  }
1286
1611
 
1287
- return chan._client._sshstream.x11Forward(chan.outgoing.id, cfg, wantReply);
1612
+ chan._client._protocol.x11Forward(chan.outgoing.id, cfg, wantReply);
1288
1613
  }
1289
1614
 
1290
1615
  function reqPty(chan, opts, cb) {
1291
- var rows = 24;
1292
- var cols = 80;
1293
- var width = 640;
1294
- var height = 480;
1295
- var term = 'vt100';
1296
- var modes = null;
1297
-
1298
- if (typeof opts === 'function')
1616
+ let rows = 24;
1617
+ let cols = 80;
1618
+ let width = 640;
1619
+ let height = 480;
1620
+ let term = 'vt100';
1621
+ let modes = null;
1622
+
1623
+ if (typeof opts === 'function') {
1299
1624
  cb = opts;
1300
- else if (typeof opts === 'object' && opts !== null) {
1625
+ } else if (typeof opts === 'object' && opts !== null) {
1301
1626
  if (typeof opts.rows === 'number')
1302
1627
  rows = opts.rows;
1303
1628
  if (typeof opts.cols === 'number')
@@ -1312,149 +1637,154 @@ function reqPty(chan, opts, cb) {
1312
1637
  modes = opts.modes;
1313
1638
  }
1314
1639
 
1315
- var wantReply = (typeof cb === 'function');
1640
+ const wantReply = (typeof cb === 'function');
1316
1641
 
1317
1642
  if (chan.outgoing.state !== 'open') {
1318
- wantReply && cb(new Error('Channel is not open'));
1319
- return true;
1643
+ if (wantReply)
1644
+ cb(new Error('Channel is not open'));
1645
+ return;
1320
1646
  }
1321
1647
 
1322
1648
  if (wantReply) {
1323
- chan._callbacks.push(function(had_err) {
1649
+ chan._callbacks.push((had_err) => {
1324
1650
  if (had_err) {
1325
- return cb(had_err !== true
1326
- ? had_err
1327
- : new Error('Unable to request a pseudo-terminal'));
1651
+ cb(had_err !== true
1652
+ ? had_err
1653
+ : new Error('Unable to request a pseudo-terminal'));
1654
+ return;
1328
1655
  }
1329
1656
  cb();
1330
1657
  });
1331
1658
  }
1332
1659
 
1333
- return chan._client._sshstream.pty(chan.outgoing.id,
1334
- rows,
1335
- cols,
1336
- height,
1337
- width,
1338
- term,
1339
- modes,
1340
- wantReply);
1660
+ chan._client._protocol.pty(chan.outgoing.id,
1661
+ rows,
1662
+ cols,
1663
+ height,
1664
+ width,
1665
+ term,
1666
+ modes,
1667
+ wantReply);
1341
1668
  }
1342
1669
 
1343
1670
  function reqAgentFwd(chan, cb) {
1344
- var wantReply = (typeof cb === 'function');
1671
+ const wantReply = (typeof cb === 'function');
1345
1672
 
1346
1673
  if (chan.outgoing.state !== 'open') {
1347
1674
  wantReply && cb(new Error('Channel is not open'));
1348
- return true;
1349
- } else if (chan._client._agentFwdEnabled) {
1675
+ return;
1676
+ }
1677
+ if (chan._client._agentFwdEnabled) {
1350
1678
  wantReply && cb(false);
1351
- return true;
1679
+ return;
1352
1680
  }
1353
1681
 
1354
1682
  chan._client._agentFwdEnabled = true;
1355
1683
 
1356
- chan._callbacks.push(function(had_err) {
1684
+ chan._callbacks.push((had_err) => {
1357
1685
  if (had_err) {
1358
1686
  chan._client._agentFwdEnabled = false;
1359
- wantReply && cb(had_err !== true
1360
- ? had_err
1361
- : new Error('Unable to request agent forwarding'));
1687
+ if (wantReply) {
1688
+ cb(had_err !== true
1689
+ ? had_err
1690
+ : new Error('Unable to request agent forwarding'));
1691
+ }
1362
1692
  return;
1363
1693
  }
1364
1694
 
1365
- wantReply && cb();
1695
+ if (wantReply)
1696
+ cb();
1366
1697
  });
1367
1698
 
1368
- return chan._client._sshstream.openssh_agentForward(chan.outgoing.id, true);
1699
+ chan._client._protocol.openssh_agentForward(chan.outgoing.id, true);
1369
1700
  }
1370
1701
 
1371
1702
  function reqShell(chan, cb) {
1372
1703
  if (chan.outgoing.state !== 'open') {
1373
1704
  cb(new Error('Channel is not open'));
1374
- return true;
1705
+ return;
1375
1706
  }
1376
- chan._callbacks.push(function(had_err) {
1707
+
1708
+ chan._callbacks.push((had_err) => {
1377
1709
  if (had_err) {
1378
- return cb(had_err !== true
1379
- ? had_err
1380
- : new Error('Unable to open shell'));
1710
+ cb(had_err !== true ? had_err : new Error('Unable to open shell'));
1711
+ return;
1381
1712
  }
1382
1713
  chan.subtype = 'shell';
1383
1714
  cb(undefined, chan);
1384
1715
  });
1385
1716
 
1386
- return chan._client._sshstream.shell(chan.outgoing.id, true);
1717
+ chan._client._protocol.shell(chan.outgoing.id, true);
1387
1718
  }
1388
1719
 
1389
1720
  function reqExec(chan, cmd, opts, cb) {
1390
1721
  if (chan.outgoing.state !== 'open') {
1391
1722
  cb(new Error('Channel is not open'));
1392
- return true;
1723
+ return;
1393
1724
  }
1394
- chan._callbacks.push(function(had_err) {
1725
+
1726
+ chan._callbacks.push((had_err) => {
1395
1727
  if (had_err) {
1396
- return cb(had_err !== true
1397
- ? had_err
1398
- : new Error('Unable to exec'));
1728
+ cb(had_err !== true ? had_err : new Error('Unable to exec'));
1729
+ return;
1399
1730
  }
1400
1731
  chan.subtype = 'exec';
1401
1732
  chan.allowHalfOpen = (opts.allowHalfOpen !== false);
1402
1733
  cb(undefined, chan);
1403
1734
  });
1404
1735
 
1405
- return chan._client._sshstream.exec(chan.outgoing.id, cmd, true);
1736
+ chan._client._protocol.exec(chan.outgoing.id, cmd, true);
1406
1737
  }
1407
1738
 
1408
1739
  function reqEnv(chan, env) {
1409
1740
  if (chan.outgoing.state !== 'open')
1410
- return true;
1411
- var ret = true;
1412
- var keys = Object.keys(env || {});
1413
- var key;
1414
- var val;
1415
-
1416
- for (var i = 0, len = keys.length; i < len; ++i) {
1417
- key = keys[i];
1418
- val = env[key];
1419
- ret = chan._client._sshstream.env(chan.outgoing.id, key, val, false);
1420
- }
1741
+ return;
1742
+
1743
+ const keys = Object.keys(env || {});
1421
1744
 
1422
- return ret;
1745
+ for (let i = 0; i < keys.length; ++i) {
1746
+ const key = keys[i];
1747
+ const val = env[key];
1748
+ chan._client._protocol.env(chan.outgoing.id, key, val, false);
1749
+ }
1423
1750
  }
1424
1751
 
1425
1752
  function reqSubsystem(chan, name, cb) {
1426
1753
  if (chan.outgoing.state !== 'open') {
1427
1754
  cb(new Error('Channel is not open'));
1428
- return true;
1755
+ return;
1429
1756
  }
1430
- chan._callbacks.push(function(had_err) {
1757
+
1758
+ chan._callbacks.push((had_err) => {
1431
1759
  if (had_err) {
1432
- return cb(had_err !== true
1433
- ? had_err
1434
- : new Error('Unable to start subsystem: ' + name));
1760
+ cb(had_err !== true
1761
+ ? had_err
1762
+ : new Error(`Unable to start subsystem: ${name}`));
1763
+ return;
1435
1764
  }
1436
1765
  chan.subtype = 'subsystem';
1437
1766
  cb(undefined, chan);
1438
1767
  });
1439
1768
 
1440
- return chan._client._sshstream.subsystem(chan.outgoing.id, name, true);
1769
+ chan._client._protocol.subsystem(chan.outgoing.id, name, true);
1441
1770
  }
1442
1771
 
1772
+ // TODO: inline implementation into single call site
1443
1773
  function onCHANNEL_OPEN(self, info) {
1444
- // the server is trying to open a channel with us, this is usually when
1774
+ // The server is trying to open a channel with us, this is usually when
1445
1775
  // we asked the server to forward us connections on some port and now they
1446
1776
  // are asking us to accept/deny an incoming connection on their side
1447
1777
 
1448
- var localChan = false;
1449
- var reason;
1778
+ let localChan = -1;
1779
+ let reason;
1450
1780
 
1451
- function accept() {
1452
- var chaninfo = {
1781
+ const accept = () => {
1782
+ const chanInfo = {
1453
1783
  type: info.type,
1454
1784
  incoming: {
1455
1785
  id: localChan,
1456
- window: Channel.MAX_WINDOW,
1457
- packetSize: Channel.PACKET_SIZE,
1786
+ window: MAX_WINDOW,
1787
+ packetSize: PACKET_SIZE,
1458
1788
  state: 'open'
1459
1789
  },
1460
1790
  outgoing: {
@@ -1464,111 +1794,221 @@ function onCHANNEL_OPEN(self, info) {
1464
1794
  state: 'open'
1465
1795
  }
1466
1796
  };
1467
- var stream = new Channel(chaninfo, self);
1797
+ const stream = new Channel(self, chanInfo);
1798
+ self._chanMgr.update(localChan, stream);
1468
1799
 
1469
- self._sshstream.channelOpenConfirm(info.sender,
1470
- localChan,
1471
- Channel.MAX_WINDOW,
1472
- Channel.PACKET_SIZE);
1800
+ self._protocol.channelOpenConfirm(info.sender,
1801
+ localChan,
1802
+ MAX_WINDOW,
1803
+ PACKET_SIZE);
1473
1804
  return stream;
1474
- }
1475
- function reject() {
1805
+ };
1806
+ const reject = () => {
1476
1807
  if (reason === undefined) {
1477
- if (localChan === false)
1478
- reason = consts.CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE;
1808
+ if (localChan === -1)
1809
+ reason = CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE;
1479
1810
  else
1480
- reason = consts.CHANNEL_OPEN_FAILURE.CONNECT_FAILED;
1811
+ reason = CHANNEL_OPEN_FAILURE.CONNECT_FAILED;
1481
1812
  }
1482
1813
 
1483
- self._sshstream.channelOpenFail(info.sender, reason, '', '');
1484
- }
1814
+ if (localChan !== -1)
1815
+ self._chanMgr.remove(localChan);
1485
1816
 
1486
- if (info.type === 'forwarded-tcpip'
1487
- || info.type === 'x11'
1488
- || info.type === 'auth-agent@openssh.com'
1489
- || info.type === 'forwarded-streamlocal@openssh.com') {
1490
-
1491
- // check for conditions for automatic rejection
1492
- var rejectConn = (
1493
- (info.type === 'forwarded-tcpip'
1494
- && self._forwarding[info.data.destIP
1495
- + ':'
1496
- + info.data.destPort] === undefined)
1497
- || (info.type === 'forwarded-streamlocal@openssh.com'
1498
- && self._forwardingUnix[info.data.socketPath] === undefined)
1499
- || (info.type === 'x11' && self._acceptX11 === 0)
1500
- || (info.type === 'auth-agent@openssh.com'
1501
- && !self._agentFwdEnabled)
1502
- );
1817
+ self._protocol.channelOpenFail(info.sender, reason, '');
1818
+ };
1819
+ const reserveChannel = () => {
1820
+ localChan = self._chanMgr.add();
1821
+
1822
+ if (localChan === -1) {
1823
+ reason = CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE;
1824
+ if (self.config.debug) {
1825
+ self.config.debug(
1826
+ 'Client: Automatic rejection of incoming channel open: '
1827
+ + 'no channels available'
1828
+ );
1829
+ }
1830
+ }
1503
1831
 
1504
- if (!rejectConn) {
1505
- localChan = nextChannel(self);
1832
+ return (localChan !== -1);
1833
+ };
1506
1834
 
1507
- if (localChan === false) {
1508
- self.config.debug('DEBUG: Client: Automatic rejection of incoming channel open: no channels available');
1509
- rejectConn = true;
1510
- } else
1511
- self._channels[localChan] = true;
1512
- } else {
1513
- reason = consts.CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED;
1514
- self.config.debug('DEBUG: Client: Automatic rejection of incoming channel open: unexpected channel open for: '
1515
- + info.type);
1835
+ const data = info.data;
1836
+ switch (info.type) {
1837
+ case 'forwarded-tcpip': {
1838
+ const val = self._forwarding[`${data.destIP}:${data.destPort}`];
1839
+ if (val !== undefined && reserveChannel()) {
1840
+ if (data.destPort === 0)
1841
+ data.destPort = val;
1842
+ self.emit('tcp connection', data, accept, reject);
1843
+ return;
1844
+ }
1845
+ break;
1516
1846
  }
1847
+ case 'forwarded-streamlocal@openssh.com':
1848
+ if (self._forwardingUnix[data.socketPath] !== undefined
1849
+ && reserveChannel()) {
1850
+ self.emit('unix connection', data, accept, reject);
1851
+ return;
1852
+ }
1853
+ break;
1854
+ case 'auth-agent@openssh.com':
1855
+ if (self._agentFwdEnabled
1856
+ && typeof self._agent.getStream === 'function'
1857
+ && reserveChannel()) {
1858
+ self._agent.getStream((err, stream) => {
1859
+ if (err)
1860
+ return reject();
1861
+
1862
+ const upstream = accept();
1863
+ upstream.pipe(stream).pipe(upstream);
1864
+ });
1865
+ return;
1866
+ }
1867
+ break;
1868
+ case 'x11':
1869
+ if (self._acceptX11 !== 0 && reserveChannel()) {
1870
+ self.emit('x11', data, accept, reject);
1871
+ return;
1872
+ }
1873
+ break;
1874
+ default:
1875
+ // Automatically reject any unsupported channel open requests
1876
+ reason = CHANNEL_OPEN_FAILURE.UNKNOWN_CHANNEL_TYPE;
1877
+ if (self.config.debug) {
1878
+ self.config.debug(
1879
+ 'Client: Automatic rejection of unsupported incoming channel open '
1880
+ + `type: ${info.type}`
1881
+ );
1882
+ }
1883
+ }
1884
+
1885
+ if (reason === undefined) {
1886
+ reason = CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED;
1887
+ if (self.config.debug) {
1888
+ self.config.debug(
1889
+ 'Client: Automatic rejection of unexpected incoming channel open for: '
1890
+ + info.type
1891
+ );
1892
+ }
1893
+ }
1894
+
1895
+ reject();
1896
+ }
1897
+
1898
+ const randomCookie = (() => {
1899
+ const buffer = Buffer.allocUnsafe(16);
1900
+ return () => {
1901
+ randomFillSync(buffer, 0, 16);
1902
+ return buffer.hexSlice(0, 16);
1903
+ };
1904
+ })();
1905
+
1906
+ function makeSimpleAuthHandler(authList) {
1907
+ if (!Array.isArray(authList))
1908
+ throw new Error('authList must be an array');
1517
1909
 
1518
- // TODO: automatic rejection after some timeout?
1910
+ let a = 0;
1911
+ return (authsLeft, partialSuccess, cb) => {
1912
+ if (a === authList.length)
1913
+ return false;
1914
+ return authList[a++];
1915
+ };
1916
+ }
1917
+
1918
+ function hostKeysProve(client, keys_, cb) {
1919
+ if (!client._sock || !isWritable(client._sock))
1920
+ return;
1519
1921
 
1520
- if (rejectConn)
1521
- reject();
1922
+ if (typeof cb !== 'function')
1923
+ cb = noop;
1522
1924
 
1523
- if (localChan !== false) {
1524
- if (info.type === 'forwarded-tcpip') {
1525
- if (info.data.destPort === 0) {
1526
- info.data.destPort = self._forwarding[info.data.destIP
1527
- + ':'
1528
- + info.data.destPort];
1925
+ if (!Array.isArray(keys_))
1926
+ throw new TypeError('Invalid keys argument type');
1927
+
1928
+ const keys = [];
1929
+ for (const key of keys_) {
1930
+ const parsed = parseKey(key);
1931
+ if (parsed instanceof Error)
1932
+ throw parsed;
1933
+ keys.push(parsed);
1934
+ }
1935
+
1936
+ if (!client.config.strictVendor
1937
+ || (client.config.strictVendor && RE_OPENSSH.test(client._remoteVer))) {
1938
+ client._callbacks.push((had_err, data) => {
1939
+ if (had_err) {
1940
+ cb(had_err !== true
1941
+ ? had_err
1942
+ : new Error('Server failed to prove supplied keys'));
1943
+ return;
1944
+ }
1945
+
1946
+ // TODO: move all of this parsing/verifying logic out of the client?
1947
+ const ret = [];
1948
+ let keyIdx = 0;
1949
+ bufferParser.init(data, 0);
1950
+ while (bufferParser.avail()) {
1951
+ if (keyIdx === keys.length)
1952
+ break;
1953
+ const key = keys[keyIdx++];
1954
+ const keyPublic = key.getPublicSSH();
1955
+
1956
+ const sigEntry = bufferParser.readString();
1957
+ sigParser.init(sigEntry, 0);
1958
+ const type = sigParser.readString(true);
1959
+ let value = sigParser.readString();
1960
+
1961
+ let algo;
1962
+ if (type !== key.type) {
1963
+ if (key.type === 'ssh-rsa') {
1964
+ switch (type) {
1965
+ case 'rsa-sha2-256':
1966
+ algo = 'sha256';
1967
+ break;
1968
+ case 'rsa-sha2-512':
1969
+ algo = 'sha512';
1970
+ break;
1971
+ default:
1972
+ continue;
1973
+ }
1974
+ } else {
1975
+ continue;
1976
+ }
1529
1977
  }
1530
- self.emit('tcp connection', info.data, accept, reject);
1531
- } else if (info.type === 'x11') {
1532
- self.emit('x11', info.data, accept, reject);
1533
- } else if (info.type === 'forwarded-streamlocal@openssh.com') {
1534
- self.emit('unix connection', info.data, accept, reject);
1535
- } else {
1536
- agentQuery(self.config.agent, accept, reject);
1978
+
1979
+ const sessionID = client._protocol._kex.sessionID;
1980
+ const verifyData = Buffer.allocUnsafe(
1981
+ 4 + 29 + 4 + sessionID.length + 4 + keyPublic.length
1982
+ );
1983
+ let p = 0;
1984
+ writeUInt32BE(verifyData, 29, p);
1985
+ verifyData.utf8Write('hostkeys-prove-00@openssh.com', p += 4, 29);
1986
+ writeUInt32BE(verifyData, sessionID.length, p += 29);
1987
+ bufferCopy(sessionID, verifyData, 0, sessionID.length, p += 4);
1988
+ writeUInt32BE(verifyData, keyPublic.length, p += sessionID.length);
1989
+ bufferCopy(keyPublic, verifyData, 0, keyPublic.length, p += 4);
1990
+
1991
+ if (!(value = sigSSHToASN1(value, type)))
1992
+ continue;
1993
+ if (key.verify(verifyData, value, algo) === true)
1994
+ ret.push(key);
1537
1995
  }
1538
- }
1539
- } else {
1540
- // automatically reject any unsupported channel open requests
1541
- self.config.debug('DEBUG: Client: Automatic rejection of incoming channel open: unsupported type: '
1542
- + info.type);
1543
- reason = consts.CHANNEL_OPEN_FAILURE.UNKNOWN_CHANNEL_TYPE;
1544
- reject();
1545
- }
1546
- }
1996
+ sigParser.clear();
1997
+ bufferParser.clear();
1547
1998
 
1548
- var randomCookie = (function() {
1549
- if (typeof crypto.randomFillSync === 'function') {
1550
- var buffer = Buffer.alloc(16);
1551
- return function randomCookie() {
1552
- crypto.randomFillSync(buffer, 0, 16);
1553
- return buffer.toString('hex');
1554
- };
1555
- } else {
1556
- return function randomCookie() {
1557
- return crypto.randomBytes(16).toString('hex');
1558
- };
1999
+ cb(null, ret);
2000
+ });
2001
+
2002
+ client._protocol.openssh_hostKeysProve(keys);
2003
+ return;
1559
2004
  }
1560
- })();
1561
2005
 
1562
- Client.Client = Client;
1563
- Client.Server = require('./server');
1564
- // pass some useful utilities on to end user (e.g. parseKey())
1565
- Client.utils = ssh2_streams.utils;
1566
- // expose useful SFTPStream constants for sftp server usage
1567
- Client.SFTP_STATUS_CODE = SFTPStream.STATUS_CODE;
1568
- Client.SFTP_OPEN_MODE = SFTPStream.OPEN_MODE;
1569
- // expose http(s).Agent implementations to allow easy tunneling of HTTP(S)
1570
- // requests
1571
- Client.HTTPAgent = HTTPAgents.SSHTTPAgent;
1572
- Client.HTTPSAgent = HTTPAgents.SSHTTPSAgent;
1573
-
1574
- module.exports = Client; // backwards compatibility
2006
+ process.nextTick(
2007
+ cb,
2008
+ new Error(
2009
+ 'strictVendor enabled and server is not OpenSSH or compatible version'
2010
+ )
2011
+ );
2012
+ }
2013
+
2014
+ module.exports = Client;