server.js•41.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;