1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | 'use strict';
|
7 |
|
8 | const { Server: netServer } = require('net');
|
9 | const EventEmitter = require('events');
|
10 | const { listenerCount } = EventEmitter;
|
11 |
|
12 | const {
|
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');
|
27 | const { init: cryptoInit } = require('./protocol/crypto.js');
|
28 | const { KexInit } = require('./protocol/kex.js');
|
29 | const { parseKey } = require('./protocol/keyParser.js');
|
30 | const Protocol = require('./protocol/Protocol.js');
|
31 | const { SFTP } = require('./protocol/SFTP.js');
|
32 | const { writeUInt32BE } = require('./protocol/utils.js');
|
33 |
|
34 | const {
|
35 | Channel,
|
36 | MAX_WINDOW,
|
37 | PACKET_SIZE,
|
38 | windowAdjust,
|
39 | WINDOW_THRESHOLD,
|
40 | } = require('./Channel.js');
|
41 |
|
42 | const {
|
43 | ChannelManager,
|
44 | generateAlgorithmList,
|
45 | isWritable,
|
46 | onChannelOpenFailure,
|
47 | onCHANNEL_CLOSE,
|
48 | } = require('./utils.js');
|
49 |
|
50 | const MAX_PENDING_AUTHS = 10;
|
51 |
|
52 | class 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 |
|
84 | class 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 |
|
134 | class 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 |
|
153 | class 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 |
|
165 | class 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 |
|
186 | class 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 |
|
213 | class 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 |
|
250 | privateKey = privateKey[0];
|
251 | }
|
252 |
|
253 | if (privateKey.getPrivatePEM() === null)
|
254 | throw new Error('privateKey value contains an invalid private key');
|
255 |
|
256 |
|
257 | if (hostKeyAlgoOrder.includes(privateKey.type))
|
258 | continue;
|
259 |
|
260 | if (privateKey.type === 'ssh-rsa') {
|
261 |
|
262 |
|
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 |
|
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 |
|
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 |
|
334 |
|
335 | const debugPrefix = `[${process.hrtime().join('.')}] `;
|
336 | debug = (msg) => {
|
337 | origDebug(`${debugPrefix}${msg}`);
|
338 | };
|
339 | }
|
340 |
|
341 |
|
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 | }
|
383 | Server.KEEPALIVE_CLIENT_INTERVAL = 15000;
|
384 | Server.KEEPALIVE_CLIENT_COUNT_MAX = 3;
|
385 |
|
386 |
|
387 | class 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 |
|
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 |
|
454 |
|
455 |
|
456 | proto.ping();
|
457 | }
|
458 | }, kaIntvl);
|
459 | });
|
460 |
|
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 |
|
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 |
|
529 |
|
530 |
|
531 |
|
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 |
|
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 |
|
736 |
|
737 |
|
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 |
|
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 |
|
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 |
|
823 | if (wantReply)
|
824 | proto.channelFailure(session.outgoing.id);
|
825 | return;
|
826 | }
|
827 |
|
828 | if (wantReply) {
|
829 |
|
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 |
|
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 |
|
886 | case 'signal':
|
887 | if (listenerCount(session, 'signal')) {
|
888 | session.emit('signal', accept, reject, {
|
889 | name: data
|
890 | });
|
891 | return;
|
892 | }
|
893 | break;
|
894 |
|
895 |
|
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 |
|
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 |
|
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 |
|
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 |
|
1113 |
|
1114 |
|
1115 |
|
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 |
|
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 |
|
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 |
|
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 |
|
1314 | function openChannel(self, type, opts, cb) {
|
1315 |
|
1316 |
|
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 |
|
1354 | function compareNumbers(a, b) {
|
1355 | return a - b;
|
1356 | }
|
1357 |
|
1358 | module.exports = Server;
|
1359 | module.exports.IncomingClient = Client;
|