diff --git a/core/bbs.js b/core/bbs.js index 625c7e87..8513f5ba 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -6,6 +6,7 @@ var conf = require('./config.js'); var logger = require('./logger.js'); var miscUtil = require('./misc_util.js'); var database = require('./database.js'); +var clientConns = require('./client_connections.js'); var iconv = require('iconv-lite'); var paths = require('path'); @@ -152,8 +153,6 @@ function initialize(cb) { ); } -var clientConnections = []; - function startListening() { if(!conf.config.servers) { // :TODO: Log error ... output to stderr as well. We can do it all with the logger @@ -189,7 +188,7 @@ function startListening() { client.runtime = {}; } - addNewClient(client); + clientConns.addNewClient(client); client.on('ready', function onClientReady() { // Go to module -- use default error handler @@ -199,7 +198,7 @@ function startListening() { }); client.on('end', function onClientEnd() { - removeClient(client); + clientConns.removeClient(client); }); client.on('error', function onClientError(err) { @@ -209,7 +208,20 @@ function startListening() { client.on('close', function onClientClose(hadError) { var l = hadError ? logger.log.info : logger.log.debug; l( { clientId : client.runtime.id }, 'Connection closed'); - removeClient(client); + + clientConns.removeClient(client); + }); + + client.on('idle timeout', function idleTimeout() { + client.log.info('User idle timeout expired'); + + client.gotoMenuModule( { name : 'idleLogoff' }, function goMenuRes(err) { + if(err) { + // likely just doesn't exist + client.term.write('\nIdle timeout expired. Goodbye!\n'); + client.end(); + } + }); }); }); @@ -222,39 +234,6 @@ function startListening() { }); } -function addNewClient(client) { - var id = client.runtime.id = clientConnections.push(client) - 1; - - // Create a client specific logger - client.log = logger.log.child( { clientId : id } ); - - var connInfo = { ip : client.input.remoteAddress }; - - if(client.log.debug()) { - connInfo.port = client.input.localPort; - connInfo.family = client.input.localFamily; - } - - client.log.info(connInfo, 'Client connected'); - - return id; -} - -function removeClient(client) { - var i = clientConnections.indexOf(client); - if(i > -1) { - clientConnections.splice(i, 1); - - logger.log.info( - { - connectionCount : clientConnections.length, - clientId : client.runtime.id - }, - 'Client disconnected' - ); - } -} - function prepareClient(client, cb) { // :TODO: it feels like this should go somewhere else... and be a bit more elegant. if('*' === conf.config.preLoginTheme) { diff --git a/core/client.js b/core/client.js index 2bf5c3c7..76c44b7a 100644 --- a/core/client.js +++ b/core/client.js @@ -37,6 +37,7 @@ var Log = require('./logger.js').log; var user = require('./user.js'); var moduleUtil = require('./module_util.js'); var menuUtil = require('./menu_util.js'); +var Config = require('./config.js').config; var stream = require('stream'); var assert = require('assert'); @@ -101,6 +102,18 @@ function Client(input, output) { this.term = new term.ClientTerminal(this.output); this.user = new user.User(); this.currentTheme = { info : { name : 'N/A', description : 'None' } }; + 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', { get : function() { @@ -372,6 +385,8 @@ function Client(input, output) { if(key || ch) { self.log.trace( { key : key, ch : escape(ch) }, 'User keyboard input'); + self.lastKeyPressMs = Date.now(); + self.emit('key press', ch, key); } }); @@ -389,6 +404,8 @@ require('util').inherits(Client, stream); Client.prototype.end = function () { this.detachCurrentMenuModule(); + + clearInterval(this.idleCheck); return this.output.end.apply(this.output, arguments); }; @@ -417,6 +434,8 @@ Client.prototype.gotoMenuModule = function(options, cb) { assert(options.name); // Assign a default missing module handler callback if none was provided + var callbackOnErrorOnly = !_.isFunction(cb); + cb = miscUtil.valueWithDefault(cb, self.defaultHandlerMissingMod()); self.detachCurrentMenuModule(); @@ -436,6 +455,10 @@ Client.prototype.gotoMenuModule = function(options, cb) { modInst.enter(self); self.currentMenuModule = modInst; + + if(!callbackOnErrorOnly) { + cb(null); + } } }); }; diff --git a/core/client_connections.js b/core/client_connections.js new file mode 100644 index 00000000..bf6b9698 --- /dev/null +++ b/core/client_connections.js @@ -0,0 +1,57 @@ +/* jslint node: true */ +'use strict'; + +var logger = require('./logger.js'); + +exports.addNewClient = addNewClient; +exports.removeClient = removeClient; + +var clientConnections = []; +exports.clientConnections = clientConnections; + +function addNewClient(client) { + var id = client.runtime.id = clientConnections.push(client) - 1; + + // Create a client specific logger + client.log = logger.log.child( { clientId : id } ); + + var connInfo = { ip : client.input.remoteAddress }; + + if(client.log.debug()) { + connInfo.port = client.input.localPort; + connInfo.family = client.input.localFamily; + } + + client.log.info(connInfo, 'Client connected'); + + return id; +} + +function removeClient(client) { + client.end(); + + var i = clientConnections.indexOf(client); + if(i > -1) { + clientConnections.splice(i, 1); + + logger.log.info( + { + connectionCount : clientConnections.length, + clientId : client.runtime.id + }, + 'Client disconnected' + ); + } +} + +/* :TODO: make a public API elsewhere +function getActiveClientInformation() { + var info = {}; + + clientConnections.forEach(function connEntry(cc) { + + }); + + return info; +} +*/ \ No newline at end of file diff --git a/core/config.js b/core/config.js index e67d62d7..45c44cf4 100644 --- a/core/config.js +++ b/core/config.js @@ -145,6 +145,10 @@ function getDefaultConfig() { */ }, + misc : { + idleLogoutSeconds : 60 * 3, // 3m + }, + logging : { level : 'debug' } diff --git a/core/connect.js b/core/connect.js index b39e5457..26db15d5 100644 --- a/core/connect.js +++ b/core/connect.js @@ -147,7 +147,7 @@ function connectEntry(client) { displayBanner(term); setTimeout(function onTimeout() { - client.gotoMenuModule( { name : Config.firstMenu }); + client.gotoMenuModule( { name : Config.firstMenu } ); }, 500); }); } diff --git a/core/system_menu_method.js b/core/system_menu_method.js index 08790e3a..a75d91d9 100644 --- a/core/system_menu_method.js +++ b/core/system_menu_method.js @@ -1,15 +1,15 @@ /* jslint node: true */ 'use strict'; -var theme = require('../core/theme.js'); -//var Log = require('../core/logger.js').log; -var ansi = require('../core/ansi_term.js'); -var userDb = require('./database.js').dbs.user; +var theme = require('./theme.js'); +var clientConnections = require('./client_connections.js').clientConnections; +var ansi = require('./ansi_term.js'); +var userDb = require('./database.js').dbs.user; -var async = require('async'); +var async = require('async'); -exports.login = login; -exports.logoff = logoff; +exports.login = login; +exports.logoff = logoff; function login(callingMenu, formData, extraArgs) { var client = callingMenu.client; @@ -18,11 +18,41 @@ function login(callingMenu, formData, extraArgs) { 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.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.runtime.id, + username : user.username, + userId : user.userId }, + 'Already logged in' + ); + + // :TODO: display message/art/etc. + + client.gotoMenuModule( { name : callingMenu.menuConfig.fallback } ); + return; + } + + // use client.user so we can get correct case client.log.info( { username : user.username }, 'Successful login'); @@ -81,12 +111,14 @@ function login(callingMenu, formData, extraArgs) { } function logoff(callingMenu, formData, extraArgs) { + // + // Simple logoff. Note that recording of @ logoff properties/stats + // occurs elsewhere! + // var client = callingMenu.client; - // :TODO: record this. - setTimeout(function timeout() { - client.term.write(ansi.normal() + '\nATH0\n'); + client.term.write(ansi.normal() + '\n+++ATH0\n'); client.end(); }, 500); } \ No newline at end of file diff --git a/core/user.js b/core/user.js index e78a9494..f05c9926 100644 --- a/core/user.js +++ b/core/user.js @@ -52,7 +52,13 @@ function User() { }; this.getLegacySecurityLevel = function() { - return self.isRoot() ? 100 : 30; + if(self.isRoot() || self.isGroupMember('sysops')) { + return 100; + } else if(self.isGroupMember('users')) { + return 30; + } else { + return 10; // :TODO: Is this what we want? + } }; } diff --git a/mods/abracadabra.js b/mods/abracadabra.js index cee6b8a0..a5b928be 100644 --- a/mods/abracadabra.js +++ b/mods/abracadabra.js @@ -19,6 +19,8 @@ exports.getModule = AbracadabraModule; var activeDoorNodeInstances = {}; +var doorInstances = {}; // name -> { count : , { : } } + exports.moduleInfo = { name : 'Abracadabra', desc : 'External BBS Door Module', @@ -56,7 +58,8 @@ function AbracadabraModule(options) { /* :TODO: * disconnecting wile door is open leaves dosemu - + * http://bbslink.net/sysop.php support + * Font support ala all other menus... or does this just work? */ this.initSequence = function() { diff --git a/mods/apply.js b/mods/apply.js index 11ef0d82..47ec1f31 100644 --- a/mods/apply.js +++ b/mods/apply.js @@ -92,7 +92,7 @@ function submitApplication(callingMenu, formData, extraArgs) { affiliation : formData.value.affils, email_address : formData.value.email, web_address : formData.value.web, - timestamp : new Date().toISOString(), + account_created : new Date().toISOString(), // :TODO: This is set in User.create() -- proabbly don't need it here: //account_status : Config.users.requireActivation ? user.User.AccountStatus.inactive : user.User.AccountStatus.active, diff --git a/mods/art/IDLELOG.ANS b/mods/art/IDLELOG.ANS new file mode 100644 index 00000000..2a6cbc13 Binary files /dev/null and b/mods/art/IDLELOG.ANS differ diff --git a/mods/menu.json b/mods/menu.json index e863ed0e..69c354c7 100644 --- a/mods/menu.json +++ b/mods/menu.json @@ -285,6 +285,14 @@ //////////////////////////////////////////////////////////////////////// // Mods //////////////////////////////////////////////////////////////////////// + "idleLogoff" : { + "art" : "IDLELOG", + "options" : { "cls" : true }, + "action" : "@systemMethod:logoff" + }, + //////////////////////////////////////////////////////////////////////// + // Mods + //////////////////////////////////////////////////////////////////////// "lastCallers" :{ "module" : "last_callers", "art" : "LASTCALL.ANS",