// TODO: // * convert listenerCount() usage to emit() return value checking? // * emit error when connection severed early (e.g. before handshake) // * add '.connected' or similar property to connection objects to allow // immediate connection status checking 'use strict' require('./buffer-polyfill.js') const { Server: netServer } = require('bare-node-net') const EventEmitter = require('bare-node-events') const { listenerCount } = EventEmitter const { CHANNEL_OPEN_FAILURE, DEFAULT_CIPHER, DEFAULT_COMPRESSION, DEFAULT_KEX, DEFAULT_MAC, DEFAULT_SERVER_HOST_KEY, DISCONNECT_REASON, DISCONNECT_REASON_BY_VALUE, SUPPORTED_CIPHER, SUPPORTED_COMPRESSION, SUPPORTED_KEX, SUPPORTED_MAC, SUPPORTED_SERVER_HOST_KEY } = require('./protocol/constants.js') const { init: cryptoInit } = require('./protocol/crypto.js') const { KexInit } = require('./protocol/kex.js') const { parseKey } = require('./protocol/keyParser.js') const Protocol = require('./protocol/Protocol.js') const { SFTP } = require('./protocol/SFTP.js') const { writeUInt32BE } = require('./protocol/utils.js') const { Channel, MAX_WINDOW, PACKET_SIZE, windowAdjust, WINDOW_THRESHOLD } = require('./Channel.js') const { ChannelManager, generateAlgorithmList, isWritable, onChannelOpenFailure, onCHANNEL_CLOSE } = require('./utils.js') const MAX_PENDING_AUTHS = 10 class AuthContext extends EventEmitter { constructor(protocol, username, service, method, cb) { super() this.username = this.user = username this.service = service this.method = method this._initialResponse = false this._finalResponse = false this._multistep = false this._cbfinal = (allowed, methodsLeft, isPartial) => { if (!this._finalResponse) { this._finalResponse = true cb(this, allowed, methodsLeft, isPartial) } } this._protocol = protocol } accept() { this._cleanup && this._cleanup() this._initialResponse = true this._cbfinal(true) } reject(methodsLeft, isPartial) { this._cleanup && this._cleanup() this._initialResponse = true this._cbfinal(false, methodsLeft, isPartial) } } class KeyboardAuthContext extends AuthContext { constructor(protocol, username, service, method, submethods, cb) { super(protocol, username, service, method, cb) this._multistep = true this._cb = undefined this._onInfoResponse = (responses) => { const callback = this._cb if (callback) { this._cb = undefined callback(responses) } } this.submethods = submethods this.on('abort', () => { this._cb && this._cb(new Error('Authentication request aborted')) }) } prompt(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 } else if (typeof cb !== 'function') { cb = undefined } for (let 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._protocol.authInfoReq(title, instructions, prompts) } } class PKAuthContext extends AuthContext { constructor(protocol, username, service, method, pkInfo, cb) { super(protocol, username, service, method, cb) this.key = { algo: pkInfo.keyAlgo, data: pkInfo.key } this.hashAlgo = pkInfo.hashAlgo this.signature = pkInfo.signature this.blob = pkInfo.blob } accept() { if (!this.signature) { this._initialResponse = true this._protocol.authPKOK(this.key.algo, this.key.data) } else { AuthContext.prototype.accept.call(this) } } } class HostbasedAuthContext extends AuthContext { constructor(protocol, username, service, method, pkInfo, cb) { super(protocol, username, service, method, cb) this.key = { algo: pkInfo.keyAlgo, data: pkInfo.key } this.hashAlgo = pkInfo.hashAlgo this.signature = pkInfo.signature this.blob = pkInfo.blob this.localHostname = pkInfo.localHostname this.localUsername = pkInfo.localUsername } } class PwdAuthContext extends AuthContext { constructor(protocol, username, service, method, password, cb) { super(protocol, username, service, method, cb) this.password = password this._changeCb = undefined } requestChange(prompt, cb) { if (this._changeCb) throw new Error('Change request already in progress') if (typeof prompt !== 'string') throw new Error('prompt argument must be a string') if (typeof cb !== 'function') throw new Error('Callback argument must be a function') this._changeCb = cb this._protocol.authPasswdChg(prompt) } } class Session extends EventEmitter { constructor(client, info, localChan) { super() this.type = 'session' this.subtype = undefined this.server = true this._ending = false this._channel = undefined this._chanInfo = { type: 'session', incoming: { id: localChan, window: MAX_WINDOW, packetSize: PACKET_SIZE, state: 'open' }, outgoing: { id: info.sender, window: info.window, packetSize: info.packetSize, state: 'open' } } } } class Server extends EventEmitter { constructor(cfg, listener) { super() if (typeof cfg !== 'object' || cfg === null) throw new Error('Missing configuration object') const hostKeys = Object.create(null) const hostKeyAlgoOrder = [] const hostKeys_ = cfg.hostKeys if (!Array.isArray(hostKeys_)) throw new Error('hostKeys must be an array') const cfgAlgos = typeof cfg.algorithms === 'object' && cfg.algorithms !== null ? cfg.algorithms : {} const hostKeyAlgos = generateAlgorithmList( cfgAlgos.serverHostKey, DEFAULT_SERVER_HOST_KEY, SUPPORTED_SERVER_HOST_KEY ) for (let i = 0; i < hostKeys_.length; ++i) { let 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 (Array.isArray(privateKey)) { // OpenSSH's newer format only stores 1 key for now privateKey = privateKey[0] } if (privateKey.getPrivatePEM() === null) throw new Error('privateKey value contains an invalid private key') // Discard key if we already found a key of the same type if (hostKeyAlgoOrder.includes(privateKey.type)) continue if (privateKey.type === 'ssh-rsa') { // SSH supports multiple signature hashing algorithms for RSA, so we add // the algorithms in the desired order let sha1Pos = hostKeyAlgos.indexOf('ssh-rsa') const sha256Pos = hostKeyAlgos.indexOf('rsa-sha2-256') const sha512Pos = hostKeyAlgos.indexOf('rsa-sha2-512') if (sha1Pos === -1) { // Fall back to giving SHA1 the lowest priority sha1Pos = Infinity } ;[sha1Pos, sha256Pos, sha512Pos].sort(compareNumbers).forEach((pos) => { if (pos === -1) return let type switch (pos) { case sha1Pos: type = 'ssh-rsa' break case sha256Pos: type = 'rsa-sha2-256' break case sha512Pos: type = 'rsa-sha2-512' break default: return } // Store same RSA key under each hash algorithm name for convenience hostKeys[type] = privateKey hostKeyAlgoOrder.push(type) }) } else { hostKeys[privateKey.type] = privateKey hostKeyAlgoOrder.push(privateKey.type) } } const algorithms = { kex: generateAlgorithmList(cfgAlgos.kex, DEFAULT_KEX, SUPPORTED_KEX).concat([ 'kex-strict-s-v00@openssh.com' ]), serverHostKey: hostKeyAlgoOrder, cs: { cipher: generateAlgorithmList(cfgAlgos.cipher, DEFAULT_CIPHER, SUPPORTED_CIPHER), mac: generateAlgorithmList(cfgAlgos.hmac, DEFAULT_MAC, SUPPORTED_MAC), compress: generateAlgorithmList( cfgAlgos.compress, DEFAULT_COMPRESSION, SUPPORTED_COMPRESSION ), lang: [] }, sc: undefined } algorithms.sc = algorithms.cs if (typeof listener === 'function') this.on('connection', listener) const origDebug = typeof cfg.debug === 'function' ? cfg.debug : undefined const ident = cfg.ident ? Buffer.from(cfg.ident) : undefined const offer = new KexInit(algorithms) this._srv = new netServer((socket) => { if (this._connections >= this.maxConnections) { socket.destroy() return } ++this._connections socket.once('close', () => { --this._connections }) let debug if (origDebug) { // Prepend debug output with a unique identifier in case there are // multiple clients connected at the same time const debugPrefix = `[${process.hrtime().join('.')}] ` debug = (msg) => { origDebug(`${debugPrefix}${msg}`) } } // eslint-disable-next-line no-use-before-define new Client(socket, hostKeys, ident, offer, debug, this, cfg) }) .on('error', (err) => { this.emit('error', err) }) .on('listening', () => { this.emit('listening') }) .on('close', () => { this.emit('close') }) this._connections = 0 this.maxConnections = Infinity } injectSocket(socket) { this._srv.emit('connection', socket) } listen(...args) { this._srv.listen(...args) return this } address() { return this._srv.address() } getConnections(cb) { this._srv.getConnections(cb) return this } close(cb) { this._srv.close(cb) return this } ref() { this._srv.ref() return this } unref() { this._srv.unref() return this } } Server.KEEPALIVE_CLIENT_INTERVAL = 15000 Server.KEEPALIVE_CLIENT_COUNT_MAX = 3 class Client extends EventEmitter { constructor(socket, hostKeys, ident, offer, debug, server, srvCfg) { super() let exchanges = 0 let acceptedAuthSvc = false let pendingAuths = [] let authCtx let kaTimer let onPacket const unsentGlobalRequestsReplies = [] this._sock = socket this._chanMgr = new ChannelManager(this) this._debug = debug this.noMoreSessions = false this.authenticated = false // Silence pre-header errors function onClientPreHeaderError(err) {} this.on('error', onClientPreHeaderError) const DEBUG_HANDLER = !debug ? undefined : (p, display, msg) => { debug(`Debug output from client: ${JSON.stringify(msg)}`) } const kaIntvl = typeof srvCfg.keepaliveInterval === 'number' && isFinite(srvCfg.keepaliveInterval) && srvCfg.keepaliveInterval > 0 ? srvCfg.keepaliveInterval : typeof Server.KEEPALIVE_CLIENT_INTERVAL === 'number' && isFinite(Server.KEEPALIVE_CLIENT_INTERVAL) && Server.KEEPALIVE_CLIENT_INTERVAL > 0 ? Server.KEEPALIVE_CLIENT_INTERVAL : -1 const kaCountMax = typeof srvCfg.keepaliveCountMax === 'number' && isFinite(srvCfg.keepaliveCountMax) && srvCfg.keepaliveCountMax >= 0 ? srvCfg.keepaliveCountMax : typeof Server.KEEPALIVE_CLIENT_COUNT_MAX === 'number' && isFinite(Server.KEEPALIVE_CLIENT_COUNT_MAX) && Server.KEEPALIVE_CLIENT_COUNT_MAX >= 0 ? Server.KEEPALIVE_CLIENT_COUNT_MAX : -1 let kaCurCount = 0 if (kaIntvl !== -1 && kaCountMax !== -1) { this.once('ready', () => { const onClose = () => { clearInterval(kaTimer) } this.on('close', onClose).on('end', onClose) kaTimer = setInterval(() => { if (++kaCurCount > kaCountMax) { clearInterval(kaTimer) const err = new Error('Keepalive timeout') err.level = 'client-timeout' this.emit('error', err) this.end() } else { // XXX: if the server ever starts sending real global requests to // the client, we will need to add a dummy callback here to // keep the correct reply order proto.ping() } }, kaIntvl) }) // TODO: re-verify keepalive behavior with OpenSSH onPacket = () => { kaTimer && kaTimer.refresh() kaCurCount = 0 } } const proto = (this._protocol = new Protocol({ server: true, hostKeys, ident, offer, onPacket, greeting: srvCfg.greeting, banner: srvCfg.banner, onWrite: (data) => { if (isWritable(socket)) socket.write(data) }, onError: (err) => { if (!proto._destruct) socket.removeAllListeners('data') this.emit('error', err) try { socket.end() } catch {} }, onHeader: (header) => { this.removeListener('error', onClientPreHeaderError) const info = { ip: socket.remoteAddress, family: socket.remoteFamily, port: socket.remotePort, header } if (!server.emit('connection', this, info)) { // auto reject proto.disconnect(DISCONNECT_REASON.BY_APPLICATION) socket.end() return } if (header.greeting) this.emit('greeting', header.greeting) }, onHandshakeComplete: (negotiated) => { if (++exchanges > 1) this.emit('rekey') this.emit('handshake', negotiated) }, debug, messageHandlers: { DEBUG: DEBUG_HANDLER, DISCONNECT: (p, reason, desc) => { if (reason !== DISCONNECT_REASON.BY_APPLICATION) { if (!desc) { desc = DISCONNECT_REASON_BY_VALUE[reason] if (desc === undefined) desc = `Unexpected disconnection reason: ${reason}` } const err = new Error(desc) err.code = reason this.emit('error', err) } socket.end() }, CHANNEL_OPEN: (p, info) => { // Handle incoming requests from client // Do early reject in some cases to prevent wasteful channel // allocation if ((info.type === 'session' && this.noMoreSessions) || !this.authenticated) { const reasonCode = CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED return proto.channelOpenFail(info.sender, reasonCode) } let localChan = -1 let reason let replied = false let accept const reject = () => { if (replied) return replied = true if (reason === undefined) { if (localChan === -1) reason = CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE else reason = CHANNEL_OPEN_FAILURE.CONNECT_FAILED } if (localChan !== -1) this._chanMgr.remove(localChan) proto.channelOpenFail(info.sender, reason, '') } const reserveChannel = () => { localChan = this._chanMgr.add() if (localChan === -1) { reason = CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE if (debug) { debug('Automatic rejection of incoming channel open: ' + 'no channels available') } } return localChan !== -1 } const data = info.data switch (info.type) { case 'session': if (listenerCount(this, 'session') && reserveChannel()) { accept = () => { if (replied) return replied = true const instance = new Session(this, info, localChan) this._chanMgr.update(localChan, instance) proto.channelOpenConfirm(info.sender, localChan, MAX_WINDOW, PACKET_SIZE) return instance } this.emit('session', accept, reject) return } break case 'direct-tcpip': if (listenerCount(this, 'tcpip') && reserveChannel()) { accept = () => { if (replied) return replied = true const chanInfo = { type: undefined, incoming: { id: localChan, window: MAX_WINDOW, packetSize: PACKET_SIZE, state: 'open' }, outgoing: { id: info.sender, window: info.window, packetSize: info.packetSize, state: 'open' } } const stream = new Channel(this, chanInfo, { server: true }) this._chanMgr.update(localChan, stream) proto.channelOpenConfirm(info.sender, localChan, MAX_WINDOW, PACKET_SIZE) return stream } this.emit('tcpip', accept, reject, data) return } break case 'direct-streamlocal@openssh.com': if (listenerCount(this, 'openssh.streamlocal') && reserveChannel()) { accept = () => { if (replied) return replied = true const chanInfo = { type: undefined, incoming: { id: localChan, window: MAX_WINDOW, packetSize: PACKET_SIZE, state: 'open' }, outgoing: { id: info.sender, window: info.window, packetSize: info.packetSize, state: 'open' } } const stream = new Channel(this, chanInfo, { server: true }) this._chanMgr.update(localChan, stream) proto.channelOpenConfirm(info.sender, localChan, MAX_WINDOW, PACKET_SIZE) return stream } this.emit('openssh.streamlocal', accept, reject, data) return } break default: // Automatically reject any unsupported channel open requests reason = CHANNEL_OPEN_FAILURE.UNKNOWN_CHANNEL_TYPE if (debug) { debug( 'Automatic rejection of unsupported incoming channel open' + ` type: ${info.type}` ) } } if (reason === undefined) { reason = CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED if (debug) { debug( 'Automatic rejection of unexpected incoming channel open' + ` for: ${info.type}` ) } } reject() }, CHANNEL_OPEN_CONFIRMATION: (p, info) => { const channel = this._chanMgr.get(info.recipient) if (typeof channel !== 'function') return const chanInfo = { type: channel.type, incoming: { id: info.recipient, window: MAX_WINDOW, packetSize: PACKET_SIZE, state: 'open' }, outgoing: { id: info.sender, window: info.window, packetSize: info.packetSize, state: 'open' } } const instance = new Channel(this, chanInfo, { server: true }) this._chanMgr.update(info.recipient, instance) channel(undefined, instance) }, CHANNEL_OPEN_FAILURE: (p, recipient, reason, description) => { const channel = this._chanMgr.get(recipient) if (typeof channel !== 'function') return const info = { reason, description } onChannelOpenFailure(this, recipient, info, channel) }, CHANNEL_DATA: (p, recipient, data) => { let channel = this._chanMgr.get(recipient) if (typeof channel !== 'object' || channel === null) return if (channel.constructor === Session) { channel = channel._channel if (!channel) return } // The remote party should not be sending us data if there is no // window space available ... // TODO: raise error on data with not enough window? if (channel.incoming.window === 0) return channel.incoming.window -= data.length if (channel.push(data) === false) { channel._waitChanDrain = true return } if (channel.incoming.window <= WINDOW_THRESHOLD) windowAdjust(channel) }, CHANNEL_EXTENDED_DATA: (p, recipient, data, type) => { // NOOP -- should not be sent by client }, CHANNEL_WINDOW_ADJUST: (p, recipient, amount) => { let channel = this._chanMgr.get(recipient) if (typeof channel !== 'object' || channel === null) return if (channel.constructor === Session) { channel = channel._channel if (!channel) return } // The other side is allowing us to send `amount` more bytes of data channel.outgoing.window += amount if (channel._waitWindow) { channel._waitWindow = false if (channel._chunk) { channel._write(channel._chunk, null, channel._chunkcb) } else if (channel._chunkcb) { channel._chunkcb() } else if (channel._chunkErr) { channel.stderr._write(channel._chunkErr, null, channel._chunkcbErr) } else if (channel._chunkcbErr) { channel._chunkcbErr() } } }, CHANNEL_SUCCESS: (p, recipient) => { let channel = this._chanMgr.get(recipient) if (typeof channel !== 'object' || channel === null) return if (channel.constructor === Session) { channel = channel._channel if (!channel) return } if (channel._callbacks.length) channel._callbacks.shift()(false) }, CHANNEL_FAILURE: (p, recipient) => { let channel = this._chanMgr.get(recipient) if (typeof channel !== 'object' || channel === null) return if (channel.constructor === Session) { channel = channel._channel if (!channel) return } if (channel._callbacks.length) channel._callbacks.shift()(true) }, CHANNEL_REQUEST: (p, recipient, type, wantReply, data) => { const session = this._chanMgr.get(recipient) if (typeof session !== 'object' || session === null) return let replied = false let accept let reject if (session.constructor !== Session) { // normal Channel instance if (wantReply) proto.channelFailure(session.outgoing.id) return } if (wantReply) { // "real session" requests will have custom accept behaviors if (type !== 'shell' && type !== 'exec' && type !== 'subsystem') { accept = () => { if (replied || session._ending || session._channel) return replied = true proto.channelSuccess(session._chanInfo.outgoing.id) } } reject = () => { if (replied || session._ending || session._channel) return replied = true proto.channelFailure(session._chanInfo.outgoing.id) } } if (session._ending) { reject && reject() return } switch (type) { // "pre-real session start" requests case 'env': if (listenerCount(session, 'env')) { session.emit('env', accept, reject, { key: data.name, val: data.value }) return } break case 'pty-req': if (listenerCount(session, 'pty')) { session.emit('pty', accept, reject, data) return } break case 'window-change': if (listenerCount(session, 'window-change')) session.emit('window-change', accept, reject, data) else reject && reject() break case 'x11-req': if (listenerCount(session, 'x11')) { session.emit('x11', accept, reject, data) return } break // "post-real session start" requests case 'signal': if (listenerCount(session, 'signal')) { session.emit('signal', accept, reject, { name: data }) return } break // XXX: is `auth-agent-req@openssh.com` really "post-real session // start"? case 'auth-agent-req@openssh.com': if (listenerCount(session, 'auth-agent')) { session.emit('auth-agent', accept, reject) return } break // "real session start" requests case 'shell': if (listenerCount(session, 'shell')) { accept = () => { if (replied || session._ending || session._channel) return replied = true if (wantReply) proto.channelSuccess(session._chanInfo.outgoing.id) const channel = new Channel(this, session._chanInfo, { server: true }) channel.subtype = session.subtype = type session._channel = channel return channel } session.emit('shell', accept, reject) return } break case 'exec': if (listenerCount(session, 'exec')) { accept = () => { if (replied || session._ending || session._channel) return replied = true if (wantReply) proto.channelSuccess(session._chanInfo.outgoing.id) const channel = new Channel(this, session._chanInfo, { server: true }) channel.subtype = session.subtype = type session._channel = channel return channel } session.emit('exec', accept, reject, { command: data }) return } break case 'subsystem': { let useSFTP = data === 'sftp' accept = () => { if (replied || session._ending || session._channel) return replied = true if (wantReply) proto.channelSuccess(session._chanInfo.outgoing.id) let instance if (useSFTP) { instance = new SFTP(this, session._chanInfo, { server: true, debug }) } else { instance = new Channel(this, session._chanInfo, { server: true }) instance.subtype = session.subtype = `${type}:${data}` } session._channel = instance return instance } if (data === 'sftp') { if (listenerCount(session, 'sftp')) { session.emit('sftp', accept, reject) return } useSFTP = false } if (listenerCount(session, 'subsystem')) { session.emit('subsystem', accept, reject, { name: data }) return } break } } debug && debug(`Automatic rejection of incoming channel request: ${type}`) reject && reject() }, CHANNEL_EOF: (p, recipient) => { let channel = this._chanMgr.get(recipient) if (typeof channel !== 'object' || channel === null) return if (channel.constructor === Session) { if (!channel._ending) { channel._ending = true channel.emit('eof') channel.emit('end') } channel = channel._channel if (!channel) return } if (channel.incoming.state !== 'open') return channel.incoming.state = 'eof' if (channel.readable) channel.push(null) }, CHANNEL_CLOSE: (p, recipient) => { let channel = this._chanMgr.get(recipient) if (typeof channel !== 'object' || channel === null) return if (channel.constructor === Session) { channel._ending = true channel.emit('close') channel = channel._channel if (!channel) return } onCHANNEL_CLOSE(this, recipient, channel) }, // Begin service/auth-related ========================================== SERVICE_REQUEST: (p, service) => { if ( exchanges === 0 || acceptedAuthSvc || this.authenticated || service !== 'ssh-userauth' ) { proto.disconnect(DISCONNECT_REASON.SERVICE_NOT_AVAILABLE) socket.end() return } acceptedAuthSvc = true proto.serviceAccept(service) }, USERAUTH_REQUEST: (p, username, service, method, methodData) => { if ( exchanges === 0 || this.authenticated || (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 ) { proto.disconnect(DISCONNECT_REASON.PROTOCOL_ERROR) socket.end() return } else if (service !== 'ssh-connection') { proto.disconnect(DISCONNECT_REASON.SERVICE_NOT_AVAILABLE) socket.end() return } let ctx switch (method) { case 'keyboard-interactive': ctx = new KeyboardAuthContext( proto, username, service, method, methodData, onAuthDecide ) break case 'publickey': ctx = new PKAuthContext(proto, username, service, method, methodData, onAuthDecide) break case 'hostbased': ctx = new HostbasedAuthContext( proto, username, service, method, methodData, onAuthDecide ) break case 'password': if (authCtx && authCtx instanceof PwdAuthContext && authCtx._changeCb) { const cb = authCtx._changeCb authCtx._changeCb = undefined cb(methodData.newPassword) return } ctx = new PwdAuthContext(proto, username, service, method, methodData, onAuthDecide) break case 'none': ctx = new AuthContext(proto, username, service, method, onAuthDecide) break } if (authCtx) { if (!authCtx._initialResponse) { return pendingAuths.push(ctx) } else if (authCtx._multistep && !authCtx._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(this, 'authentication')) this.emit('authentication', authCtx) else authCtx.reject() }, USERAUTH_INFO_RESPONSE: (p, responses) => { if (authCtx && authCtx instanceof KeyboardAuthContext) authCtx._onInfoResponse(responses) }, // End service/auth-related ============================================ GLOBAL_REQUEST: (p, name, wantReply, data) => { const 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(this, 'request') && this.authenticated ) { let accept let reject if (wantReply) { let replied = false accept = (chosenPort) => { if (replied) return replied = true let bufPort if ( name === 'tcpip-forward' && data.bindPort === 0 && typeof chosenPort === 'number' ) { bufPort = Buffer.allocUnsafe(4) writeUInt32BE(bufPort, chosenPort, 0) } setReply('SUCCESS', bufPort) } reject = () => { if (replied) return replied = true setReply('FAILURE') } } if (name === 'no-more-sessions@openssh.com') { this.noMoreSessions = true accept && accept() return } this.emit('request', accept, reject, name, data) } else if (wantReply) { setReply('FAILURE') } } } })) socket.pause() cryptoInit .then(() => { proto.start() socket.on('data', (data) => { try { proto.parse(data, 0, data.length) } catch (ex) { this.emit('error', ex) try { if (isWritable(socket)) socket.end() } catch {} } }) socket.resume() }) .catch((err) => { this.emit('error', err) try { if (isWritable(socket)) socket.end() } catch {} }) socket .on('error', (err) => { err.level = 'socket' this.emit('error', err) }) .once('end', () => { debug && debug('Socket ended') proto.cleanup() this.emit('end') }) .once('close', () => { debug && debug('Socket closed') proto.cleanup() this.emit('close') const err = new Error('No response from server') // Simulate error for pending channels and close any open channels this._chanMgr.cleanup(err) }) const onAuthDecide = (ctx, allowed, methodsLeft, isPartial) => { if (authCtx === ctx && !this.authenticated) { if (allowed) { authCtx = undefined this.authenticated = true proto.authSuccess() pendingAuths = [] this.emit('ready') } else { proto.authFailure(methodsLeft, isPartial) if (pendingAuths.length) { authCtx = pendingAuths.pop() if (listenerCount(this, 'authentication')) this.emit('authentication', authCtx) else authCtx.reject() } } } } function sendReplies() { while (unsentGlobalRequestsReplies.length > 0 && unsentGlobalRequestsReplies[0].type) { const reply = unsentGlobalRequestsReplies.shift() if (reply.type === 'SUCCESS') proto.requestSuccess(reply.buf) if (reply.type === 'FAILURE') proto.requestFailure() } } } end() { if (this._sock && isWritable(this._sock)) { this._protocol.disconnect(DISCONNECT_REASON.BY_APPLICATION) this._sock.end() } return this } x11(originAddr, originPort, cb) { const opts = { originAddr, originPort } openChannel(this, 'x11', opts, cb) return this } forwardOut(boundAddr, boundPort, remoteAddr, remotePort, cb) { const opts = { boundAddr, boundPort, remoteAddr, remotePort } openChannel(this, 'forwarded-tcpip', opts, cb) return this } openssh_forwardOutStreamLocal(socketPath, cb) { const opts = { socketPath } openChannel(this, 'forwarded-streamlocal@openssh.com', opts, cb) return this } rekey(cb) { let error try { this._protocol.rekey() } catch (ex) { error = ex } // TODO: re-throw error if no callback? if (typeof cb === 'function') { if (error) process.nextTick(cb, error) else this.once('rekey', cb) } } setNoDelay(noDelay) { if (this._sock && typeof this._sock.setNoDelay === 'function') this._sock.setNoDelay(noDelay) return this } } function openChannel(self, type, opts, cb) { // Ask the client to open a channel for some purpose (e.g. a forwarded TCP // connection) const initWindow = MAX_WINDOW const maxPacket = PACKET_SIZE if (typeof opts === 'function') { cb = opts opts = {} } const wrapper = (err, stream) => { cb(err, stream) } wrapper.type = type const localChan = self._chanMgr.add(wrapper) if (localChan === -1) { cb(new Error('No free channels available')) return } switch (type) { case 'forwarded-tcpip': self._protocol.forwardedTcpip(localChan, initWindow, maxPacket, opts) break case 'x11': self._protocol.x11(localChan, initWindow, maxPacket, opts) break case 'forwarded-streamlocal@openssh.com': self._protocol.openssh_forwardedStreamLocal(localChan, initWindow, maxPacket, opts) break default: throw new Error(`Unsupported channel type: ${type}`) } } function compareNumbers(a, b) { return a - b } module.exports = Server module.exports.IncomingClient = Client