diff --git a/core/servers/ssh.js b/core/servers/ssh.js index 918286c2..f457769e 100644 --- a/core/servers/ssh.js +++ b/core/servers/ssh.js @@ -4,12 +4,14 @@ // ENiGMA½ var conf = require('../config.js'); var baseClient = require('../client.js'); -var user = require('../user.js'); +var Log = require('../logger.js').log; var ServerModule = require('../server_module.js').ServerModule; +var userLogin = require('../user_login.js').userLogin; var ssh2 = require('ssh2'); var fs = require('fs'); var util = require('util'); +var _ = require('lodash'); exports.moduleInfo = { name : 'SSH', @@ -19,50 +21,84 @@ exports.moduleInfo = { exports.getModule = SSHServerModule; -function SSHClient(input, output) { +function SSHClient(clientConn) { baseClient.Client.apply(this, arguments); + // + // WARNING: Until we have emit 'ready', self.input, and self.output and + // not yet defined! + // + var self = this; - this.input.on('authentication', function onAuthentication(ctx) { - console.log('auth: ' + ctx.method); + clientConn.on('authentication', function authentication(ctx) { + self.log.trace( { context : ctx }, 'SSH authentication'); - if('password' === ctx.method) { - // :TODO: Log attempts - user.authenticate(ctx.username, ctx.password, self, function onAuthResult(err) { - if(err) { - ctx.reject(); - } else { - ctx.accept(); + // :TODO: check Config max failed logon attempts/etc. + + switch(ctx.method) { + case 'password' : + // :TODO: Proper userLogin() here + self.user.authenticate(ctx.username, ctx.password, self, function authResult(err) { + if(err) { + ctx.reject(); + } else { + ctx.accept(); + } + }); + break; + + case 'publickey' : + // :TODO: + ctx.reject(); + break; + + case 'keyboard-interactive' : + if(!_.isString(ctx.username)) { + // :TODO: Let client know a username is required! + ctx.reject() } - }); - } else if('publickey' === ctx.method) { - console.log('pub key path'); - } else if('keyboard-interactive' === ctx.method) { - ctx.reject(['password']); - // :TODO: support this. Allow users to generate a key for use or w/e - - /*} else if('keyboard-interactive' === ctx.method) { - console.log(ctx.submethods); // :TODO: proper logging; handle known types, etc. - ctx.prompt([ { prompt : 'Password: ', echo : false } ], function onPromptResponses(err, responses) { - console.log(err); - console.log(responses); - });*/ - } else { - 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 : + self.log.info( { method : ctx.method }, 'Unsupported SSH authentication method. Rejecting connection.'); + ctx.reject(); } }); - this.input.on('ready', function onReady() { - console.log('Client authenticated'); - - self.input.on('session', function onSession(accept, reject) { + clientConn.on('ready', function clientReady() { + self.log.info('SSH authentication success'); + clientConn.on('session', function sess(accept, reject) { + self.input = accept(); + self.output = self.input; }); }); - this.input.on('end', function onEnd() { + clientConn.on('end', function clientEnd() { self.emit('end'); }); } @@ -80,15 +116,20 @@ SSHServerModule.prototype.createServer = function() { // :TODO: setup all options here. What should the banner, etc. really be???? var serverConf = { - privateKey : fs.readFileSync(conf.config.servers.ssh.rsaPrivateKey), - banner : 'ENiGMA½ BBS SSH Server', - debug : function onDebug(s) { console.log(s); } + privateKey : fs.readFileSync(conf.config.servers.ssh.rsaPrivateKey), + banner : 'ENiGMA½ BBS SSH Server', + debug : function debugSsh(dbgLine) { + if(true === conf.config.servers.ssh.debugConnections) { + self.log.trace('SSH: ' + dbgLine); + } + } }; var server = ssh2.Server(serverConf); server.on('connection', function onConnection(conn, info) { - console.log(info); // :TODO: Proper logging - var client = new SSHClient(conn, conn); + Log.info(info, 'New SSH connection'); + + var client = new SSHClient(conn); this.emit('client', client); }); diff --git a/core/system_menu_method.js b/core/system_menu_method.js index 07874034..c61e8699 100644 --- a/core/system_menu_method.js +++ b/core/system_menu_method.js @@ -6,6 +6,7 @@ var clientConnections = require('./client_connections.js').clientConnections; var ansi = require('./ansi_term.js'); var userDb = require('./database.js').dbs.user; var sysProp = require('./system_property.js'); +var userLogin = require('./user_login.js').userLogin; var async = require('async'); var _ = require('lodash'); @@ -17,52 +18,16 @@ exports.fallbackMenu = fallbackMenu; function login(callingMenu, formData, extraArgs) { var client = callingMenu.client; - client.user.authenticate(formData.value.username, formData.value.password, function authenticated(err) { + userLogin(callingMenu.client, formData.value.username, formData.value.password, function authResult(err) { if(err) { - client.log.info( { username : formData.value.username }, 'Failed login attempt %s', err); - - // :TODO: if username exists, record failed login attempt to properties - // :TODO: check Config max failed logon attempts/etc. - - client.fallbackMenuModule(); - //client.gotoMenuModule( { name : callingMenu.menuConfig.fallback } ); - } else { - var now = new Date(); - var user = callingMenu.client.user; - - // - // Ensure this user is not already logged in. - // Loop through active connections -- which includes the current -- - // and check for matching user ID. If the count is > 1, disallow. - // - var existingClientConnection; - clientConnections.forEach(function connEntry(cc) { - if(cc.user !== user && cc.user.userId === user.userId) { - existingClientConnection = cc; - } - }); - - if(existingClientConnection) { - client.log.info( { - existingClientId : existingClientConnection.session.id, - username : user.username, - userId : user.userId }, - 'Already logged in' - ); - + // login failure + if(err.existingConn) { client.term.rawWrite(ansi.resetScreen()); - var tooNodeArt; - if(_.has(callingMenu, 'menuConfig.config.tooNodeArt')) { - tooNodeArt = callingMenu.menuConfig.config.tooNodeArt; - } else { - tooNodeArt = 'TOONODE'; - } - var artOpts = { client : client, - font : callingMenu.menuConfig.font, - name : tooNodeArt, + font : _.has(callingMenu, 'menuConfig.config.tooNode.font') ? callingMenu.menuConfig.config.tooNode.font : null, + name : _.has(callingMenu, 'menuConfig.config.tooNode.art') ? callingMenu.menuConfig.config.tooNode.art : 'TOONODE', }; theme.displayThemeArt(artOpts, function artDisplayed(err) { @@ -76,66 +41,14 @@ function login(callingMenu, formData, extraArgs) { }); return; + } else { + // Other error + client.fallbackMenuModule(); } - - // use client.user so we can get correct case - client.log.info( { username : user.username }, 'Successful login'); - - async.parallel( - [ - function loadThemeConfig(callback) { - theme.loadTheme(user.properties.theme_id, function themeLoaded(err, theme) { - client.currentTheme = theme; - callback(null); // always non-fatal - }); - }, - function updateSystemLoginCount(callback) { - var sysLoginCount = sysProp.getSystemProperty('login_count') || 0; - sysLoginCount = parseInt(sysLoginCount, 10) + 1; - sysProp.persistSystemProperty('login_count', sysLoginCount, callback); - }, - function recordLastLogin(callback) { - user.persistProperty('last_login_timestamp', now.toISOString(), function persisted(err) { - callback(err); - }); - }, - function updateUserLoginCount(callback) { - if(!user.properties.login_count) { - user.properties.login_count = 1; - } else { - user.properties.login_count++; - } - - user.persistProperty('login_count', user.properties.login_count, function persisted(err) { - callback(err); - }); - }, - function recordLoginHistory(callback) { - userDb.serialize(function serialized() { - userDb.run( - 'INSERT INTO user_login_history (user_id, user_name, timestamp) ' + - 'VALUES(?, ?, ?);', [ user.userId, user.username, now.toISOString() ] - ); - - // keep 30 days of records - userDb.run( - 'DELETE FROM user_login_history ' + - 'WHERE timestamp <= DATETIME("now", "-30 day");' - ); - }); - - callback(null); - } - ], - function complete(err, results) { - if(err) { - client.log.error(err); - // :TODO: drop the connection? - } - client.gotoMenuModule( { name : callingMenu.menuConfig.next } ); - } - ); + } else { + // success! + client.gotoMenuModule( { name : callingMenu.menuConfig.next } ); } }); } diff --git a/core/user_login.js b/core/user_login.js new file mode 100644 index 00000000..cfa4caa4 --- /dev/null +++ b/core/user_login.js @@ -0,0 +1,111 @@ +/* jslint node: true */ +'use strict'; + +var theme = require('./theme.js'); +var clientConnections = require('./client_connections.js').clientConnections; +var userDb = require('./database.js').dbs.user; +var sysProp = require('./system_property.js'); + +var async = require('async'); +var _ = require('lodash'); +var assert = require('assert'); + +exports.userLogin = userLogin; + +function userLogin(client, username, password, cb) { + client.user.authenticate(username, password, function authenticated(err) { + if(err) { + client.log.info( { username : username }, 'Failed login attempt: %s', err); + + // :TODO: if username exists, record failed login attempt to properties + // :TODO: check Config max failed logon attempts/etc. - set err.maxAttempts = true + + cb(err); + } else { + var now = new Date(); + var user = client.user; + + // + // Ensure this user is not already logged in. + // Loop through active connections -- which includes the current -- + // and check for matching user ID. If the count is > 1, disallow. + // + var existingClientConnection; + clientConnections.forEach(function connEntry(cc) { + if(cc.user !== user && cc.user.userId === user.userId) { + existingClientConnection = cc; + } + }); + + if(existingClientConnection) { + client.log.info( { + existingClientId : existingClientConnection.session.id, + username : user.username, + userId : user.userId }, + 'Already logged in' + ); + + var existingConnError = new Error('Already logged in as supplied user'); + existingClientConnection.existingConn = true; + + cb(existingClientConnection); + return; + } + + + // use client.user so we can get correct case + client.log.info( { username : user.username }, 'Successful login'); + + async.parallel( + [ + function loadThemeConfig(callback) { + theme.loadTheme(user.properties.theme_id, function themeLoaded(err, theme) { + client.currentTheme = theme; + callback(null); // always non-fatal + }); + }, + function updateSystemLoginCount(callback) { + var sysLoginCount = sysProp.getSystemProperty('login_count') || 0; + sysLoginCount = parseInt(sysLoginCount, 10) + 1; + sysProp.persistSystemProperty('login_count', sysLoginCount, callback); + }, + function recordLastLogin(callback) { + user.persistProperty('last_login_timestamp', now.toISOString(), function persisted(err) { + callback(err); + }); + }, + function updateUserLoginCount(callback) { + if(!user.properties.login_count) { + user.properties.login_count = 1; + } else { + user.properties.login_count++; + } + + user.persistProperty('login_count', user.properties.login_count, function persisted(err) { + callback(err); + }); + }, + function recordLoginHistory(callback) { + userDb.serialize(function serialized() { + userDb.run( + 'INSERT INTO user_login_history (user_id, user_name, timestamp) ' + + 'VALUES(?, ?, ?);', [ user.userId, user.username, now.toISOString() ] + ); + + // keep 30 days of records + userDb.run( + 'DELETE FROM user_login_history ' + + 'WHERE timestamp <= DATETIME("now", "-30 day");' + ); + }); + + callback(null); + } + ], + function complete(err) { + cb(err); + } + ); + } + }); +} \ No newline at end of file diff --git a/mods/menu.hjson b/mods/menu.hjson index cd6e8d8d..f9e7b840 100644 --- a/mods/menu.hjson +++ b/mods/menu.hjson @@ -120,7 +120,10 @@ //next: messageArea next: fullLoginSequenceLoginArt config: { - tooNodeArt: TOONODE + //tooNodeArt: TOONODE + tooNode: { + art: TOONODE + } } form: { 0: {