UNPKG

58.7 kBJavaScriptView Raw
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
7const {
8 createHash,
9 getHashes,
10 randomFillSync,
11} = require('crypto');
12const { Socket } = require('net');
13const { lookup: dnsLookup } = require('dns');
14const EventEmitter = require('events');
15const HASHES = getHashes();
16
17const {
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');
34const { init: cryptoInit } = require('./protocol/crypto.js');
35const Protocol = require('./protocol/Protocol.js');
36const { parseKey } = require('./protocol/keyParser.js');
37const { SFTP } = require('./protocol/SFTP.js');
38const {
39 bufferCopy,
40 makeBufferParser,
41 makeError,
42 readUInt32BE,
43 sigSSHToASN1,
44 writeUInt32BE,
45} = require('./protocol/utils.js');
46
47const { AgentContext, createAgent, isAgent } = require('./agent.js');
48const {
49 Channel,
50 MAX_WINDOW,
51 PACKET_SIZE,
52 windowAdjust,
53 WINDOW_THRESHOLD,
54} = require('./Channel.js');
55const {
56 ChannelManager,
57 generateAlgorithmList,
58 isWritable,
59 onChannelOpenFailure,
60 onCHANNEL_CLOSE,
61} = require('./utils.js');
62
63const bufferParser = makeBufferParser();
64const sigParser = makeBufferParser();
65const RE_OPENSSH = /^OpenSSH_(?:(?![0-4])\d)|(?:\d{2,})/;
66const noop = (err) => {};
67
68class 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 };
97
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;
111 }
112
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;
120 }
121
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;
196 }
197
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');
204
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 );
233
234 this.config.strictVendor = (typeof cfg.strictVendor === 'boolean'
235 ? cfg.strictVendor
236 : true);
237
238 const debug = this.config.debug = (typeof cfg.debug === 'function'
239 ? cfg.debug
240 : undefined);
241
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 }
247
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 );
270 }
271 }
272
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);
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 };
290 }
291
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)}`);
299 });
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');
328 }
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();
367 }
368
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(
431 name,
432 instructions,
433 '',
434 prompts,
435 (answers) => {
436 proto.authInfoRes(answers);
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'
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;
528 }
529
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;
536
537 const channel = this._chanMgr.get(recipient);
538 if (typeof channel !== 'object' || channel === null)
539 return;
540
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;
546
547 channel.incoming.window -= data.length;
548
549 if (!channel.stderr.push(data)) {
550 channel._waitChanDrain = true;
551 return;
552 }
553
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 }
621
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 },
646 });
647
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;
684
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 sock.on('data', (data) => {
711 try {
712 proto.parse(data, 0, data.length);
713 } catch (ex) {
714 this.emit('error', ex);
715 try {
716 if (isWritable(sock))
717 sock.end();
718 } catch {}
719 }
720 });
721
722 // Drain stderr if we are connection hopping using an exec stream
723 if (sock.stderr && typeof sock.stderr.resume === 'function')
724 sock.stderr.resume();
725
726 sock.resume();
727 }).catch((err) => {
728 this.emit('error', err);
729 try {
730 if (isWritable(sock))
731 sock.end();
732 } catch {}
733 });
734 };
735 })();
736 let wasConnected = false;
737 sock.on('connect', onConnect)
738 .on('timeout', () => {
739 this.emit('timeout');
740 }).on('error', (err) => {
741 debug && debug(`Socket error: ${err.message}`);
742 clearTimeout(this._readyTimeout);
743 err.level = 'client-socket';
744 this.emit('error', err);
745 }).on('end', () => {
746 debug && debug('Socket ended');
747 onDone();
748 proto.cleanup();
749 clearTimeout(this._readyTimeout);
750 clearInterval(katimer);
751 this.emit('end');
752 }).on('close', () => {
753 debug && debug('Socket closed');
754 onDone();
755 proto.cleanup();
756 clearTimeout(this._readyTimeout);
757 clearInterval(katimer);
758 this.emit('close');
759
760 // Notify outstanding channel requests of disconnection ...
761 const callbacks_ = callbacks;
762 callbacks = this._callbacks = [];
763 const err = new Error('No response from server');
764 for (let i = 0; i < callbacks_.length; ++i)
765 callbacks_[i](err);
766
767 // Simulate error for any channels waiting to be opened
768 this._chanMgr.cleanup(err);
769 });
770
771 // Begin authentication handling ===========================================
772 let curAuth;
773 let curPartial = null;
774 let curAuthsLeft = null;
775 const authsAllowed = ['none'];
776 if (this.config.password !== undefined)
777 authsAllowed.push('password');
778 if (privateKey !== undefined)
779 authsAllowed.push('publickey');
780 if (this._agent !== undefined)
781 authsAllowed.push('agent');
782 if (this.config.tryKeyboard)
783 authsAllowed.push('keyboard-interactive');
784 if (privateKey !== undefined
785 && this.config.localHostname !== undefined
786 && this.config.localUsername !== undefined) {
787 authsAllowed.push('hostbased');
788 }
789
790 if (Array.isArray(authHandler))
791 authHandler = makeSimpleAuthHandler(authHandler);
792 else if (typeof authHandler !== 'function')
793 authHandler = makeSimpleAuthHandler(authsAllowed);
794
795 let hasSentAuth = false;
796 const doNextAuth = (nextAuth) => {
797 if (hasSentAuth)
798 return;
799 hasSentAuth = true;
800
801 if (nextAuth === false) {
802 const err = new Error('All configured authentication methods failed');
803 err.level = 'client-authentication';
804 this.emit('error', err);
805 this.end();
806 return;
807 }
808
809 if (typeof nextAuth === 'string') {
810 // Remain backwards compatible with original `authHandler()` usage,
811 // which only supported passing names of next method to try using data
812 // from the `connect()` config object
813
814 const type = nextAuth;
815 if (authsAllowed.indexOf(type) === -1)
816 return skipAuth(`Authentication method not allowed: ${type}`);
817
818 const username = this.config.username;
819 switch (type) {
820 case 'password':
821 nextAuth = { type, username, password: this.config.password };
822 break;
823 case 'publickey':
824 nextAuth = { type, username, key: privateKey };
825 break;
826 case 'hostbased':
827 nextAuth = {
828 type,
829 username,
830 key: privateKey,
831 localHostname: this.config.localHostname,
832 localUsername: this.config.localUsername,
833 };
834 break;
835 case 'agent':
836 nextAuth = {
837 type,
838 username,
839 agentCtx: new AgentContext(this._agent),
840 };
841 break;
842 case 'keyboard-interactive':
843 nextAuth = {
844 type,
845 username,
846 prompt: (...args) => this.emit('keyboard-interactive', ...args),
847 };
848 break;
849 case 'none':
850 nextAuth = { type, username };
851 break;
852 default:
853 return skipAuth(
854 `Skipping unsupported authentication method: ${nextAuth}`
855 );
856 }
857 } else if (typeof nextAuth !== 'object' || nextAuth === null) {
858 return skipAuth(
859 `Skipping invalid authentication attempt: ${nextAuth}`
860 );
861 } else {
862 const username = nextAuth.username;
863 if (typeof username !== 'string') {
864 return skipAuth(
865 `Skipping invalid authentication attempt: ${nextAuth}`
866 );
867 }
868 const type = nextAuth.type;
869 switch (type) {
870 case 'password': {
871 const { password } = nextAuth;
872 if (typeof password !== 'string' && !Buffer.isBuffer(password))
873 return skipAuth('Skipping invalid password auth attempt');
874 nextAuth = { type, username, password };
875 break;
876 }
877 case 'publickey': {
878 const key = parseKey(nextAuth.key, nextAuth.passphrase);
879 if (key instanceof Error)
880 return skipAuth('Skipping invalid key auth attempt');
881 if (!key.isPrivateKey())
882 return skipAuth('Skipping non-private key');
883 nextAuth = { type, username, key };
884 break;
885 }
886 case 'hostbased': {
887 const { localHostname, localUsername } = nextAuth;
888 const key = parseKey(nextAuth.key, nextAuth.passphrase);
889 if (key instanceof Error
890 || typeof localHostname !== 'string'
891 || typeof localUsername !== 'string') {
892 return skipAuth('Skipping invalid hostbased auth attempt');
893 }
894 if (!key.isPrivateKey())
895 return skipAuth('Skipping non-private key');
896 nextAuth = { type, username, key, localHostname, localUsername };
897 break;
898 }
899 case 'agent': {
900 let agent = nextAuth.agent;
901 if (typeof agent === 'string' && agent.length) {
902 agent = createAgent(agent);
903 } else if (!isAgent(agent)) {
904 return skipAuth(
905 `Skipping invalid agent: ${nextAuth.agent}`
906 );
907 }
908 nextAuth = { type, username, agentCtx: new AgentContext(agent) };
909 break;
910 }
911 case 'keyboard-interactive': {
912 const { prompt } = nextAuth;
913 if (typeof prompt !== 'function') {
914 return skipAuth(
915 'Skipping invalid keyboard-interactive auth attempt'
916 );
917 }
918 nextAuth = { type, username, prompt };
919 break;
920 }
921 case 'none':
922 nextAuth = { type, username };
923 break;
924 default:
925 return skipAuth(
926 `Skipping unsupported authentication method: ${nextAuth}`
927 );
928 }
929 }
930 curAuth = nextAuth;
931
932 // Begin authentication method's process
933 try {
934 const username = curAuth.username;
935 switch (curAuth.type) {
936 case 'password':
937 proto.authPassword(username, curAuth.password);
938 break;
939 case 'publickey':
940 proto.authPK(username, curAuth.key);
941 break;
942 case 'hostbased':
943 proto.authHostbased(username,
944 curAuth.key,
945 curAuth.localHostname,
946 curAuth.localUsername,
947 (buf, cb) => {
948 const signature = curAuth.key.sign(buf);
949 if (signature instanceof Error) {
950 signature.message =
951 `Error while signing with key: ${signature.message}`;
952 signature.level = 'client-authentication';
953 this.emit('error', signature);
954 return tryNextAuth();
955 }
956
957 cb(signature);
958 });
959 break;
960 case 'agent':
961 curAuth.agentCtx.init((err) => {
962 if (err) {
963 err.level = 'agent';
964 this.emit('error', err);
965 return tryNextAuth();
966 }
967 tryNextAgentKey();
968 });
969 break;
970 case 'keyboard-interactive':
971 proto.authKeyboard(username);
972 break;
973 case 'none':
974 proto.authNone(username);
975 break;
976 }
977 } finally {
978 hasSentAuth = false;
979 }
980 };
981
982 function skipAuth(msg) {
983 debug && debug(msg);
984 process.nextTick(tryNextAuth);
985 }
986
987 function tryNextAuth() {
988 hasSentAuth = false;
989 const auth = authHandler(curAuthsLeft, curPartial, doNextAuth);
990 if (hasSentAuth || auth === undefined)
991 return;
992 doNextAuth(auth);
993 }
994
995 const tryNextAgentKey = () => {
996 if (curAuth.type === 'agent') {
997 const key = curAuth.agentCtx.nextKey();
998 if (key === false) {
999 debug && debug('Agent: No more keys left to try');
1000 debug && debug('Client: agent auth failed');
1001 tryNextAuth();
1002 } else {
1003 const pos = curAuth.agentCtx.pos();
1004 debug && debug(`Agent: Trying key #${pos + 1}`);
1005 proto.authPK(curAuth.username, key);
1006 }
1007 }
1008 };
1009
1010 const startTimeout = () => {
1011 if (this.config.readyTimeout > 0) {
1012 this._readyTimeout = setTimeout(() => {
1013 const err = new Error('Timed out while waiting for handshake');
1014 err.level = 'client-timeout';
1015 this.emit('error', err);
1016 sock.destroy();
1017 }, this.config.readyTimeout);
1018 }
1019 };
1020
1021 if (!cfg.sock) {
1022 let host = this.config.host;
1023 const forceIPv4 = this.config.forceIPv4;
1024 const forceIPv6 = this.config.forceIPv6;
1025
1026 debug && debug(`Client: Trying ${host} on port ${this.config.port} ...`);
1027
1028 const doConnect = () => {
1029 startTimeout();
1030 sock.connect({
1031 host,
1032 port: this.config.port,
1033 localAddress: this.config.localAddress,
1034 localPort: this.config.localPort
1035 });
1036 sock.setNoDelay(true);
1037 sock.setMaxListeners(0);
1038 sock.setTimeout(typeof cfg.timeout === 'number' ? cfg.timeout : 0);
1039 };
1040
1041 if ((!forceIPv4 && !forceIPv6) || (forceIPv4 && forceIPv6)) {
1042 doConnect();
1043 } else {
1044 dnsLookup(host, (forceIPv4 ? 4 : 6), (err, address, family) => {
1045 if (err) {
1046 const type = (forceIPv4 ? 'IPv4' : 'IPv6');
1047 const error = new Error(
1048 `Error while looking up ${type} address for '${host}': ${err}`
1049 );
1050 clearTimeout(this._readyTimeout);
1051 error.level = 'client-dns';
1052 this.emit('error', error);
1053 this.emit('close');
1054 return;
1055 }
1056 host = address;
1057 doConnect();
1058 });
1059 }
1060 } else {
1061 // Custom socket passed in
1062 startTimeout();
1063 if (typeof sock.connecting === 'boolean') {
1064 // net.Socket
1065
1066 if (!sock.connecting) {
1067 // Already connected
1068 onConnect();
1069 }
1070 } else {
1071 // Assume socket/stream is already "connected"
1072 onConnect();
1073 }
1074 }
1075
1076 return this;
1077 }
1078
1079 end() {
1080 if (this._sock && isWritable(this._sock)) {
1081 this._protocol.disconnect(DISCONNECT_REASON.BY_APPLICATION);
1082 this._sock.end();
1083 }
1084 return this;
1085 }
1086
1087 destroy() {
1088 this._sock && isWritable(this._sock) && this._sock.destroy();
1089 return this;
1090 }
1091
1092 exec(cmd, opts, cb) {
1093 if (!this._sock || !isWritable(this._sock))
1094 throw new Error('Not connected');
1095
1096 if (typeof opts === 'function') {
1097 cb = opts;
1098 opts = {};
1099 }
1100
1101 const extraOpts = { allowHalfOpen: (opts.allowHalfOpen !== false) };
1102
1103 openChannel(this, 'session', extraOpts, (err, chan) => {
1104 if (err) {
1105 cb(err);
1106 return;
1107 }
1108
1109 const todo = [];
1110
1111 function reqCb(err) {
1112 if (err) {
1113 chan.close();
1114 cb(err);
1115 return;
1116 }
1117 if (todo.length)
1118 todo.shift()();
1119 }
1120
1121 if (this.config.allowAgentFwd === true
1122 || (opts
1123 && opts.agentForward === true
1124 && this._agent !== undefined)) {
1125 todo.push(() => reqAgentFwd(chan, reqCb));
1126 }
1127
1128 if (typeof opts === 'object' && opts !== null) {
1129 if (typeof opts.env === 'object' && opts.env !== null)
1130 reqEnv(chan, opts.env);
1131 if ((typeof opts.pty === 'object' && opts.pty !== null)
1132 || opts.pty === true) {
1133 todo.push(() => reqPty(chan, opts.pty, reqCb));
1134 }
1135 if ((typeof opts.x11 === 'object' && opts.x11 !== null)
1136 || opts.x11 === 'number'
1137 || opts.x11 === true) {
1138 todo.push(() => reqX11(chan, opts.x11, reqCb));
1139 }
1140 }
1141
1142 todo.push(() => reqExec(chan, cmd, opts, cb));
1143 todo.shift()();
1144 });
1145
1146 return this;
1147 }
1148
1149 shell(wndopts, opts, cb) {
1150 if (!this._sock || !isWritable(this._sock))
1151 throw new Error('Not connected');
1152
1153 if (typeof wndopts === 'function') {
1154 cb = wndopts;
1155 wndopts = opts = undefined;
1156 } else if (typeof opts === 'function') {
1157 cb = opts;
1158 opts = undefined;
1159 }
1160 if (wndopts && (wndopts.x11 !== undefined || wndopts.env !== undefined)) {
1161 opts = wndopts;
1162 wndopts = undefined;
1163 }
1164
1165 openChannel(this, 'session', (err, chan) => {
1166 if (err) {
1167 cb(err);
1168 return;
1169 }
1170
1171 const todo = [];
1172
1173 function reqCb(err) {
1174 if (err) {
1175 chan.close();
1176 cb(err);
1177 return;
1178 }
1179 if (todo.length)
1180 todo.shift()();
1181 }
1182
1183 if (this.config.allowAgentFwd === true
1184 || (opts
1185 && opts.agentForward === true
1186 && this._agent !== undefined)) {
1187 todo.push(() => reqAgentFwd(chan, reqCb));
1188 }
1189
1190 if (wndopts !== false)
1191 todo.push(() => reqPty(chan, wndopts, reqCb));
1192
1193 if (typeof opts === 'object' && opts !== null) {
1194 if (typeof opts.env === 'object' && opts.env !== null)
1195 reqEnv(chan, opts.env);
1196 if ((typeof opts.x11 === 'object' && opts.x11 !== null)
1197 || opts.x11 === 'number'
1198 || opts.x11 === true) {
1199 todo.push(() => reqX11(chan, opts.x11, reqCb));
1200 }
1201 }
1202
1203 todo.push(() => reqShell(chan, cb));
1204 todo.shift()();
1205 });
1206
1207 return this;
1208 }
1209
1210 subsys(name, cb) {
1211 if (!this._sock || !isWritable(this._sock))
1212 throw new Error('Not connected');
1213
1214 openChannel(this, 'session', (err, chan) => {
1215 if (err) {
1216 cb(err);
1217 return;
1218 }
1219
1220 reqSubsystem(chan, name, (err, stream) => {
1221 if (err) {
1222 cb(err);
1223 return;
1224 }
1225
1226 cb(undefined, stream);
1227 });
1228 });
1229
1230 return this;
1231 }
1232
1233 forwardIn(bindAddr, bindPort, cb) {
1234 if (!this._sock || !isWritable(this._sock))
1235 throw new Error('Not connected');
1236
1237 // Send a request for the server to start forwarding TCP connections to us
1238 // on a particular address and port
1239
1240 const wantReply = (typeof cb === 'function');
1241
1242 if (wantReply) {
1243 this._callbacks.push((had_err, data) => {
1244 if (had_err) {
1245 cb(had_err !== true
1246 ? had_err
1247 : new Error(`Unable to bind to ${bindAddr}:${bindPort}`));
1248 return;
1249 }
1250
1251 let realPort = bindPort;
1252 if (bindPort === 0 && data && data.length >= 4) {
1253 realPort = readUInt32BE(data, 0);
1254 if (!(this._protocol._compatFlags & COMPAT.DYN_RPORT_BUG))
1255 bindPort = realPort;
1256 }
1257
1258 this._forwarding[`${bindAddr}:${bindPort}`] = realPort;
1259
1260 cb(undefined, realPort);
1261 });
1262 }
1263
1264 this._protocol.tcpipForward(bindAddr, bindPort, wantReply);
1265
1266 return this;
1267 }
1268
1269 unforwardIn(bindAddr, bindPort, cb) {
1270 if (!this._sock || !isWritable(this._sock))
1271 throw new Error('Not connected');
1272
1273 // Send a request to stop forwarding us new connections for a particular
1274 // address and port
1275
1276 const wantReply = (typeof cb === 'function');
1277
1278 if (wantReply) {
1279 this._callbacks.push((had_err) => {
1280 if (had_err) {
1281 cb(had_err !== true
1282 ? had_err
1283 : new Error(`Unable to unbind from ${bindAddr}:${bindPort}`));
1284 return;
1285 }
1286
1287 delete this._forwarding[`${bindAddr}:${bindPort}`];
1288
1289 cb();
1290 });
1291 }
1292
1293 this._protocol.cancelTcpipForward(bindAddr, bindPort, wantReply);
1294
1295 return this;
1296 }
1297
1298 forwardOut(srcIP, srcPort, dstIP, dstPort, cb) {
1299 if (!this._sock || !isWritable(this._sock))
1300 throw new Error('Not connected');
1301
1302 // Send a request to forward a TCP connection to the server
1303
1304 const cfg = {
1305 srcIP: srcIP,
1306 srcPort: srcPort,
1307 dstIP: dstIP,
1308 dstPort: dstPort
1309 };
1310
1311 if (typeof cb !== 'function')
1312 cb = noop;
1313
1314 openChannel(this, 'direct-tcpip', cfg, cb);
1315
1316 return this;
1317 }
1318
1319 openssh_noMoreSessions(cb) {
1320 if (!this._sock || !isWritable(this._sock))
1321 throw new Error('Not connected');
1322
1323 const wantReply = (typeof cb === 'function');
1324
1325 if (!this.config.strictVendor
1326 || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) {
1327 if (wantReply) {
1328 this._callbacks.push((had_err) => {
1329 if (had_err) {
1330 cb(had_err !== true
1331 ? had_err
1332 : new Error('Unable to disable future sessions'));
1333 return;
1334 }
1335
1336 cb();
1337 });
1338 }
1339
1340 this._protocol.openssh_noMoreSessions(wantReply);
1341 return this;
1342 }
1343
1344 if (!wantReply)
1345 return this;
1346
1347 process.nextTick(
1348 cb,
1349 new Error(
1350 'strictVendor enabled and server is not OpenSSH or compatible version'
1351 )
1352 );
1353
1354 return this;
1355 }
1356
1357 openssh_forwardInStreamLocal(socketPath, cb) {
1358 if (!this._sock || !isWritable(this._sock))
1359 throw new Error('Not connected');
1360
1361 const wantReply = (typeof cb === 'function');
1362
1363 if (!this.config.strictVendor
1364 || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) {
1365 if (wantReply) {
1366 this._callbacks.push((had_err) => {
1367 if (had_err) {
1368 cb(had_err !== true
1369 ? had_err
1370 : new Error(`Unable to bind to ${socketPath}`));
1371 return;
1372 }
1373 this._forwardingUnix[socketPath] = true;
1374 cb();
1375 });
1376 }
1377
1378 this._protocol.openssh_streamLocalForward(socketPath, wantReply);
1379 return this;
1380 }
1381
1382 if (!wantReply)
1383 return this;
1384
1385 process.nextTick(
1386 cb,
1387 new Error(
1388 'strictVendor enabled and server is not OpenSSH or compatible version'
1389 )
1390 );
1391
1392 return this;
1393 }
1394
1395 openssh_unforwardInStreamLocal(socketPath, cb) {
1396 if (!this._sock || !isWritable(this._sock))
1397 throw new Error('Not connected');
1398
1399 const wantReply = (typeof cb === 'function');
1400
1401 if (!this.config.strictVendor
1402 || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) {
1403 if (wantReply) {
1404 this._callbacks.push((had_err) => {
1405 if (had_err) {
1406 cb(had_err !== true
1407 ? had_err
1408 : new Error(`Unable to unbind from ${socketPath}`));
1409 return;
1410 }
1411 delete this._forwardingUnix[socketPath];
1412 cb();
1413 });
1414 }
1415
1416 this._protocol.openssh_cancelStreamLocalForward(socketPath, wantReply);
1417 return this;
1418 }
1419
1420 if (!wantReply)
1421 return this;
1422
1423 process.nextTick(
1424 cb,
1425 new Error(
1426 'strictVendor enabled and server is not OpenSSH or compatible version'
1427 )
1428 );
1429
1430 return this;
1431 }
1432
1433 openssh_forwardOutStreamLocal(socketPath, cb) {
1434 if (!this._sock || !isWritable(this._sock))
1435 throw new Error('Not connected');
1436
1437 if (typeof cb !== 'function')
1438 cb = noop;
1439
1440 if (!this.config.strictVendor
1441 || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) {
1442 openChannel(this, 'direct-streamlocal@openssh.com', { socketPath }, cb);
1443 return this;
1444 }
1445 process.nextTick(
1446 cb,
1447 new Error(
1448 'strictVendor enabled and server is not OpenSSH or compatible version'
1449 )
1450 );
1451
1452 return this;
1453 }
1454
1455 sftp(cb) {
1456 if (!this._sock || !isWritable(this._sock))
1457 throw new Error('Not connected');
1458
1459 openChannel(this, 'sftp', (err, sftp) => {
1460 if (err) {
1461 cb(err);
1462 return;
1463 }
1464
1465 reqSubsystem(sftp, 'sftp', (err, sftp_) => {
1466 if (err) {
1467 cb(err);
1468 return;
1469 }
1470
1471 function removeListeners() {
1472 sftp.removeListener('ready', onReady);
1473 sftp.removeListener('error', onError);
1474 sftp.removeListener('exit', onExit);
1475 sftp.removeListener('close', onExit);
1476 }
1477
1478 function onReady() {
1479 // TODO: do not remove exit/close in case remote end closes the
1480 // channel abruptly and we need to notify outstanding callbacks
1481 removeListeners();
1482 cb(undefined, sftp);
1483 }
1484
1485 function onError(err) {
1486 removeListeners();
1487 cb(err);
1488 }
1489
1490 function onExit(code, signal) {
1491 removeListeners();
1492 let msg;
1493 if (typeof code === 'number')
1494 msg = `Received exit code ${code} while establishing SFTP session`;
1495 else if (signal !== undefined)
1496 msg = `Received signal ${signal} while establishing SFTP session`;
1497 else
1498 msg = 'Received unexpected SFTP session termination';
1499 const err = new Error(msg);
1500 err.code = code;
1501 err.signal = signal;
1502 cb(err);
1503 }
1504
1505 sftp.on('ready', onReady)
1506 .on('error', onError)
1507 .on('exit', onExit)
1508 .on('close', onExit);
1509
1510 sftp._init();
1511 });
1512 });
1513
1514 return this;
1515 }
1516}
1517
1518function openChannel(self, type, opts, cb) {
1519 // Ask the server to open a channel for some purpose
1520 // (e.g. session (sftp, exec, shell), or forwarding a TCP connection
1521 const initWindow = MAX_WINDOW;
1522 const maxPacket = PACKET_SIZE;
1523
1524 if (typeof opts === 'function') {
1525 cb = opts;
1526 opts = {};
1527 }
1528
1529 const wrapper = (err, stream) => {
1530 cb(err, stream);
1531 };
1532 wrapper.type = type;
1533
1534 const localChan = self._chanMgr.add(wrapper);
1535
1536 if (localChan === -1) {
1537 cb(new Error('No free channels available'));
1538 return;
1539 }
1540
1541 switch (type) {
1542 case 'session':
1543 case 'sftp':
1544 self._protocol.session(localChan, initWindow, maxPacket);
1545 break;
1546 case 'direct-tcpip':
1547 self._protocol.directTcpip(localChan, initWindow, maxPacket, opts);
1548 break;
1549 case 'direct-streamlocal@openssh.com':
1550 self._protocol.openssh_directStreamLocal(
1551 localChan, initWindow, maxPacket, opts
1552 );
1553 break;
1554 default:
1555 throw new Error(`Unsupported channel type: ${type}`);
1556 }
1557}
1558
1559function reqX11(chan, screen, cb) {
1560 // Asks server to start sending us X11 connections
1561 const cfg = {
1562 single: false,
1563 protocol: 'MIT-MAGIC-COOKIE-1',
1564 cookie: undefined,
1565 screen: 0
1566 };
1567
1568 if (typeof screen === 'function') {
1569 cb = screen;
1570 } else if (typeof screen === 'object' && screen !== null) {
1571 if (typeof screen.single === 'boolean')
1572 cfg.single = screen.single;
1573 if (typeof screen.screen === 'number')
1574 cfg.screen = screen.screen;
1575 if (typeof screen.protocol === 'string')
1576 cfg.protocol = screen.protocol;
1577 if (typeof screen.cookie === 'string')
1578 cfg.cookie = screen.cookie;
1579 else if (Buffer.isBuffer(screen.cookie))
1580 cfg.cookie = screen.cookie.hexSlice(0, screen.cookie.length);
1581 }
1582 if (cfg.cookie === undefined)
1583 cfg.cookie = randomCookie();
1584
1585 const wantReply = (typeof cb === 'function');
1586
1587 if (chan.outgoing.state !== 'open') {
1588 if (wantReply)
1589 cb(new Error('Channel is not open'));
1590 return;
1591 }
1592
1593 if (wantReply) {
1594 chan._callbacks.push((had_err) => {
1595 if (had_err) {
1596 cb(had_err !== true ? had_err : new Error('Unable to request X11'));
1597 return;
1598 }
1599
1600 chan._hasX11 = true;
1601 ++chan._client._acceptX11;
1602 chan.once('close', () => {
1603 if (chan._client._acceptX11)
1604 --chan._client._acceptX11;
1605 });
1606
1607 cb();
1608 });
1609 }
1610
1611 chan._client._protocol.x11Forward(chan.outgoing.id, cfg, wantReply);
1612}
1613
1614function reqPty(chan, opts, cb) {
1615 let rows = 24;
1616 let cols = 80;
1617 let width = 640;
1618 let height = 480;
1619 let term = 'vt100';
1620 let modes = null;
1621
1622 if (typeof opts === 'function') {
1623 cb = opts;
1624 } else if (typeof opts === 'object' && opts !== null) {
1625 if (typeof opts.rows === 'number')
1626 rows = opts.rows;
1627 if (typeof opts.cols === 'number')
1628 cols = opts.cols;
1629 if (typeof opts.width === 'number')
1630 width = opts.width;
1631 if (typeof opts.height === 'number')
1632 height = opts.height;
1633 if (typeof opts.term === 'string')
1634 term = opts.term;
1635 if (typeof opts.modes === 'object')
1636 modes = opts.modes;
1637 }
1638
1639 const wantReply = (typeof cb === 'function');
1640
1641 if (chan.outgoing.state !== 'open') {
1642 if (wantReply)
1643 cb(new Error('Channel is not open'));
1644 return;
1645 }
1646
1647 if (wantReply) {
1648 chan._callbacks.push((had_err) => {
1649 if (had_err) {
1650 cb(had_err !== true
1651 ? had_err
1652 : new Error('Unable to request a pseudo-terminal'));
1653 return;
1654 }
1655 cb();
1656 });
1657 }
1658
1659 chan._client._protocol.pty(chan.outgoing.id,
1660 rows,
1661 cols,
1662 height,
1663 width,
1664 term,
1665 modes,
1666 wantReply);
1667}
1668
1669function reqAgentFwd(chan, cb) {
1670 const wantReply = (typeof cb === 'function');
1671
1672 if (chan.outgoing.state !== 'open') {
1673 wantReply && cb(new Error('Channel is not open'));
1674 return;
1675 }
1676 if (chan._client._agentFwdEnabled) {
1677 wantReply && cb(false);
1678 return;
1679 }
1680
1681 chan._client._agentFwdEnabled = true;
1682
1683 chan._callbacks.push((had_err) => {
1684 if (had_err) {
1685 chan._client._agentFwdEnabled = false;
1686 if (wantReply) {
1687 cb(had_err !== true
1688 ? had_err
1689 : new Error('Unable to request agent forwarding'));
1690 }
1691 return;
1692 }
1693
1694 if (wantReply)
1695 cb();
1696 });
1697
1698 chan._client._protocol.openssh_agentForward(chan.outgoing.id, true);
1699}
1700
1701function reqShell(chan, cb) {
1702 if (chan.outgoing.state !== 'open') {
1703 cb(new Error('Channel is not open'));
1704 return;
1705 }
1706
1707 chan._callbacks.push((had_err) => {
1708 if (had_err) {
1709 cb(had_err !== true ? had_err : new Error('Unable to open shell'));
1710 return;
1711 }
1712 chan.subtype = 'shell';
1713 cb(undefined, chan);
1714 });
1715
1716 chan._client._protocol.shell(chan.outgoing.id, true);
1717}
1718
1719function reqExec(chan, cmd, opts, cb) {
1720 if (chan.outgoing.state !== 'open') {
1721 cb(new Error('Channel is not open'));
1722 return;
1723 }
1724
1725 chan._callbacks.push((had_err) => {
1726 if (had_err) {
1727 cb(had_err !== true ? had_err : new Error('Unable to exec'));
1728 return;
1729 }
1730 chan.subtype = 'exec';
1731 chan.allowHalfOpen = (opts.allowHalfOpen !== false);
1732 cb(undefined, chan);
1733 });
1734
1735 chan._client._protocol.exec(chan.outgoing.id, cmd, true);
1736}
1737
1738function reqEnv(chan, env) {
1739 if (chan.outgoing.state !== 'open')
1740 return;
1741
1742 const keys = Object.keys(env || {});
1743
1744 for (let i = 0; i < keys.length; ++i) {
1745 const key = keys[i];
1746 const val = env[key];
1747 chan._client._protocol.env(chan.outgoing.id, key, val, false);
1748 }
1749}
1750
1751function reqSubsystem(chan, name, cb) {
1752 if (chan.outgoing.state !== 'open') {
1753 cb(new Error('Channel is not open'));
1754 return;
1755 }
1756
1757 chan._callbacks.push((had_err) => {
1758 if (had_err) {
1759 cb(had_err !== true
1760 ? had_err
1761 : new Error(`Unable to start subsystem: ${name}`));
1762 return;
1763 }
1764 chan.subtype = 'subsystem';
1765 cb(undefined, chan);
1766 });
1767
1768 chan._client._protocol.subsystem(chan.outgoing.id, name, true);
1769}
1770
1771// TODO: inline implementation into single call site
1772function onCHANNEL_OPEN(self, info) {
1773 // The server is trying to open a channel with us, this is usually when
1774 // we asked the server to forward us connections on some port and now they
1775 // are asking us to accept/deny an incoming connection on their side
1776
1777 let localChan = -1;
1778 let reason;
1779
1780 const accept = () => {
1781 const chanInfo = {
1782 type: info.type,
1783 incoming: {
1784 id: localChan,
1785 window: MAX_WINDOW,
1786 packetSize: PACKET_SIZE,
1787 state: 'open'
1788 },
1789 outgoing: {
1790 id: info.sender,
1791 window: info.window,
1792 packetSize: info.packetSize,
1793 state: 'open'
1794 }
1795 };
1796 const stream = new Channel(self, chanInfo);
1797 self._chanMgr.update(localChan, stream);
1798
1799 self._protocol.channelOpenConfirm(info.sender,
1800 localChan,
1801 MAX_WINDOW,
1802 PACKET_SIZE);
1803 return stream;
1804 };
1805 const reject = () => {
1806 if (reason === undefined) {
1807 if (localChan === -1)
1808 reason = CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE;
1809 else
1810 reason = CHANNEL_OPEN_FAILURE.CONNECT_FAILED;
1811 }
1812
1813 if (localChan !== -1)
1814 self._chanMgr.remove(localChan);
1815
1816 self._protocol.channelOpenFail(info.sender, reason, '');
1817 };
1818 const reserveChannel = () => {
1819 localChan = self._chanMgr.add();
1820
1821 if (localChan === -1) {
1822 reason = CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE;
1823 if (self.config.debug) {
1824 self.config.debug(
1825 'Client: Automatic rejection of incoming channel open: '
1826 + 'no channels available'
1827 );
1828 }
1829 }
1830
1831 return (localChan !== -1);
1832 };
1833
1834 const data = info.data;
1835 switch (info.type) {
1836 case 'forwarded-tcpip': {
1837 const val = self._forwarding[`${data.destIP}:${data.destPort}`];
1838 if (val !== undefined && reserveChannel()) {
1839 if (data.destPort === 0)
1840 data.destPort = val;
1841 self.emit('tcp connection', data, accept, reject);
1842 return;
1843 }
1844 break;
1845 }
1846 case 'forwarded-streamlocal@openssh.com':
1847 if (self._forwardingUnix[data.socketPath] !== undefined
1848 && reserveChannel()) {
1849 self.emit('unix connection', data, accept, reject);
1850 return;
1851 }
1852 break;
1853 case 'auth-agent@openssh.com':
1854 if (self._agentFwdEnabled
1855 && typeof self._agent.getStream === 'function'
1856 && reserveChannel()) {
1857 self._agent.getStream((err, stream) => {
1858 if (err)
1859 return reject();
1860
1861 const upstream = accept();
1862 upstream.pipe(stream).pipe(upstream);
1863 });
1864 return;
1865 }
1866 break;
1867 case 'x11':
1868 if (self._acceptX11 !== 0 && reserveChannel()) {
1869 self.emit('x11', data, accept, reject);
1870 return;
1871 }
1872 break;
1873 default:
1874 // Automatically reject any unsupported channel open requests
1875 reason = CHANNEL_OPEN_FAILURE.UNKNOWN_CHANNEL_TYPE;
1876 if (self.config.debug) {
1877 self.config.debug(
1878 'Client: Automatic rejection of unsupported incoming channel open '
1879 + `type: ${info.type}`
1880 );
1881 }
1882 }
1883
1884 if (reason === undefined) {
1885 reason = CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED;
1886 if (self.config.debug) {
1887 self.config.debug(
1888 'Client: Automatic rejection of unexpected incoming channel open for: '
1889 + info.type
1890 );
1891 }
1892 }
1893
1894 reject();
1895}
1896
1897const randomCookie = (() => {
1898 const buffer = Buffer.allocUnsafe(16);
1899 return () => {
1900 randomFillSync(buffer, 0, 16);
1901 return buffer.hexSlice(0, 16);
1902 };
1903})();
1904
1905function makeSimpleAuthHandler(authList) {
1906 if (!Array.isArray(authList))
1907 throw new Error('authList must be an array');
1908
1909 let a = 0;
1910 return (authsLeft, partialSuccess, cb) => {
1911 if (a === authList.length)
1912 return false;
1913 return authList[a++];
1914 };
1915}
1916
1917function hostKeysProve(client, keys_, cb) {
1918 if (!client._sock || !isWritable(client._sock))
1919 return;
1920
1921 if (typeof cb !== 'function')
1922 cb = noop;
1923
1924 if (!Array.isArray(keys_))
1925 throw new TypeError('Invalid keys argument type');
1926
1927 const keys = [];
1928 for (const key of keys_) {
1929 const parsed = parseKey(key);
1930 if (parsed instanceof Error)
1931 throw parsed;
1932 keys.push(parsed);
1933 }
1934
1935 if (!client.config.strictVendor
1936 || (client.config.strictVendor && RE_OPENSSH.test(client._remoteVer))) {
1937 client._callbacks.push((had_err, data) => {
1938 if (had_err) {
1939 cb(had_err !== true
1940 ? had_err
1941 : new Error('Server failed to prove supplied keys'));
1942 return;
1943 }
1944
1945 // TODO: move all of this parsing/verifying logic out of the client?
1946 const ret = [];
1947 let keyIdx = 0;
1948 bufferParser.init(data, 0);
1949 while (bufferParser.avail()) {
1950 if (keyIdx === keys.length)
1951 break;
1952 const key = keys[keyIdx++];
1953 const keyPublic = key.getPublicSSH();
1954
1955 const sigEntry = bufferParser.readString();
1956 sigParser.init(sigEntry, 0);
1957 const type = sigParser.readString(true);
1958 let value = sigParser.readString();
1959
1960 let algo;
1961 if (type !== key.type) {
1962 if (key.type === 'ssh-rsa') {
1963 switch (type) {
1964 case 'rsa-sha2-256':
1965 algo = 'sha256';
1966 break;
1967 case 'rsa-sha2-512':
1968 algo = 'sha512';
1969 break;
1970 default:
1971 continue;
1972 }
1973 } else {
1974 continue;
1975 }
1976 }
1977
1978 const sessionID = client._protocol._kex.sessionID;
1979 const verifyData = Buffer.allocUnsafe(
1980 4 + 29 + 4 + sessionID.length + 4 + keyPublic.length
1981 );
1982 let p = 0;
1983 writeUInt32BE(verifyData, 29, p);
1984 verifyData.utf8Write('hostkeys-prove-00@openssh.com', p += 4, 29);
1985 writeUInt32BE(verifyData, sessionID.length, p += 29);
1986 bufferCopy(sessionID, verifyData, 0, sessionID.length, p += 4);
1987 writeUInt32BE(verifyData, keyPublic.length, p += sessionID.length);
1988 bufferCopy(keyPublic, verifyData, 0, keyPublic.length, p += 4);
1989
1990 if (!(value = sigSSHToASN1(value, type)))
1991 continue;
1992 if (key.verify(verifyData, value, algo) === true)
1993 ret.push(key);
1994 }
1995 sigParser.clear();
1996 bufferParser.clear();
1997
1998 cb(null, ret);
1999 });
2000
2001 client._protocol.openssh_hostKeysProve(keys);
2002 return;
2003 }
2004
2005 process.nextTick(
2006 cb,
2007 new Error(
2008 'strictVendor enabled and server is not OpenSSH or compatible version'
2009 )
2010 );
2011}
2012
2013module.exports = Client;