UNPKG

32.5 kBJavaScriptView Raw
1var net = require('net');
2var EventEmitter = require('events').EventEmitter;
3var listenerCount = EventEmitter.listenerCount;
4var inherits = require('util').inherits;
5
6var ssh2_streams = require('ssh2-streams');
7var parseKey = ssh2_streams.utils.parseKey;
8var SSH2Stream = ssh2_streams.SSH2Stream;
9var SFTPStream = ssh2_streams.SFTPStream;
10var consts = ssh2_streams.constants;
11var DISCONNECT_REASON = consts.DISCONNECT_REASON;
12var CHANNEL_OPEN_FAILURE = consts.CHANNEL_OPEN_FAILURE;
13var ALGORITHMS = consts.ALGORITHMS;
14
15var Channel = require('./Channel');
16var KeepaliveManager = require('./keepalivemgr');
17var writeUInt32BE = require('./buffer-helpers').writeUInt32BE;
18
19var MAX_CHANNEL = Math.pow(2, 32) - 1;
20var MAX_PENDING_AUTHS = 10;
21
22var kaMgr;
23
24function Server(cfg, listener) {
25 if (!(this instanceof Server))
26 return new Server(cfg, listener);
27
28 var hostKeys = {
29 'ssh-rsa': null,
30 'ssh-dss': null,
31 'ecdsa-sha2-nistp256': null,
32 'ecdsa-sha2-nistp384': null,
33 'ecdsa-sha2-nistp521': null
34 };
35
36 var hostKeys_ = cfg.hostKeys;
37 if (!Array.isArray(hostKeys_))
38 throw new Error('hostKeys must be an array');
39
40 var i;
41 for (i = 0; i < hostKeys_.length; ++i) {
42 var privateKey;
43 if (Buffer.isBuffer(hostKeys_[i]) || typeof hostKeys_[i] === 'string')
44 privateKey = parseKey(hostKeys_[i]);
45 else
46 privateKey = parseKey(hostKeys_[i].key, hostKeys_[i].passphrase);
47 if (privateKey instanceof Error)
48 throw new Error('Cannot parse privateKey: ' + privateKey.message);
49 if (privateKey.getPrivatePEM() === null)
50 throw new Error('privateKey value contains an invalid private key');
51 if (hostKeys[privateKey.type])
52 continue;
53 hostKeys[privateKey.type] = privateKey;
54 }
55
56 var algorithms = {
57 kex: undefined,
58 kexBuf: undefined,
59 cipher: undefined,
60 cipherBuf: undefined,
61 serverHostKey: undefined,
62 serverHostKeyBuf: undefined,
63 hmac: undefined,
64 hmacBuf: undefined,
65 compress: undefined,
66 compressBuf: undefined
67 };
68 if (typeof cfg.algorithms === 'object' && cfg.algorithms !== null) {
69 var algosSupported;
70 var algoList;
71
72 algoList = cfg.algorithms.kex;
73 if (Array.isArray(algoList) && algoList.length > 0) {
74 algosSupported = ALGORITHMS.SUPPORTED_KEX;
75 for (i = 0; i < algoList.length; ++i) {
76 if (algosSupported.indexOf(algoList[i]) === -1)
77 throw new Error('Unsupported key exchange algorithm: ' + algoList[i]);
78 }
79 algorithms.kex = algoList;
80 }
81
82 algoList = cfg.algorithms.cipher;
83 if (Array.isArray(algoList) && algoList.length > 0) {
84 algosSupported = ALGORITHMS.SUPPORTED_CIPHER;
85 for (i = 0; i < algoList.length; ++i) {
86 if (algosSupported.indexOf(algoList[i]) === -1)
87 throw new Error('Unsupported cipher algorithm: ' + algoList[i]);
88 }
89 algorithms.cipher = algoList;
90 }
91
92 algoList = cfg.algorithms.serverHostKey;
93 var copied = false;
94 if (Array.isArray(algoList) && algoList.length > 0) {
95 algosSupported = ALGORITHMS.SUPPORTED_SERVER_HOST_KEY;
96 for (i = algoList.length - 1; i >= 0; --i) {
97 if (algosSupported.indexOf(algoList[i]) === -1) {
98 throw new Error('Unsupported server host key algorithm: '
99 + algoList[i]);
100 }
101 if (!hostKeys[algoList[i]]) {
102 // Silently discard for now
103 if (!copied) {
104 algoList = algoList.slice();
105 copied = true;
106 }
107 algoList.splice(i, 1);
108 }
109 }
110 if (algoList.length > 0)
111 algorithms.serverHostKey = algoList;
112 }
113
114 algoList = cfg.algorithms.hmac;
115 if (Array.isArray(algoList) && algoList.length > 0) {
116 algosSupported = ALGORITHMS.SUPPORTED_HMAC;
117 for (i = 0; i < algoList.length; ++i) {
118 if (algosSupported.indexOf(algoList[i]) === -1)
119 throw new Error('Unsupported HMAC algorithm: ' + algoList[i]);
120 }
121 algorithms.hmac = algoList;
122 }
123
124 algoList = cfg.algorithms.compress;
125 if (Array.isArray(algoList) && algoList.length > 0) {
126 algosSupported = ALGORITHMS.SUPPORTED_COMPRESS;
127 for (i = 0; i < algoList.length; ++i) {
128 if (algosSupported.indexOf(algoList[i]) === -1)
129 throw new Error('Unsupported compression algorithm: ' + algoList[i]);
130 }
131 algorithms.compress = algoList;
132 }
133 }
134
135 // Make sure we at least have some kind of valid list of support key
136 // formats
137 if (algorithms.serverHostKey === undefined) {
138 var hostKeyAlgos = Object.keys(hostKeys);
139 for (i = hostKeyAlgos.length - 1; i >= 0; --i) {
140 if (!hostKeys[hostKeyAlgos[i]])
141 hostKeyAlgos.splice(i, 1);
142 }
143 algorithms.serverHostKey = hostKeyAlgos;
144 }
145
146 if (!kaMgr
147 && Server.KEEPALIVE_INTERVAL > 0
148 && Server.KEEPALIVE_CLIENT_INTERVAL > 0
149 && Server.KEEPALIVE_CLIENT_COUNT_MAX >= 0) {
150 kaMgr = new KeepaliveManager(Server.KEEPALIVE_INTERVAL,
151 Server.KEEPALIVE_CLIENT_INTERVAL,
152 Server.KEEPALIVE_CLIENT_COUNT_MAX);
153 }
154
155 var self = this;
156
157 EventEmitter.call(this);
158
159 if (typeof listener === 'function')
160 self.on('connection', listener);
161
162 var streamcfg = {
163 algorithms: algorithms,
164 hostKeys: hostKeys,
165 server: true
166 };
167 var keys;
168 var len;
169 for (i = 0, keys = Object.keys(cfg), len = keys.length; i < len; ++i) {
170 var key = keys[i];
171 if (key === 'privateKey'
172 || key === 'publicKey'
173 || key === 'passphrase'
174 || key === 'algorithms'
175 || key === 'hostKeys'
176 || key === 'server') {
177 continue;
178 }
179 streamcfg[key] = cfg[key];
180 }
181
182 if (typeof streamcfg.debug === 'function') {
183 var oldDebug = streamcfg.debug;
184 var cfgKeys = Object.keys(streamcfg);
185 }
186
187 this._srv = new net.Server(function(socket) {
188 if (self._connections >= self.maxConnections) {
189 socket.destroy();
190 return;
191 }
192 ++self._connections;
193 socket.once('close', function(had_err) {
194 --self._connections;
195
196 // since joyent/node#993bb93e0a, we have to "read past EOF" in order to
197 // get an `end` event on streams. thankfully adding this does not
198 // negatively affect node versions pre-joyent/node#993bb93e0a.
199 sshstream.read();
200 }).on('error', function(err) {
201 sshstream.reset();
202 sshstream.emit('error', err);
203 });
204
205 var conncfg = streamcfg;
206
207 // prepend debug output with a unique identifier in case there are multiple
208 // clients connected at the same time
209 if (oldDebug) {
210 conncfg = {};
211 for (var i = 0, key; i < cfgKeys.length; ++i) {
212 key = cfgKeys[i];
213 conncfg[key] = streamcfg[key];
214 }
215 var debugPrefix = '[' + process.hrtime().join('.') + '] ';
216 conncfg.debug = function(msg) {
217 oldDebug(debugPrefix + msg);
218 };
219 }
220
221 var sshstream = new SSH2Stream(conncfg);
222 var client = new Client(sshstream, socket);
223
224 socket.pipe(sshstream).pipe(socket);
225
226 // silence pre-header errors
227 function onClientPreHeaderError(err) {}
228 client.on('error', onClientPreHeaderError);
229
230 sshstream.once('header', function(header) {
231 if (sshstream._readableState.ended) {
232 // already disconnected internally in SSH2Stream due to incompatible
233 // protocol version
234 return;
235 } else if (!listenerCount(self, 'connection')) {
236 // auto reject
237 return sshstream.disconnect(DISCONNECT_REASON.BY_APPLICATION);
238 }
239
240 client.removeListener('error', onClientPreHeaderError);
241
242 self.emit('connection',
243 client,
244 { ip: socket.remoteAddress,
245 family: socket.remoteFamily,
246 port: socket.remotePort,
247 header: header });
248 });
249 }).on('error', function(err) {
250 self.emit('error', err);
251 }).on('listening', function() {
252 self.emit('listening');
253 }).on('close', function() {
254 self.emit('close');
255 });
256 this._connections = 0;
257 this.maxConnections = Infinity;
258}
259inherits(Server, EventEmitter);
260
261Server.prototype.listen = function() {
262 this._srv.listen.apply(this._srv, arguments);
263 return this;
264};
265
266Server.prototype.address = function() {
267 return this._srv.address();
268};
269
270Server.prototype.getConnections = function(cb) {
271 this._srv.getConnections(cb);
272};
273
274Server.prototype.close = function(cb) {
275 this._srv.close(cb);
276 return this;
277};
278
279Server.prototype.ref = function() {
280 this._srv.ref();
281};
282
283Server.prototype.unref = function() {
284 this._srv.unref();
285};
286
287
288function Client(stream, socket) {
289 EventEmitter.call(this);
290
291 var self = this;
292
293 this._sshstream = stream;
294 var channels = this._channels = {};
295 this._curChan = -1;
296 this._sock = socket;
297 this.noMoreSessions = false;
298 this.authenticated = false;
299
300 stream.on('end', function() {
301 socket.resume();
302 self.emit('end');
303 }).on('close', function(hasErr) {
304 self.emit('close', hasErr);
305 }).on('error', function(err) {
306 self.emit('error', err);
307 }).on('drain', function() {
308 self.emit('drain');
309 }).on('continue', function() {
310 self.emit('continue');
311 });
312
313 var exchanges = 0;
314 var acceptedAuthSvc = false;
315 var pendingAuths = [];
316 var authCtx;
317
318 // begin service/auth-related ================================================
319 stream.on('SERVICE_REQUEST', function(service) {
320 if (exchanges === 0
321 || acceptedAuthSvc
322 || self.authenticated
323 || service !== 'ssh-userauth')
324 return stream.disconnect(DISCONNECT_REASON.SERVICE_NOT_AVAILABLE);
325
326 acceptedAuthSvc = true;
327 stream.serviceAccept(service);
328 }).on('USERAUTH_REQUEST', onUSERAUTH_REQUEST);
329 function onUSERAUTH_REQUEST(username, service, method, methodData) {
330 if (exchanges === 0
331 || (authCtx
332 && (authCtx.username !== username || authCtx.service !== service))
333 // TODO: support hostbased auth
334 || (method !== 'password'
335 && method !== 'publickey'
336 && method !== 'hostbased'
337 && method !== 'keyboard-interactive'
338 && method !== 'none')
339 || pendingAuths.length === MAX_PENDING_AUTHS)
340 return stream.disconnect(DISCONNECT_REASON.PROTOCOL_ERROR);
341 else if (service !== 'ssh-connection')
342 return stream.disconnect(DISCONNECT_REASON.SERVICE_NOT_AVAILABLE);
343
344 // XXX: this really shouldn't be reaching into private state ...
345 stream._state.authMethod = method;
346
347 var ctx;
348 if (method === 'keyboard-interactive') {
349 ctx = new KeyboardAuthContext(stream, username, service, method,
350 methodData, onAuthDecide);
351 } else if (method === 'publickey') {
352 ctx = new PKAuthContext(stream, username, service, method, methodData,
353 onAuthDecide);
354 } else if (method === 'hostbased') {
355 ctx = new HostbasedAuthContext(stream, username, service, method,
356 methodData, onAuthDecide);
357 } else if (method === 'password') {
358 ctx = new PwdAuthContext(stream, username, service, method, methodData,
359 onAuthDecide);
360 } else if (method === 'none')
361 ctx = new AuthContext(stream, username, service, method, onAuthDecide);
362
363 if (authCtx) {
364 if (!authCtx._initialResponse)
365 return pendingAuths.push(ctx);
366 else if (authCtx._multistep && !this._finalResponse) {
367 // RFC 4252 says to silently abort the current auth request if a new
368 // auth request comes in before the final response from an auth method
369 // that requires additional request/response exchanges -- this means
370 // keyboard-interactive for now ...
371 authCtx._cleanup && authCtx._cleanup();
372 authCtx.emit('abort');
373 }
374 }
375
376 authCtx = ctx;
377
378 if (listenerCount(self, 'authentication'))
379 self.emit('authentication', authCtx);
380 else
381 authCtx.reject();
382 }
383 function onAuthDecide(ctx, allowed, methodsLeft, isPartial) {
384 if (authCtx === ctx && !self.authenticated) {
385 if (allowed) {
386 stream.removeListener('USERAUTH_REQUEST', onUSERAUTH_REQUEST);
387 authCtx = undefined;
388 self.authenticated = true;
389 stream.authSuccess();
390 pendingAuths = [];
391 self.emit('ready');
392 } else {
393 stream.authFailure(methodsLeft, isPartial);
394 if (pendingAuths.length) {
395 authCtx = pendingAuths.pop();
396 if (listenerCount(self, 'authentication'))
397 self.emit('authentication', authCtx);
398 else
399 authCtx.reject();
400 }
401 }
402 }
403 }
404 // end service/auth-related ==================================================
405
406 var unsentGlobalRequestsReplies = [];
407
408 function sendReplies() {
409 var reply;
410 while (unsentGlobalRequestsReplies.length > 0
411 && unsentGlobalRequestsReplies[0].type) {
412 reply = unsentGlobalRequestsReplies.shift();
413 if (reply.type === 'SUCCESS')
414 stream.requestSuccess(reply.buf);
415 if (reply.type === 'FAILURE')
416 stream.requestFailure();
417 }
418 }
419
420 stream.on('GLOBAL_REQUEST', function(name, wantReply, data) {
421 var reply = {
422 type: null,
423 buf: null
424 };
425
426 function setReply(type, buf) {
427 reply.type = type;
428 reply.buf = buf;
429 sendReplies();
430 }
431
432 if (wantReply)
433 unsentGlobalRequestsReplies.push(reply);
434
435 if ((name === 'tcpip-forward'
436 || name === 'cancel-tcpip-forward'
437 || name === 'no-more-sessions@openssh.com'
438 || name === 'streamlocal-forward@openssh.com'
439 || name === 'cancel-streamlocal-forward@openssh.com')
440 && listenerCount(self, 'request')
441 && self.authenticated) {
442 var accept;
443 var reject;
444
445 if (wantReply) {
446 var replied = false;
447 accept = function(chosenPort) {
448 if (replied)
449 return;
450 replied = true;
451 var bufPort;
452 if (name === 'tcpip-forward'
453 && data.bindPort === 0
454 && typeof chosenPort === 'number') {
455 bufPort = Buffer.allocUnsafe(4);
456 writeUInt32BE(bufPort, chosenPort, 0);
457 }
458 setReply('SUCCESS', bufPort);
459 };
460 reject = function() {
461 if (replied)
462 return;
463 replied = true;
464 setReply('FAILURE');
465 };
466 }
467
468 if (name === 'no-more-sessions@openssh.com') {
469 self.noMoreSessions = true;
470 accept && accept();
471 return;
472 }
473
474 self.emit('request', accept, reject, name, data);
475 } else if (wantReply)
476 setReply('FAILURE');
477 });
478
479 stream.on('CHANNEL_OPEN', function(info) {
480 // do early reject in some cases to prevent wasteful channel allocation
481 if ((info.type === 'session' && self.noMoreSessions)
482 || !self.authenticated) {
483 var reasonCode = CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED;
484 return stream.channelOpenFail(info.sender, reasonCode);
485 }
486
487 var localChan = nextChannel(self);
488 var accept;
489 var reject;
490 var replied = false;
491 if (localChan === false) {
492 // auto-reject due to no channels available
493 return stream.channelOpenFail(info.sender,
494 CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE);
495 }
496
497 // be optimistic, reserve channel to prevent another request from trying to
498 // take the same channel
499 channels[localChan] = true;
500
501 reject = function() {
502 if (replied)
503 return;
504
505 replied = true;
506
507 delete channels[localChan];
508
509 var reasonCode = CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED;
510 return stream.channelOpenFail(info.sender, reasonCode);
511 };
512
513 switch (info.type) {
514 case 'session':
515 if (listenerCount(self, 'session')) {
516 accept = function() {
517 if (replied)
518 return;
519
520 replied = true;
521
522 stream.channelOpenConfirm(info.sender,
523 localChan,
524 Channel.MAX_WINDOW,
525 Channel.PACKET_SIZE);
526
527 return new Session(self, info, localChan);
528 };
529
530 self.emit('session', accept, reject);
531 } else
532 reject();
533 break;
534 case 'direct-tcpip':
535 if (listenerCount(self, 'tcpip')) {
536 accept = function() {
537 if (replied)
538 return;
539
540 replied = true;
541
542 stream.channelOpenConfirm(info.sender,
543 localChan,
544 Channel.MAX_WINDOW,
545 Channel.PACKET_SIZE);
546
547 var chaninfo = {
548 type: undefined,
549 incoming: {
550 id: localChan,
551 window: Channel.MAX_WINDOW,
552 packetSize: Channel.PACKET_SIZE,
553 state: 'open'
554 },
555 outgoing: {
556 id: info.sender,
557 window: info.window,
558 packetSize: info.packetSize,
559 state: 'open'
560 }
561 };
562
563 return new Channel(chaninfo, self);
564 };
565
566 self.emit('tcpip', accept, reject, info.data);
567 } else
568 reject();
569 break;
570 case 'direct-streamlocal@openssh.com':
571 if (listenerCount(self, 'openssh.streamlocal')) {
572 accept = function() {
573 if (replied)
574 return;
575
576 replied = true;
577
578 stream.channelOpenConfirm(info.sender,
579 localChan,
580 Channel.MAX_WINDOW,
581 Channel.PACKET_SIZE);
582
583 var chaninfo = {
584 type: undefined,
585 incoming: {
586 id: localChan,
587 window: Channel.MAX_WINDOW,
588 packetSize: Channel.PACKET_SIZE,
589 state: 'open'
590 },
591 outgoing: {
592 id: info.sender,
593 window: info.window,
594 packetSize: info.packetSize,
595 state: 'open'
596 }
597 };
598
599 return new Channel(chaninfo, self);
600 };
601
602 self.emit('openssh.streamlocal', accept, reject, info.data);
603 } else
604 reject();
605 break;
606 default:
607 // auto-reject unsupported channel types
608 reject();
609 }
610 });
611
612 stream.on('NEWKEYS', function() {
613 if (++exchanges > 1)
614 self.emit('rekey');
615 });
616
617 if (kaMgr) {
618 this.once('ready', function() {
619 kaMgr.add(stream);
620 });
621 }
622}
623inherits(Client, EventEmitter);
624
625Client.prototype.end = function() {
626 return this._sshstream.disconnect(DISCONNECT_REASON.BY_APPLICATION);
627};
628
629Client.prototype.x11 = function(originAddr, originPort, cb) {
630 var opts = {
631 originAddr: originAddr,
632 originPort: originPort
633 };
634 return openChannel(this, 'x11', opts, cb);
635};
636
637Client.prototype.forwardOut = function(boundAddr, boundPort, remoteAddr,
638 remotePort, cb) {
639 var opts = {
640 boundAddr: boundAddr,
641 boundPort: boundPort,
642 remoteAddr: remoteAddr,
643 remotePort: remotePort
644 };
645 return openChannel(this, 'forwarded-tcpip', opts, cb);
646};
647
648Client.prototype.openssh_forwardOutStreamLocal = function(socketPath, cb) {
649 var opts = {
650 socketPath: socketPath
651 };
652 return openChannel(this, 'forwarded-streamlocal@openssh.com', opts, cb);
653};
654
655Client.prototype.rekey = function(cb) {
656 var stream = this._sshstream;
657 var ret = true;
658 var error;
659
660 try {
661 ret = stream.rekey();
662 } catch (ex) {
663 error = ex;
664 }
665
666 // TODO: re-throw error if no callback?
667
668 if (typeof cb === 'function') {
669 if (error) {
670 process.nextTick(function() {
671 cb(error);
672 });
673 } else
674 this.once('rekey', cb);
675 }
676
677 return ret;
678};
679
680function Session(client, info, localChan) {
681 this.subtype = undefined;
682
683 var ending = false;
684 var self = this;
685 var outgoingId = info.sender;
686 var channel;
687
688 var chaninfo = {
689 type: 'session',
690 incoming: {
691 id: localChan,
692 window: Channel.MAX_WINDOW,
693 packetSize: Channel.PACKET_SIZE,
694 state: 'open'
695 },
696 outgoing: {
697 id: info.sender,
698 window: info.window,
699 packetSize: info.packetSize,
700 state: 'open'
701 }
702 };
703
704 function onREQUEST(info) {
705 var replied = false;
706 var accept;
707 var reject;
708
709 if (info.wantReply) {
710 // "real session" requests will have custom accept behaviors
711 if (info.request !== 'shell'
712 && info.request !== 'exec'
713 && info.request !== 'subsystem') {
714 accept = function() {
715 if (replied || ending || channel)
716 return;
717
718 replied = true;
719
720 return client._sshstream.channelSuccess(outgoingId);
721 };
722 }
723
724 reject = function() {
725 if (replied || ending || channel)
726 return;
727
728 replied = true;
729
730 return client._sshstream.channelFailure(outgoingId);
731 };
732 }
733
734 if (ending) {
735 reject && reject();
736 return;
737 }
738
739 switch (info.request) {
740 // "pre-real session start" requests
741 case 'env':
742 if (listenerCount(self, 'env')) {
743 self.emit('env', accept, reject, {
744 key: info.key,
745 val: info.val
746 });
747 } else
748 reject && reject();
749 break;
750 case 'pty-req':
751 if (listenerCount(self, 'pty')) {
752 self.emit('pty', accept, reject, {
753 cols: info.cols,
754 rows: info.rows,
755 width: info.width,
756 height: info.height,
757 term: info.term,
758 modes: info.modes,
759 });
760 } else
761 reject && reject();
762 break;
763 case 'window-change':
764 if (listenerCount(self, 'window-change')) {
765 self.emit('window-change', accept, reject, {
766 cols: info.cols,
767 rows: info.rows,
768 width: info.width,
769 height: info.height
770 });
771 } else
772 reject && reject();
773 break;
774 case 'x11-req':
775 if (listenerCount(self, 'x11')) {
776 self.emit('x11', accept, reject, {
777 single: info.single,
778 protocol: info.protocol,
779 cookie: info.cookie,
780 screen: info.screen
781 });
782 } else
783 reject && reject();
784 break;
785 // "post-real session start" requests
786 case 'signal':
787 if (listenerCount(self, 'signal')) {
788 self.emit('signal', accept, reject, {
789 name: info.signal
790 });
791 } else
792 reject && reject();
793 break;
794 // XXX: is `auth-agent-req@openssh.com` really "post-real session start"?
795 case 'auth-agent-req@openssh.com':
796 if (listenerCount(self, 'auth-agent'))
797 self.emit('auth-agent', accept, reject);
798 else
799 reject && reject();
800 break;
801 // "real session start" requests
802 case 'shell':
803 if (listenerCount(self, 'shell')) {
804 accept = function() {
805 if (replied || ending || channel)
806 return;
807
808 replied = true;
809
810 if (info.wantReply)
811 client._sshstream.channelSuccess(outgoingId);
812
813 channel = new Channel(chaninfo, client, { server: true });
814
815 channel.subtype = self.subtype = info.request;
816
817 return channel;
818 };
819
820 self.emit('shell', accept, reject);
821 } else
822 reject && reject();
823 break;
824 case 'exec':
825 if (listenerCount(self, 'exec')) {
826 accept = function() {
827 if (replied || ending || channel)
828 return;
829
830 replied = true;
831
832 if (info.wantReply)
833 client._sshstream.channelSuccess(outgoingId);
834
835 channel = new Channel(chaninfo, client, { server: true });
836
837 channel.subtype = self.subtype = info.request;
838
839 return channel;
840 };
841
842 self.emit('exec', accept, reject, {
843 command: info.command
844 });
845 } else
846 reject && reject();
847 break;
848 case 'subsystem':
849 accept = function() {
850 if (replied || ending || channel)
851 return;
852
853 replied = true;
854
855 if (info.wantReply)
856 client._sshstream.channelSuccess(outgoingId);
857
858 channel = new Channel(chaninfo, client, { server: true });
859
860 channel.subtype = self.subtype = (info.request + ':' + info.subsystem);
861
862 if (info.subsystem === 'sftp') {
863 var sftp = new SFTPStream({
864 server: true,
865 debug: client._sshstream.debug
866 });
867 channel.pipe(sftp).pipe(channel);
868
869 return sftp;
870 } else
871 return channel;
872 };
873
874 if (info.subsystem === 'sftp' && listenerCount(self, 'sftp'))
875 self.emit('sftp', accept, reject);
876 else if (info.subsystem !== 'sftp' && listenerCount(self, 'subsystem')) {
877 self.emit('subsystem', accept, reject, {
878 name: info.subsystem
879 });
880 } else
881 reject && reject();
882 break;
883 default:
884 reject && reject();
885 }
886 }
887 function onEOF() {
888 ending = true;
889 self.emit('eof');
890 self.emit('end');
891 }
892 function onCLOSE() {
893 ending = true;
894 self.emit('close');
895 }
896 client._sshstream
897 .on('CHANNEL_REQUEST:' + localChan, onREQUEST)
898 .once('CHANNEL_EOF:' + localChan, onEOF)
899 .once('CHANNEL_CLOSE:' + localChan, onCLOSE);
900}
901inherits(Session, EventEmitter);
902
903
904function AuthContext(stream, username, service, method, cb) {
905 EventEmitter.call(this);
906
907 var self = this;
908
909 this.username = this.user = username;
910 this.service = service;
911 this.method = method;
912 this._initialResponse = false;
913 this._finalResponse = false;
914 this._multistep = false;
915 this._cbfinal = function(allowed, methodsLeft, isPartial) {
916 if (!self._finalResponse) {
917 self._finalResponse = true;
918 cb(self, allowed, methodsLeft, isPartial);
919 }
920 };
921 this._stream = stream;
922}
923inherits(AuthContext, EventEmitter);
924AuthContext.prototype.accept = function() {
925 this._cleanup && this._cleanup();
926 this._initialResponse = true;
927 this._cbfinal(true);
928};
929AuthContext.prototype.reject = function(methodsLeft, isPartial) {
930 this._cleanup && this._cleanup();
931 this._initialResponse = true;
932 this._cbfinal(false, methodsLeft, isPartial);
933};
934
935var RE_KBINT_SUBMETHODS = /[ \t\r\n]*,[ \t\r\n]*/g;
936function KeyboardAuthContext(stream, username, service, method, submethods, cb) {
937 AuthContext.call(this, stream, username, service, method, cb);
938 this._multistep = true;
939
940 var self = this;
941
942 this._cb = undefined;
943 this._onInfoResponse = function(responses) {
944 if (self._cb) {
945 var callback = self._cb;
946 self._cb = undefined;
947 callback(responses);
948 }
949 };
950 this.submethods = submethods.split(RE_KBINT_SUBMETHODS);
951 this.on('abort', function() {
952 self._cb && self._cb(new Error('Authentication request aborted'));
953 });
954}
955inherits(KeyboardAuthContext, AuthContext);
956KeyboardAuthContext.prototype._cleanup = function() {
957 this._stream.removeListener('USERAUTH_INFO_RESPONSE', this._onInfoResponse);
958};
959KeyboardAuthContext.prototype.prompt = function(prompts, title, instructions,
960 cb) {
961 if (!Array.isArray(prompts))
962 prompts = [ prompts ];
963
964 if (typeof title === 'function') {
965 cb = title;
966 title = instructions = undefined;
967 } else if (typeof instructions === 'function') {
968 cb = instructions;
969 instructions = undefined;
970 }
971
972 for (var i = 0; i < prompts.length; ++i) {
973 if (typeof prompts[i] === 'string') {
974 prompts[i] = {
975 prompt: prompts[i],
976 echo: true
977 };
978 }
979 }
980
981 this._cb = cb;
982 this._initialResponse = true;
983 this._stream.once('USERAUTH_INFO_RESPONSE', this._onInfoResponse);
984
985 return this._stream.authInfoReq(title, instructions, prompts);
986};
987
988function PKAuthContext(stream, username, service, method, pkInfo, cb) {
989 AuthContext.call(this, stream, username, service, method, cb);
990
991 this.key = { algo: pkInfo.keyAlgo, data: pkInfo.key };
992 this.signature = pkInfo.signature;
993 var sigAlgo;
994 if (this.signature) {
995 switch (pkInfo.keyAlgo) {
996 case 'ssh-rsa':
997 case 'ssh-dss':
998 sigAlgo = 'sha1';
999 break;
1000 case 'ecdsa-sha2-nistp256':
1001 sigAlgo = 'sha256';
1002 break;
1003 case 'ecdsa-sha2-nistp384':
1004 sigAlgo = 'sha384';
1005 break;
1006 case 'ecdsa-sha2-nistp521':
1007 sigAlgo = 'sha512';
1008 break;
1009 }
1010 }
1011 this.sigAlgo = sigAlgo;
1012 this.blob = pkInfo.blob;
1013}
1014inherits(PKAuthContext, AuthContext);
1015PKAuthContext.prototype.accept = function() {
1016 if (!this.signature) {
1017 this._initialResponse = true;
1018 this._stream.authPKOK(this.key.algo, this.key.data);
1019 } else
1020 AuthContext.prototype.accept.call(this);
1021};
1022
1023function HostbasedAuthContext(stream, username, service, method, pkInfo, cb) {
1024 AuthContext.call(this, stream, username, service, method, cb);
1025
1026 this.key = { algo: pkInfo.keyAlgo, data: pkInfo.key };
1027 this.signature = pkInfo.signature;
1028 var sigAlgo;
1029 if (this.signature) {
1030 switch (pkInfo.keyAlgo) {
1031 case 'ssh-rsa':
1032 case 'ssh-dss':
1033 sigAlgo = 'sha1';
1034 break;
1035 case 'ecdsa-sha2-nistp256':
1036 sigAlgo = 'sha256';
1037 break;
1038 case 'ecdsa-sha2-nistp384':
1039 sigAlgo = 'sha384';
1040 break;
1041 case 'ecdsa-sha2-nistp521':
1042 sigAlgo = 'sha512';
1043 break;
1044 }
1045 }
1046 this.sigAlgo = sigAlgo;
1047 this.blob = pkInfo.blob;
1048 this.localHostname = pkInfo.localHostname;
1049 this.localUsername = pkInfo.localUsername;
1050}
1051inherits(HostbasedAuthContext, AuthContext);
1052
1053function PwdAuthContext(stream, username, service, method, password, cb) {
1054 AuthContext.call(this, stream, username, service, method, cb);
1055
1056 this.password = password;
1057}
1058inherits(PwdAuthContext, AuthContext);
1059
1060
1061function openChannel(self, type, opts, cb) {
1062 // ask the client to open a channel for some purpose
1063 // (e.g. a forwarded TCP connection)
1064 var localChan = nextChannel(self);
1065 var initWindow = Channel.MAX_WINDOW;
1066 var maxPacket = Channel.PACKET_SIZE;
1067 var ret = true;
1068
1069 if (localChan === false)
1070 return cb(new Error('No free channels available'));
1071
1072 if (typeof opts === 'function') {
1073 cb = opts;
1074 opts = {};
1075 }
1076
1077 self._channels[localChan] = true;
1078
1079 var sshstream = self._sshstream;
1080 sshstream.once('CHANNEL_OPEN_CONFIRMATION:' + localChan, function(info) {
1081 sshstream.removeAllListeners('CHANNEL_OPEN_FAILURE:' + localChan);
1082
1083 var chaninfo = {
1084 type: type,
1085 incoming: {
1086 id: localChan,
1087 window: initWindow,
1088 packetSize: maxPacket,
1089 state: 'open'
1090 },
1091 outgoing: {
1092 id: info.sender,
1093 window: info.window,
1094 packetSize: info.packetSize,
1095 state: 'open'
1096 }
1097 };
1098 cb(undefined, new Channel(chaninfo, self, { server: true }));
1099 }).once('CHANNEL_OPEN_FAILURE:' + localChan, function(info) {
1100 sshstream.removeAllListeners('CHANNEL_OPEN_CONFIRMATION:' + localChan);
1101
1102 delete self._channels[localChan];
1103
1104 var err = new Error('(SSH) Channel open failure: ' + info.description);
1105 err.reason = info.reason;
1106 err.lang = info.lang;
1107 cb(err);
1108 });
1109
1110 if (type === 'forwarded-tcpip')
1111 ret = sshstream.forwardedTcpip(localChan, initWindow, maxPacket, opts);
1112 else if (type === 'x11')
1113 ret = sshstream.x11(localChan, initWindow, maxPacket, opts);
1114 else if (type === 'forwarded-streamlocal@openssh.com') {
1115 ret = sshstream.openssh_forwardedStreamLocal(localChan,
1116 initWindow,
1117 maxPacket,
1118 opts);
1119 }
1120
1121 return ret;
1122}
1123
1124function nextChannel(self) {
1125 // get the next available channel number
1126
1127 // fast path
1128 if (self._curChan < MAX_CHANNEL)
1129 return ++self._curChan;
1130
1131 // slower lookup path
1132 for (var i = 0, channels = self._channels; i < MAX_CHANNEL; ++i)
1133 if (!channels[i])
1134 return i;
1135
1136 return false;
1137}
1138
1139
1140Server.createServer = function(cfg, listener) {
1141 return new Server(cfg, listener);
1142};
1143Server.KEEPALIVE_INTERVAL = 1000;
1144Server.KEEPALIVE_CLIENT_INTERVAL = 15000;
1145Server.KEEPALIVE_CLIENT_COUNT_MAX = 3;
1146
1147module.exports = Server;
1148module.exports.IncomingClient = Client;