* Rewrite SSH authentication - now works with PuTTY (thoguh there are some issues) and standard ssh. WIP still!

* Idle monitor not started until 'ready' signal
This commit is contained in:
Bryan Ashby 2015-10-21 16:30:32 -06:00
parent 6a2d283fad
commit e7e9746a85
4 changed files with 105 additions and 120 deletions

View File

@ -197,6 +197,8 @@ function startListening() {
client.on('ready', function onClientReady() { client.on('ready', function onClientReady() {
client.startIdleMonitor();
// Go to module -- use default error handler // Go to module -- use default error handler
prepareClient(client, function onPrepared() { prepareClient(client, function onPrepared() {
require('./connect.js').connectEntry(client); require('./connect.js').connectEntry(client);
@ -219,7 +221,7 @@ function startListening() {
}); });
client.on('idle timeout', function idleTimeout() { client.on('idle timeout', function idleTimeout() {
client.log.info('User idle timeout expired'); client.log.info('User idle timeout expired');
client.gotoMenuModule( { name : 'idleLogoff' }, function goMenuRes(err) { client.gotoMenuModule( { name : 'idleLogoff' }, function goMenuRes(err) {
if(err) { if(err) {

View File

@ -94,17 +94,6 @@ function Client(input, output) {
this.currentTheme = { info : { name : 'N/A', description : 'None' } }; this.currentTheme = { info : { name : 'N/A', description : 'None' } };
this.lastKeyPressMs = Date.now(); this.lastKeyPressMs = Date.now();
//
// Every 1m, check for idle.
//
this.idleCheck = setInterval(function checkForIdle() {
var nowMs = Date.now();
if(nowMs - self.lastKeyPressMs >= (Config.misc.idleLogoutSeconds * 1000)) {
self.emit('idle timeout');
}
}, 1000 * 60);
Object.defineProperty(this, 'node', { Object.defineProperty(this, 'node', {
get : function() { get : function() {
return self.session.id + 1; return self.session.id + 1;
@ -112,7 +101,6 @@ function Client(input, output) {
}); });
// //
// Peek at incoming |data| and emit events for any special // Peek at incoming |data| and emit events for any special
// handling that may include: // handling that may include:
@ -408,6 +396,30 @@ Client.prototype.setInputOutput = function(input, output) {
this.term = new term.ClientTerminal(this.output); this.term = new term.ClientTerminal(this.output);
}; };
Client.prototype.setTermType = function(termType) {
this.term.env.TERM = termType;
this.term.termType = termType;
this.log.debug( { termType : termType }, 'Set terminal type');
};
Client.prototype.startIdleMonitor = function() {
var self = this;
self.lastKeyPressMs = Date.now();
//
// Every 1m, check for idle.
//
self.idleCheck = setInterval(function checkForIdle() {
var nowMs = Date.now();
if(nowMs - self.lastKeyPressMs >= (Config.misc.idleLogoutSeconds * 1000)) {
self.emit('idle timeout');
}
}, 1000 * 60);
};
Client.prototype.end = function () { Client.prototype.end = function () {
this.detachCurrentMenuModule(); this.detachCurrentMenuModule();

View File

@ -24,25 +24,12 @@ exports.moduleInfo = {
exports.getModule = SSHServerModule; exports.getModule = SSHServerModule;
/* /*
Hello, TODO's
* Need to handle new user path
If you follow the first server example in the `ssh2` readme and substitute the `session.once('exec', ...)` with `session.once('shell', ...)` => [ new username(s) ] -> apply path ->
you should be fine. Just about all ssh clients default to an interactive shell session so that is what you will want to look for. As the => "new" or "apply" -> ....
documentation notes, the `shell` event handler is just passed `accept, reject` with `accept()` returning a duplex stream representing
stdin/stdout. You can write to stderr by using the `stderr` property of the duplex stream object.
You will probably also want to handle the `pty` event on the session, since most clients (by default) will request a pseudo-TTY before
requesting an interactive shell. I believe this event may be especially useful in your case because the ssh client can send certain terminal
modes which can have relevance with your telnet usage. The event info also contains window dimensions which may help in determining layout
of your display (there is also a `window-change` event that contains these same dimensions whenever the client's screen/window dimensions
change).
If you are still having problems after making these changes, post your code somewhere and I will see if there is anything out of place.
Additionally, you can set `debug: console.log` in the server config object to show debug output which may be useful to see what is or isn't
being sent/received ssh protocol-wise.
*/ */
function SSHClient(clientConn) { function SSHClient(clientConn) {
baseClient.Client.apply(this, arguments); baseClient.Client.apply(this, arguments);
@ -53,88 +40,54 @@ function SSHClient(clientConn) {
var self = this; var self = this;
this.userLoginWithCredentials = function(username, password, ctx) { clientConn.on('authentication', function authAttempt(ctx) {
userLogin(self, ctx.username, ctx.password, function authResult(err) { self.log.trace( { method : ctx.method, username : ctx.username }, 'SSH authentication attempt');
if(err) {
if(err.existingConn) {
// :TODO: Already logged in - how to let the SSH client know?
//self.term.write('User already logged in');
ctx.reject();
} else {
ctx.reject();
}
} else {
ctx.accept();
}
});
};
clientConn.on('authentication', function authentication(ctx) { var username = ctx.username || '';
self.log.trace( var password = ctx.password || '';
{
domain : ctx.domain,
username : ctx.username,
method : ctx.method,
}, 'SSH authentication');
// :TODO: check Config max failed logon attempts/etc. if(0 === username.length > 0 && password.length > 0) {
userLogin(self, ctx.username, ctx.password, function authResult(err) {
switch(ctx.method) { if(err) {
case 'password' : if(err.existingConn) {
// :TODO: Proper userLogin() here // :TODO: Can we display somthing here?
self.userLoginWithCredentials(ctx.username, ctx.password, ctx); ctx.reject();
break; clientConn.end();
return;
case 'publickey' :
// :TODO:
ctx.reject();
break;
case 'keyboard-interactive' :
if(!_.isString(ctx.username)) {
// :TODO: Let client know a username is required!
ctx.reject()
}
var PASS_PROMPT = { prompt : 'Password: ', echo : false };
ctx.prompt(PASS_PROMPT, function promptResponse(responses) {
if(0 === responses.length) {
return ctx.reject( ['keyboard-interactive'] );
} }
userLogin(self, ctx.username, responses[0], function authResult(err) {
if(err) {
if(err.existingConn) {
// :TODO: Already logged in - how to let the SSH client know?
//self.term.write('User already logged in');
ctx.reject();
} else {
PASS_PROMPT.prompt = 'Invalid username or password\nPassword: ';
ctx.prompt(PASS_PROMPT, promptResponse);
}
} else {
ctx.accept();
}
});
});
break;
default :
//
// Some terminals such as EtherTERM send a 'none' auth type, but include
// a username and password. For now, allow this. This should be looked
// into further as it may be a security issue!
//
if('none' === ctx.method && _.isString(ctx.username) && _.isString(ctx.password)) {
self.log.warn('Attempting authentication with \'none\' method');
self.userLoginWithCredentials(ctx.username, ctx.password, ctx);
} else { } else {
self.log.warn( { method : ctx.method }, 'Unsupported SSH authentication method'); ctx.accept();
ctx.reject( [ 'password', 'keyboard-interactive' ] );
} }
});
} }
if('keyboard-interactive' !== ctx.method) {
return ctx.reject( ['keyboard-interactive'] );
}
if(0 === username.length) {
// :TODO: can we display something here?
return ctx.reject();
}
var interactivePrompt = { prompt: ctx.username + '\'s password: ', echo : false };
ctx.prompt(interactivePrompt, function retryPrompt(answers) {
userLogin(self, username, (answers[0] || ''), function authResult(err) {
if(err) {
if(err.existingConn) {
// :TODO: can we display something here?
ctx.reject();
clientConn.end();
} else {
interactivePrompt.prompt = 'Access denied\n' + interactivePrompt.prompt;
return ctx.prompt(interactivePrompt, retryPrompt);
}
} else {
ctx.accept();
}
});
});
}); });
this.updateTermInfo = function(info) { this.updateTermInfo = function(info) {
@ -142,8 +95,8 @@ function SSHClient(clientConn) {
// From ssh2 docs: // From ssh2 docs:
// "rows and cols override width and height when rows and cols are non-zero." // "rows and cols override width and height when rows and cols are non-zero."
// //
var termHeight = 24; var termHeight;
var termWidth = 80; var termWidth;
if(info.rows > 0 && info.cols > 0) { if(info.rows > 0 && info.cols > 0) {
termHeight = info.rows; termHeight = info.rows;
@ -155,30 +108,40 @@ function SSHClient(clientConn) {
assert(_.isObject(self.term)); assert(_.isObject(self.term));
self.term.termHeight = termHeight; //
self.term.termWidth = termWidth; // Note that if we fail here, connect.js attempts some non-standard
// queries/etc., and ultimately will default to 80x24 if all else fails
//
if(termHeight > 0 && termWidth > 0) {
self.term.termHeight = termHeight;
self.term.termWidth = termWidth;
}
if(_.isString(info.term) && info.term.length > 0 && 'unknown' === self.term.termType) { if(_.isString(info.term) && info.term.length > 0 && 'unknown' === self.term.termType) {
self.setTermType(info.term); self.setTermType(info.term);
} }
}; };
clientConn.on('ready', function clientReady() { clientConn.once('ready', function clientReady() {
self.log.info('SSH authentication success'); self.log.info('SSH authentication success');
clientConn.on('session', function sess(accept, reject) { clientConn.once('session', function sess(accept, reject) {
var session = accept(); var session = accept();
session.on('pty', function pty(accept, reject, info) { session.once('pty', function pty(accept, reject, info) {
self.log.debug(info, 'SSH pty event'); self.log.debug(info, 'SSH pty event');
accept();
if(self.input) { // do we have I/O? if(self.input) { // do we have I/O?
self.updateTermInfo(info); self.updateTermInfo(info);
} else {
self.cachedPtyInfo = info;
} }
}); });
session.on('shell', function shell(accept, reject) { session.once('shell', function shell(accept, reject) {
self.log.debug('SSH shell event'); self.log.debug('SSH shell event');
var channel = accept(); var channel = accept();
@ -189,12 +152,13 @@ function SSHClient(clientConn) {
self.emit('data', data); self.emit('data', data);
}); });
self.emit('ready') if(self.cachedPtyInfo) {
}); self.updateTermInfo(self.cachedPtyInfo);
delete self.cachedPtyInfo;
}
session.on('subsystem', function subsystem(accept, reject, info) { // we're ready!
console.log('subsystem') self.emit('ready');
console.log(info)
}); });
session.on('window-change', function windowChange(accept, reject, info) { session.on('window-change', function windowChange(accept, reject, info) {
@ -207,7 +171,12 @@ function SSHClient(clientConn) {
}); });
clientConn.on('end', function clientEnd() { clientConn.on('end', function clientEnd() {
//self.emit('end'); self.emit('end'); // remove client connection/tracking
});
clientConn.on('error', function connError(err) {
// :TODO: what to do here?
console.log(err)
}); });
} }

View File

@ -558,12 +558,14 @@ TelnetClient.prototype.handleDontCommand = function(evt) {
this.log.trace(evt, 'dont'); this.log.trace(evt, 'dont');
}; };
/*
TelnetClient.prototype.setTermType = function(ttype) { TelnetClient.prototype.setTermType = function(ttype) {
this.term.env.TERM = ttype; this.term.env.TERM = ttype;
this.term.termType = ttype; this.term.termType = ttype;
this.log.debug( { termType : ttype }, 'Set terminal type'); this.log.debug( { termType : ttype }, 'Set terminal type');
}; };
*/
TelnetClient.prototype.handleSbCommand = function(evt) { TelnetClient.prototype.handleSbCommand = function(evt) {
var self = this; var self = this;