Skip to main content
Glama

SSH MCP Server

by mfangtao
server.js41.7 kB
// 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'; const { Server: netServer } = require('net'); const EventEmitter = require('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;

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mfangtao/mcp-ssh-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server