2023-10-03 11:14:36 +08:00
|
|
|
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 genPublicKey = ssh2_streams.utils.genPublicKey;
|
|
|
|
var decryptKey = ssh2_streams.utils.decryptKey;
|
|
|
|
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 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,
|
|
|
|
'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);
|
|
|
|
if (privateKey instanceof Error)
|
|
|
|
throw new Error('Cannot parse privateKey: ' + privateKey.message);
|
|
|
|
if (!privateKey.private)
|
|
|
|
throw new Error('privateKey value contains an invalid private key');
|
|
|
|
if (hostKeys[privateKey.fulltype])
|
|
|
|
continue;
|
|
|
|
if (privateKey.encryption) {
|
|
|
|
if (typeof hostKeys_[i].passphrase !== 'string')
|
|
|
|
throw new Error('Missing passphrase for encrypted private key');
|
|
|
|
decryptKey(privateKey, hostKeys_[i].passphrase);
|
|
|
|
}
|
|
|
|
hostKeys[privateKey.fulltype] = {
|
|
|
|
privateKey: privateKey,
|
|
|
|
publicKey: genPublicKey(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, 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() {
|
|
|
|
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 = new Buffer(4);
|
|
|
|
bufPort.writeUInt32BE(chosenPort, 0, true);
|
|
|
|
}
|
|
|
|
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) {
|
|
|
|
switch (pkInfo.keyAlgo) {
|
|
|
|
case 'ssh-rsa':
|
|
|
|
sigAlgo = 'RSA-SHA1';
|
|
|
|
break;
|
|
|
|
case 'ssh-dss':
|
|
|
|
sigAlgo = 'DSA-SHA1';
|
|
|
|
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) {
|
|
|
|
switch (pkInfo.keyAlgo) {
|
|
|
|
case 'ssh-rsa':
|
|
|
|
sigAlgo = 'RSA-SHA1';
|
|
|
|
break;
|
|
|
|
case 'ssh-dss':
|
|
|
|
sigAlgo = 'DSA-SHA1';
|
|
|
|
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;
|