hexo/node_modules/ssh2/lib/server.js

1158 lines
33 KiB
JavaScript

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;