var net = require('net'); var EventEmitter = require('events').EventEmitter; var listenerCount = EventEmitter.listenerCount; var inherits = require('util').inherits; var ssh2_streams = require('ssh2-streams'); var parseKey = ssh2_streams.utils.parseKey; var SSH2Stream = ssh2_streams.SSH2Stream; var SFTPStream = ssh2_streams.SFTPStream; var consts = ssh2_streams.constants; var DISCONNECT_REASON = consts.DISCONNECT_REASON; var CHANNEL_OPEN_FAILURE = consts.CHANNEL_OPEN_FAILURE; var ALGORITHMS = consts.ALGORITHMS; var Channel = require('./Channel'); var KeepaliveManager = require('./keepalivemgr'); var writeUInt32BE = require('./buffer-helpers').writeUInt32BE; var MAX_CHANNEL = Math.pow(2, 32) - 1; var MAX_PENDING_AUTHS = 10; var kaMgr; function Server(cfg, listener) { if (!(this instanceof Server)) return new Server(cfg, listener); var hostKeys = { 'ssh-rsa': null, 'ssh-dss': null, 'ssh-ed25519': null, 'ecdsa-sha2-nistp256': null, 'ecdsa-sha2-nistp384': null, 'ecdsa-sha2-nistp521': null }; var hostKeys_ = cfg.hostKeys; if (!Array.isArray(hostKeys_)) throw new Error('hostKeys must be an array'); var i; for (i = 0; i < hostKeys_.length; ++i) { var privateKey; if (Buffer.isBuffer(hostKeys_[i]) || typeof hostKeys_[i] === 'string') privateKey = parseKey(hostKeys_[i]); else privateKey = parseKey(hostKeys_[i].key, hostKeys_[i].passphrase); if (privateKey instanceof Error) throw new Error('Cannot parse privateKey: ' + privateKey.message); if (privateKey.getPrivatePEM() === null) throw new Error('privateKey value contains an invalid private key'); if (hostKeys[privateKey.type]) continue; hostKeys[privateKey.type] = privateKey; } var algorithms = { kex: undefined, kexBuf: undefined, cipher: undefined, cipherBuf: undefined, serverHostKey: undefined, serverHostKeyBuf: undefined, hmac: undefined, hmacBuf: undefined, compress: undefined, compressBuf: undefined }; if (typeof cfg.algorithms === 'object' && cfg.algorithms !== null) { var algosSupported; var algoList; algoList = cfg.algorithms.kex; if (Array.isArray(algoList) && algoList.length > 0) { algosSupported = ALGORITHMS.SUPPORTED_KEX; for (i = 0; i < algoList.length; ++i) { if (algosSupported.indexOf(algoList[i]) === -1) throw new Error('Unsupported key exchange algorithm: ' + algoList[i]); } algorithms.kex = algoList; } algoList = cfg.algorithms.cipher; if (Array.isArray(algoList) && algoList.length > 0) { algosSupported = ALGORITHMS.SUPPORTED_CIPHER; for (i = 0; i < algoList.length; ++i) { if (algosSupported.indexOf(algoList[i]) === -1) throw new Error('Unsupported cipher algorithm: ' + algoList[i]); } algorithms.cipher = algoList; } algoList = cfg.algorithms.serverHostKey; var copied = false; if (Array.isArray(algoList) && algoList.length > 0) { algosSupported = ALGORITHMS.SUPPORTED_SERVER_HOST_KEY; for (i = algoList.length - 1; i >= 0; --i) { if (algosSupported.indexOf(algoList[i]) === -1) { throw new Error('Unsupported server host key algorithm: ' + algoList[i]); } if (!hostKeys[algoList[i]]) { // Silently discard for now if (!copied) { algoList = algoList.slice(); copied = true; } algoList.splice(i, 1); } } if (algoList.length > 0) algorithms.serverHostKey = algoList; } algoList = cfg.algorithms.hmac; if (Array.isArray(algoList) && algoList.length > 0) { algosSupported = ALGORITHMS.SUPPORTED_HMAC; for (i = 0; i < algoList.length; ++i) { if (algosSupported.indexOf(algoList[i]) === -1) throw new Error('Unsupported HMAC algorithm: ' + algoList[i]); } algorithms.hmac = algoList; } algoList = cfg.algorithms.compress; if (Array.isArray(algoList) && algoList.length > 0) { algosSupported = ALGORITHMS.SUPPORTED_COMPRESS; for (i = 0; i < algoList.length; ++i) { if (algosSupported.indexOf(algoList[i]) === -1) throw new Error('Unsupported compression algorithm: ' + algoList[i]); } algorithms.compress = algoList; } } // Make sure we at least have some kind of valid list of support key // formats if (algorithms.serverHostKey === undefined) { var hostKeyAlgos = Object.keys(hostKeys); for (i = hostKeyAlgos.length - 1; i >= 0; --i) { if (!hostKeys[hostKeyAlgos[i]]) hostKeyAlgos.splice(i, 1); } algorithms.serverHostKey = hostKeyAlgos; } if (!kaMgr && Server.KEEPALIVE_INTERVAL > 0 && Server.KEEPALIVE_CLIENT_INTERVAL > 0 && Server.KEEPALIVE_CLIENT_COUNT_MAX >= 0) { kaMgr = new KeepaliveManager(Server.KEEPALIVE_INTERVAL, Server.KEEPALIVE_CLIENT_INTERVAL, Server.KEEPALIVE_CLIENT_COUNT_MAX); } var self = this; EventEmitter.call(this); if (typeof listener === 'function') self.on('connection', listener); var streamcfg = { algorithms: algorithms, hostKeys: hostKeys, server: true }; var keys; var len; for (i = 0, keys = Object.keys(cfg), len = keys.length; i < len; ++i) { var key = keys[i]; if (key === 'privateKey' || key === 'publicKey' || key === 'passphrase' || key === 'algorithms' || key === 'hostKeys' || key === 'server') { continue; } streamcfg[key] = cfg[key]; } if (typeof streamcfg.debug === 'function') { var oldDebug = streamcfg.debug; var cfgKeys = Object.keys(streamcfg); } this._srv = new net.Server(function(socket) { if (self._connections >= self.maxConnections) { socket.destroy(); return; } ++self._connections; socket.once('close', function(had_err) { --self._connections; // since joyent/node#993bb93e0a, we have to "read past EOF" in order to // get an `end` event on streams. thankfully adding this does not // negatively affect node versions pre-joyent/node#993bb93e0a. sshstream.read(); }).on('error', function(err) { sshstream.reset(); sshstream.emit('error', err); }); var conncfg = streamcfg; // prepend debug output with a unique identifier in case there are multiple // clients connected at the same time if (oldDebug) { conncfg = {}; for (var i = 0, key; i < cfgKeys.length; ++i) { key = cfgKeys[i]; conncfg[key] = streamcfg[key]; } var debugPrefix = '[' + process.hrtime().join('.') + '] '; conncfg.debug = function(msg) { oldDebug(debugPrefix + msg); }; } var sshstream = new SSH2Stream(conncfg); var client = new Client(sshstream, socket); socket.pipe(sshstream).pipe(socket); // silence pre-header errors function onClientPreHeaderError(err) {} client.on('error', onClientPreHeaderError); sshstream.once('header', function(header) { if (sshstream._readableState.ended) { // already disconnected internally in SSH2Stream due to incompatible // protocol version return; } else if (!listenerCount(self, 'connection')) { // auto reject return sshstream.disconnect(DISCONNECT_REASON.BY_APPLICATION); } client.removeListener('error', onClientPreHeaderError); self.emit('connection', client, { ip: socket.remoteAddress, family: socket.remoteFamily, port: socket.remotePort, header: header }); }); }).on('error', function(err) { self.emit('error', err); }).on('listening', function() { self.emit('listening'); }).on('close', function() { self.emit('close'); }); this._connections = 0; this.maxConnections = Infinity; } inherits(Server, EventEmitter); Server.prototype.listen = function() { this._srv.listen.apply(this._srv, arguments); return this; }; Server.prototype.address = function() { return this._srv.address(); }; Server.prototype.getConnections = function(cb) { this._srv.getConnections(cb); }; Server.prototype.close = function(cb) { this._srv.close(cb); return this; }; Server.prototype.ref = function() { this._srv.ref(); }; Server.prototype.unref = function() { this._srv.unref(); }; function Client(stream, socket) { EventEmitter.call(this); var self = this; this._sshstream = stream; var channels = this._channels = {}; this._curChan = -1; this._sock = socket; this.noMoreSessions = false; this.authenticated = false; stream.on('end', function() { socket.resume(); self.emit('end'); }).on('close', function(hasErr) { self.emit('close', hasErr); }).on('error', function(err) { self.emit('error', err); }).on('drain', function() { self.emit('drain'); }).on('continue', function() { self.emit('continue'); }); var exchanges = 0; var acceptedAuthSvc = false; var pendingAuths = []; var authCtx; // begin service/auth-related ================================================ stream.on('SERVICE_REQUEST', function(service) { if (exchanges === 0 || acceptedAuthSvc || self.authenticated || service !== 'ssh-userauth') return stream.disconnect(DISCONNECT_REASON.SERVICE_NOT_AVAILABLE); acceptedAuthSvc = true; stream.serviceAccept(service); }).on('USERAUTH_REQUEST', onUSERAUTH_REQUEST); function onUSERAUTH_REQUEST(username, service, method, methodData) { if (exchanges === 0 || (authCtx && (authCtx.username !== username || authCtx.service !== service)) // TODO: support hostbased auth || (method !== 'password' && method !== 'publickey' && method !== 'hostbased' && method !== 'keyboard-interactive' && method !== 'none') || pendingAuths.length === MAX_PENDING_AUTHS) return stream.disconnect(DISCONNECT_REASON.PROTOCOL_ERROR); else if (service !== 'ssh-connection') return stream.disconnect(DISCONNECT_REASON.SERVICE_NOT_AVAILABLE); // XXX: this really shouldn't be reaching into private state ... stream._state.authMethod = method; var ctx; if (method === 'keyboard-interactive') { ctx = new KeyboardAuthContext(stream, username, service, method, methodData, onAuthDecide); } else if (method === 'publickey') { ctx = new PKAuthContext(stream, username, service, method, methodData, onAuthDecide); } else if (method === 'hostbased') { ctx = new HostbasedAuthContext(stream, username, service, method, methodData, onAuthDecide); } else if (method === 'password') { ctx = new PwdAuthContext(stream, username, service, method, methodData, onAuthDecide); } else if (method === 'none') ctx = new AuthContext(stream, username, service, method, onAuthDecide); if (authCtx) { if (!authCtx._initialResponse) return pendingAuths.push(ctx); else if (authCtx._multistep && !this._finalResponse) { // RFC 4252 says to silently abort the current auth request if a new // auth request comes in before the final response from an auth method // that requires additional request/response exchanges -- this means // keyboard-interactive for now ... authCtx._cleanup && authCtx._cleanup(); authCtx.emit('abort'); } } authCtx = ctx; if (listenerCount(self, 'authentication')) self.emit('authentication', authCtx); else authCtx.reject(); } function onAuthDecide(ctx, allowed, methodsLeft, isPartial) { if (authCtx === ctx && !self.authenticated) { if (allowed) { stream.removeListener('USERAUTH_REQUEST', onUSERAUTH_REQUEST); authCtx = undefined; self.authenticated = true; stream.authSuccess(); pendingAuths = []; self.emit('ready'); } else { stream.authFailure(methodsLeft, isPartial); if (pendingAuths.length) { authCtx = pendingAuths.pop(); if (listenerCount(self, 'authentication')) self.emit('authentication', authCtx); else authCtx.reject(); } } } } // end service/auth-related ================================================== var unsentGlobalRequestsReplies = []; function sendReplies() { var reply; while (unsentGlobalRequestsReplies.length > 0 && unsentGlobalRequestsReplies[0].type) { reply = unsentGlobalRequestsReplies.shift(); if (reply.type === 'SUCCESS') stream.requestSuccess(reply.buf); if (reply.type === 'FAILURE') stream.requestFailure(); } } stream.on('GLOBAL_REQUEST', function(name, wantReply, data) { var reply = { type: null, buf: null }; function setReply(type, buf) { reply.type = type; reply.buf = buf; sendReplies(); } if (wantReply) unsentGlobalRequestsReplies.push(reply); if ((name === 'tcpip-forward' || name === 'cancel-tcpip-forward' || name === 'no-more-sessions@openssh.com' || name === 'streamlocal-forward@openssh.com' || name === 'cancel-streamlocal-forward@openssh.com') && listenerCount(self, 'request') && self.authenticated) { var accept; var reject; if (wantReply) { var replied = false; accept = function(chosenPort) { if (replied) return; replied = true; var bufPort; if (name === 'tcpip-forward' && data.bindPort === 0 && typeof chosenPort === 'number') { bufPort = Buffer.allocUnsafe(4); writeUInt32BE(bufPort, chosenPort, 0); } setReply('SUCCESS', bufPort); }; reject = function() { if (replied) return; replied = true; setReply('FAILURE'); }; } if (name === 'no-more-sessions@openssh.com') { self.noMoreSessions = true; accept && accept(); return; } self.emit('request', accept, reject, name, data); } else if (wantReply) setReply('FAILURE'); }); stream.on('CHANNEL_OPEN', function(info) { // do early reject in some cases to prevent wasteful channel allocation if ((info.type === 'session' && self.noMoreSessions) || !self.authenticated) { var reasonCode = CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED; return stream.channelOpenFail(info.sender, reasonCode); } var localChan = nextChannel(self); var accept; var reject; var replied = false; if (localChan === false) { // auto-reject due to no channels available return stream.channelOpenFail(info.sender, CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE); } // be optimistic, reserve channel to prevent another request from trying to // take the same channel channels[localChan] = true; reject = function() { if (replied) return; replied = true; delete channels[localChan]; var reasonCode = CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED; return stream.channelOpenFail(info.sender, reasonCode); }; switch (info.type) { case 'session': if (listenerCount(self, 'session')) { accept = function() { if (replied) return; replied = true; stream.channelOpenConfirm(info.sender, localChan, Channel.MAX_WINDOW, Channel.PACKET_SIZE); return new Session(self, info, localChan); }; self.emit('session', accept, reject); } else reject(); break; case 'direct-tcpip': if (listenerCount(self, 'tcpip')) { accept = function() { if (replied) return; replied = true; stream.channelOpenConfirm(info.sender, localChan, Channel.MAX_WINDOW, Channel.PACKET_SIZE); var chaninfo = { type: undefined, incoming: { id: localChan, window: Channel.MAX_WINDOW, packetSize: Channel.PACKET_SIZE, state: 'open' }, outgoing: { id: info.sender, window: info.window, packetSize: info.packetSize, state: 'open' } }; return new Channel(chaninfo, self); }; self.emit('tcpip', accept, reject, info.data); } else reject(); break; case 'direct-streamlocal@openssh.com': if (listenerCount(self, 'openssh.streamlocal')) { accept = function() { if (replied) return; replied = true; stream.channelOpenConfirm(info.sender, localChan, Channel.MAX_WINDOW, Channel.PACKET_SIZE); var chaninfo = { type: undefined, incoming: { id: localChan, window: Channel.MAX_WINDOW, packetSize: Channel.PACKET_SIZE, state: 'open' }, outgoing: { id: info.sender, window: info.window, packetSize: info.packetSize, state: 'open' } }; return new Channel(chaninfo, self); }; self.emit('openssh.streamlocal', accept, reject, info.data); } else reject(); break; default: // auto-reject unsupported channel types reject(); } }); stream.on('NEWKEYS', function() { if (++exchanges > 1) self.emit('rekey'); }); if (kaMgr) { this.once('ready', function() { kaMgr.add(stream); }); } } inherits(Client, EventEmitter); Client.prototype.end = function() { return this._sshstream.disconnect(DISCONNECT_REASON.BY_APPLICATION); }; Client.prototype.x11 = function(originAddr, originPort, cb) { var opts = { originAddr: originAddr, originPort: originPort }; return openChannel(this, 'x11', opts, cb); }; Client.prototype.forwardOut = function(boundAddr, boundPort, remoteAddr, remotePort, cb) { var opts = { boundAddr: boundAddr, boundPort: boundPort, remoteAddr: remoteAddr, remotePort: remotePort }; return openChannel(this, 'forwarded-tcpip', opts, cb); }; Client.prototype.openssh_forwardOutStreamLocal = function(socketPath, cb) { var opts = { socketPath: socketPath }; return openChannel(this, 'forwarded-streamlocal@openssh.com', opts, cb); }; Client.prototype.rekey = function(cb) { var stream = this._sshstream; var ret = true; var error; try { ret = stream.rekey(); } catch (ex) { error = ex; } // TODO: re-throw error if no callback? if (typeof cb === 'function') { if (error) { process.nextTick(function() { cb(error); }); } else this.once('rekey', cb); } return ret; }; function Session(client, info, localChan) { this.subtype = undefined; var ending = false; var self = this; var outgoingId = info.sender; var channel; var chaninfo = { type: 'session', incoming: { id: localChan, window: Channel.MAX_WINDOW, packetSize: Channel.PACKET_SIZE, state: 'open' }, outgoing: { id: info.sender, window: info.window, packetSize: info.packetSize, state: 'open' } }; function onREQUEST(info) { var replied = false; var accept; var reject; if (info.wantReply) { // "real session" requests will have custom accept behaviors if (info.request !== 'shell' && info.request !== 'exec' && info.request !== 'subsystem') { accept = function() { if (replied || ending || channel) return; replied = true; return client._sshstream.channelSuccess(outgoingId); }; } reject = function() { if (replied || ending || channel) return; replied = true; return client._sshstream.channelFailure(outgoingId); }; } if (ending) { reject && reject(); return; } switch (info.request) { // "pre-real session start" requests case 'env': if (listenerCount(self, 'env')) { self.emit('env', accept, reject, { key: info.key, val: info.val }); } else reject && reject(); break; case 'pty-req': if (listenerCount(self, 'pty')) { self.emit('pty', accept, reject, { cols: info.cols, rows: info.rows, width: info.width, height: info.height, term: info.term, modes: info.modes, }); } else reject && reject(); break; case 'window-change': if (listenerCount(self, 'window-change')) { self.emit('window-change', accept, reject, { cols: info.cols, rows: info.rows, width: info.width, height: info.height }); } else reject && reject(); break; case 'x11-req': if (listenerCount(self, 'x11')) { self.emit('x11', accept, reject, { single: info.single, protocol: info.protocol, cookie: info.cookie, screen: info.screen }); } else reject && reject(); break; // "post-real session start" requests case 'signal': if (listenerCount(self, 'signal')) { self.emit('signal', accept, reject, { name: info.signal }); } else reject && reject(); break; // XXX: is `auth-agent-req@openssh.com` really "post-real session start"? case 'auth-agent-req@openssh.com': if (listenerCount(self, 'auth-agent')) self.emit('auth-agent', accept, reject); else reject && reject(); break; // "real session start" requests case 'shell': if (listenerCount(self, 'shell')) { accept = function() { if (replied || ending || channel) return; replied = true; if (info.wantReply) client._sshstream.channelSuccess(outgoingId); channel = new Channel(chaninfo, client, { server: true }); channel.subtype = self.subtype = info.request; return channel; }; self.emit('shell', accept, reject); } else reject && reject(); break; case 'exec': if (listenerCount(self, 'exec')) { accept = function() { if (replied || ending || channel) return; replied = true; if (info.wantReply) client._sshstream.channelSuccess(outgoingId); channel = new Channel(chaninfo, client, { server: true }); channel.subtype = self.subtype = info.request; return channel; }; self.emit('exec', accept, reject, { command: info.command }); } else reject && reject(); break; case 'subsystem': accept = function() { if (replied || ending || channel) return; replied = true; if (info.wantReply) client._sshstream.channelSuccess(outgoingId); channel = new Channel(chaninfo, client, { server: true }); channel.subtype = self.subtype = (info.request + ':' + info.subsystem); if (info.subsystem === 'sftp') { var sftp = new SFTPStream({ server: true, debug: client._sshstream.debug }); channel.pipe(sftp).pipe(channel); return sftp; } else return channel; }; if (info.subsystem === 'sftp' && listenerCount(self, 'sftp')) self.emit('sftp', accept, reject); else if (info.subsystem !== 'sftp' && listenerCount(self, 'subsystem')) { self.emit('subsystem', accept, reject, { name: info.subsystem }); } else reject && reject(); break; default: reject && reject(); } } function onEOF() { ending = true; self.emit('eof'); self.emit('end'); } function onCLOSE() { ending = true; self.emit('close'); } client._sshstream .on('CHANNEL_REQUEST:' + localChan, onREQUEST) .once('CHANNEL_EOF:' + localChan, onEOF) .once('CHANNEL_CLOSE:' + localChan, onCLOSE); } inherits(Session, EventEmitter); function AuthContext(stream, username, service, method, cb) { EventEmitter.call(this); var self = this; this.username = this.user = username; this.service = service; this.method = method; this._initialResponse = false; this._finalResponse = false; this._multistep = false; this._cbfinal = function(allowed, methodsLeft, isPartial) { if (!self._finalResponse) { self._finalResponse = true; cb(self, allowed, methodsLeft, isPartial); } }; this._stream = stream; } inherits(AuthContext, EventEmitter); AuthContext.prototype.accept = function() { this._cleanup && this._cleanup(); this._initialResponse = true; this._cbfinal(true); }; AuthContext.prototype.reject = function(methodsLeft, isPartial) { this._cleanup && this._cleanup(); this._initialResponse = true; this._cbfinal(false, methodsLeft, isPartial); }; var RE_KBINT_SUBMETHODS = /[ \t\r\n]*,[ \t\r\n]*/g; function KeyboardAuthContext(stream, username, service, method, submethods, cb) { AuthContext.call(this, stream, username, service, method, cb); this._multistep = true; var self = this; this._cb = undefined; this._onInfoResponse = function(responses) { if (self._cb) { var callback = self._cb; self._cb = undefined; callback(responses); } }; this.submethods = submethods.split(RE_KBINT_SUBMETHODS); this.on('abort', function() { self._cb && self._cb(new Error('Authentication request aborted')); }); } inherits(KeyboardAuthContext, AuthContext); KeyboardAuthContext.prototype._cleanup = function() { this._stream.removeListener('USERAUTH_INFO_RESPONSE', this._onInfoResponse); }; KeyboardAuthContext.prototype.prompt = function(prompts, title, instructions, cb) { if (!Array.isArray(prompts)) prompts = [ prompts ]; if (typeof title === 'function') { cb = title; title = instructions = undefined; } else if (typeof instructions === 'function') { cb = instructions; instructions = undefined; } for (var i = 0; i < prompts.length; ++i) { if (typeof prompts[i] === 'string') { prompts[i] = { prompt: prompts[i], echo: true }; } } this._cb = cb; this._initialResponse = true; this._stream.once('USERAUTH_INFO_RESPONSE', this._onInfoResponse); return this._stream.authInfoReq(title, instructions, prompts); }; function PKAuthContext(stream, username, service, method, pkInfo, cb) { AuthContext.call(this, stream, username, service, method, cb); this.key = { algo: pkInfo.keyAlgo, data: pkInfo.key }; this.signature = pkInfo.signature; var sigAlgo; if (this.signature) { // TODO: move key type checking logic to ssh2-streams switch (pkInfo.keyAlgo) { case 'ssh-rsa': case 'ssh-dss': sigAlgo = 'sha1'; break; case 'ssh-ed25519': sigAlgo = null; break; case 'ecdsa-sha2-nistp256': sigAlgo = 'sha256'; break; case 'ecdsa-sha2-nistp384': sigAlgo = 'sha384'; break; case 'ecdsa-sha2-nistp521': sigAlgo = 'sha512'; break; } } this.sigAlgo = sigAlgo; this.blob = pkInfo.blob; } inherits(PKAuthContext, AuthContext); PKAuthContext.prototype.accept = function() { if (!this.signature) { this._initialResponse = true; this._stream.authPKOK(this.key.algo, this.key.data); } else { AuthContext.prototype.accept.call(this); } }; function HostbasedAuthContext(stream, username, service, method, pkInfo, cb) { AuthContext.call(this, stream, username, service, method, cb); this.key = { algo: pkInfo.keyAlgo, data: pkInfo.key }; this.signature = pkInfo.signature; var sigAlgo; if (this.signature) { // TODO: move key type checking logic to ssh2-streams switch (pkInfo.keyAlgo) { case 'ssh-rsa': case 'ssh-dss': sigAlgo = 'sha1'; break; case 'ssh-ed25519': sigAlgo = null; break; case 'ecdsa-sha2-nistp256': sigAlgo = 'sha256'; break; case 'ecdsa-sha2-nistp384': sigAlgo = 'sha384'; break; case 'ecdsa-sha2-nistp521': sigAlgo = 'sha512'; break; } } this.sigAlgo = sigAlgo; this.blob = pkInfo.blob; this.localHostname = pkInfo.localHostname; this.localUsername = pkInfo.localUsername; } inherits(HostbasedAuthContext, AuthContext); function PwdAuthContext(stream, username, service, method, password, cb) { AuthContext.call(this, stream, username, service, method, cb); this.password = password; } inherits(PwdAuthContext, AuthContext); function openChannel(self, type, opts, cb) { // ask the client to open a channel for some purpose // (e.g. a forwarded TCP connection) var localChan = nextChannel(self); var initWindow = Channel.MAX_WINDOW; var maxPacket = Channel.PACKET_SIZE; var ret = true; if (localChan === false) return cb(new Error('No free channels available')); if (typeof opts === 'function') { cb = opts; opts = {}; } self._channels[localChan] = true; var sshstream = self._sshstream; sshstream.once('CHANNEL_OPEN_CONFIRMATION:' + localChan, function(info) { sshstream.removeAllListeners('CHANNEL_OPEN_FAILURE:' + localChan); var chaninfo = { type: type, incoming: { id: localChan, window: initWindow, packetSize: maxPacket, state: 'open' }, outgoing: { id: info.sender, window: info.window, packetSize: info.packetSize, state: 'open' } }; cb(undefined, new Channel(chaninfo, self, { server: true })); }).once('CHANNEL_OPEN_FAILURE:' + localChan, function(info) { sshstream.removeAllListeners('CHANNEL_OPEN_CONFIRMATION:' + localChan); delete self._channels[localChan]; var err = new Error('(SSH) Channel open failure: ' + info.description); err.reason = info.reason; err.lang = info.lang; cb(err); }); if (type === 'forwarded-tcpip') ret = sshstream.forwardedTcpip(localChan, initWindow, maxPacket, opts); else if (type === 'x11') ret = sshstream.x11(localChan, initWindow, maxPacket, opts); else if (type === 'forwarded-streamlocal@openssh.com') { ret = sshstream.openssh_forwardedStreamLocal(localChan, initWindow, maxPacket, opts); } return ret; } function nextChannel(self) { // get the next available channel number // fast path if (self._curChan < MAX_CHANNEL) return ++self._curChan; // slower lookup path for (var i = 0, channels = self._channels; i < MAX_CHANNEL; ++i) if (!channels[i]) return i; return false; } Server.createServer = function(cfg, listener) { return new Server(cfg, listener); }; Server.KEEPALIVE_INTERVAL = 1000; Server.KEEPALIVE_CLIENT_INTERVAL = 15000; Server.KEEPALIVE_CLIENT_COUNT_MAX = 3; module.exports = Server; module.exports.IncomingClient = Client;