1 | var net = require('net');
|
2 | var EventEmitter = require('events').EventEmitter;
|
3 | var listenerCount = EventEmitter.listenerCount;
|
4 | var inherits = require('util').inherits;
|
5 |
|
6 | var ssh2_streams = require('ssh2-streams');
|
7 | var parseKey = ssh2_streams.utils.parseKey;
|
8 | var SSH2Stream = ssh2_streams.SSH2Stream;
|
9 | var SFTPStream = ssh2_streams.SFTPStream;
|
10 | var consts = ssh2_streams.constants;
|
11 | var DISCONNECT_REASON = consts.DISCONNECT_REASON;
|
12 | var CHANNEL_OPEN_FAILURE = consts.CHANNEL_OPEN_FAILURE;
|
13 | var ALGORITHMS = consts.ALGORITHMS;
|
14 |
|
15 | var Channel = require('./Channel');
|
16 | var KeepaliveManager = require('./keepalivemgr');
|
17 | var writeUInt32BE = require('./buffer-helpers').writeUInt32BE;
|
18 |
|
19 | var MAX_CHANNEL = Math.pow(2, 32) - 1;
|
20 | var MAX_PENDING_AUTHS = 10;
|
21 |
|
22 | var kaMgr;
|
23 |
|
24 | function Server(cfg, listener) {
|
25 | if (!(this instanceof Server))
|
26 | return new Server(cfg, listener);
|
27 |
|
28 | var hostKeys = {
|
29 | 'ssh-rsa': null,
|
30 | 'ssh-dss': null,
|
31 | '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 |
|
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 |
|
136 |
|
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 |
|
197 |
|
198 |
|
199 | sshstream.read();
|
200 | }).on('error', function(err) {
|
201 | sshstream.reset();
|
202 | sshstream.emit('error', err);
|
203 | });
|
204 |
|
205 | var conncfg = streamcfg;
|
206 |
|
207 |
|
208 |
|
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 |
|
227 | function onClientPreHeaderError(err) {}
|
228 | client.on('error', onClientPreHeaderError);
|
229 |
|
230 | sshstream.once('header', function(header) {
|
231 | if (sshstream._readableState.ended) {
|
232 |
|
233 |
|
234 | return;
|
235 | } else if (!listenerCount(self, 'connection')) {
|
236 |
|
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 | }
|
259 | inherits(Server, EventEmitter);
|
260 |
|
261 | Server.prototype.listen = function() {
|
262 | this._srv.listen.apply(this._srv, arguments);
|
263 | return this;
|
264 | };
|
265 |
|
266 | Server.prototype.address = function() {
|
267 | return this._srv.address();
|
268 | };
|
269 |
|
270 | Server.prototype.getConnections = function(cb) {
|
271 | this._srv.getConnections(cb);
|
272 | };
|
273 |
|
274 | Server.prototype.close = function(cb) {
|
275 | this._srv.close(cb);
|
276 | return this;
|
277 | };
|
278 |
|
279 | Server.prototype.ref = function() {
|
280 | this._srv.ref();
|
281 | };
|
282 |
|
283 | Server.prototype.unref = function() {
|
284 | this._srv.unref();
|
285 | };
|
286 |
|
287 |
|
288 | function 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 |
|
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 |
|
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 |
|
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 |
|
368 |
|
369 |
|
370 |
|
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 |
|
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 |
|
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 |
|
493 | return stream.channelOpenFail(info.sender,
|
494 | CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE);
|
495 | }
|
496 |
|
497 |
|
498 |
|
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 |
|
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 | }
|
623 | inherits(Client, EventEmitter);
|
624 |
|
625 | Client.prototype.end = function() {
|
626 | return this._sshstream.disconnect(DISCONNECT_REASON.BY_APPLICATION);
|
627 | };
|
628 |
|
629 | Client.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 |
|
637 | Client.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 |
|
648 | Client.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 |
|
655 | Client.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 |
|
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 |
|
680 | function 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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 | }
|
901 | inherits(Session, EventEmitter);
|
902 |
|
903 |
|
904 | function 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 | }
|
923 | inherits(AuthContext, EventEmitter);
|
924 | AuthContext.prototype.accept = function() {
|
925 | this._cleanup && this._cleanup();
|
926 | this._initialResponse = true;
|
927 | this._cbfinal(true);
|
928 | };
|
929 | AuthContext.prototype.reject = function(methodsLeft, isPartial) {
|
930 | this._cleanup && this._cleanup();
|
931 | this._initialResponse = true;
|
932 | this._cbfinal(false, methodsLeft, isPartial);
|
933 | };
|
934 |
|
935 | var RE_KBINT_SUBMETHODS = /[ \t\r\n]*,[ \t\r\n]*/g;
|
936 | function 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 | }
|
955 | inherits(KeyboardAuthContext, AuthContext);
|
956 | KeyboardAuthContext.prototype._cleanup = function() {
|
957 | this._stream.removeListener('USERAUTH_INFO_RESPONSE', this._onInfoResponse);
|
958 | };
|
959 | KeyboardAuthContext.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 |
|
988 | function 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 | }
|
1014 | inherits(PKAuthContext, AuthContext);
|
1015 | PKAuthContext.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 |
|
1023 | function 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 | }
|
1051 | inherits(HostbasedAuthContext, AuthContext);
|
1052 |
|
1053 | function PwdAuthContext(stream, username, service, method, password, cb) {
|
1054 | AuthContext.call(this, stream, username, service, method, cb);
|
1055 |
|
1056 | this.password = password;
|
1057 | }
|
1058 | inherits(PwdAuthContext, AuthContext);
|
1059 |
|
1060 |
|
1061 | function openChannel(self, type, opts, cb) {
|
1062 |
|
1063 |
|
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 |
|
1124 | function nextChannel(self) {
|
1125 |
|
1126 |
|
1127 |
|
1128 | if (self._curChan < MAX_CHANNEL)
|
1129 | return ++self._curChan;
|
1130 |
|
1131 |
|
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 |
|
1140 | Server.createServer = function(cfg, listener) {
|
1141 | return new Server(cfg, listener);
|
1142 | };
|
1143 | Server.KEEPALIVE_INTERVAL = 1000;
|
1144 | Server.KEEPALIVE_CLIENT_INTERVAL = 15000;
|
1145 | Server.KEEPALIVE_CLIENT_COUNT_MAX = 3;
|
1146 |
|
1147 | module.exports = Server;
|
1148 | module.exports.IncomingClient = Client;
|