UNPKG

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