From 608d4dc0941f09192c871d920910e7516a87cb4b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 3 Aug 2015 23:11:17 -0600 Subject: [PATCH] * DOOR.SYS support * LORD now works via DOOR.SYS at least * Abracadabra: nodeMax, tooManyArt support, etc. * Abracadabra: Exit back to menu * Some code cleanup --- core/door.js | 45 +++++++------- core/dropfile.js | 110 ++++++++++++++++++++++++++++------ core/user.js | 4 ++ mods/abracadabra.js | 136 +++++++++++++++++++++++++++++++------------ mods/last_callers.js | 2 +- mods/menu.json | 34 +++++++++-- 6 files changed, 246 insertions(+), 85 deletions(-) diff --git a/core/door.js b/core/door.js index 87890ee7..14c02264 100644 --- a/core/door.js +++ b/core/door.js @@ -4,6 +4,7 @@ var spawn = require('child_process').spawn; var events = require('events'); +var _ = require('lodash'); var pty = require('pty'); exports.Door = Door; @@ -14,6 +15,8 @@ function Door(client, exeInfo) { this.client = client; this.exeInfo = exeInfo; + this.exeInfo.encoding = this.exeInfo.encoding || 'cp437'; + // exeInfo.cmd // exeInfo.args[] // exeInfo.env{} @@ -30,44 +33,36 @@ Door.prototype.run = function() { var self = this; - var doorProc = spawn(this.exeInfo.cmd, this.exeInfo.args); - -/* - doorProc.stderr.pipe(self.client.term.output); - doorProc.stdout.pipe(self.client.term.output); - doorProc.stdout.on('data', function stdOutData(data) { - console.log('got data') - self.client.term.write(data); - }); - - doorProc.stderr.on('data', function stdErrData(data) { - console.log('got error data') - self.client.term.write(data); - }); - - doorProc.on('close', function closed(exitCode) { - console.log('closed') - self.emit('closed', exitCode); // just fwd on - }); -*/ - var door = pty.spawn(this.exeInfo.cmd, this.exeInfo.args, { + var door = pty.spawn(self.exeInfo.cmd, self.exeInfo.args, { cols : self.client.term.termWidth, rows : self.client.term.termHeight, + // :TODO: cwd + env : self.exeInfo.env, }); + // :TODO: can we pause the stream, write our own "Loading...", then on resume? + //door.pipe(self.client.term.output); self.client.term.output.pipe(door); // :TODO: do this with pluggable pipe/filter classes - door.setEncoding('cp437'); + door.setEncoding(this.exeInfo.encoding); + door.on('data', function doorData(data) { self.client.term.write(data); - //console.log(data); }); -//*/ door.on('close', function closed() { - console.log('closed...') + self.client.term.output.unpipe(door); + self.client.term.output.resume(); + }); + + door.on('exit', function exited(code) { + self.client.log.info( { code : code }, 'Door exited'); + + door.removeAllListeners(); + + self.emit('finished'); }); }; \ No newline at end of file diff --git a/core/dropfile.js b/core/dropfile.js index 55fbc38d..7eed7daa 100644 --- a/core/dropfile.js +++ b/core/dropfile.js @@ -7,6 +7,7 @@ var fs = require('fs'); var paths = require('path'); var _ = require('lodash'); var async = require('async'); +var moment = require('moment'); exports.DropFile = DropFile; @@ -55,7 +56,9 @@ function DropFile(client, fileType) { Object.defineProperty(this, 'dropFileContents', { get : function() { return { - DORINFO : self.getDoorInfoBuffer(), + DOOR : self.getDoorSysBuffer(), + + DORINFO : self.getDoorInfoDefBuffer(), }[self.fileType]; } }); @@ -73,7 +76,76 @@ function DropFile(client, fileType) { return 'DORINFO' + x + '.DEF'; }; - this.getDoorInfoBuffer = function() { + this.getDoorSysBuffer = function() { + var up = self.client.user.properties; + var now = moment(); + var secLevel = self.client.user.getLegacySecurityLevel().toString(); + + // :TODO: fix time remaining + // :TODO: fix default protocol -- user prop: transfer_protocol + + return new Buffer( [ + 'COM1:', // "Comm Port - COM0: = LOCAL MODE" + '57600', // "Baud Rate - 300 to 38400" (Note: set as 57600 instead!) + '8', // "Parity - 7 or 8" + self.client.node.toString(), // "Node Number - 1 to 99" + '57600', // "DTE Rate. Actual BPS rate to use. (kg)" + 'Y', // "Screen Display - Y=On N=Off (Default to Y)" + 'Y', // "Printer Toggle - Y=On N=Off (Default to Y)" + 'Y', // "Page Bell - Y=On N=Off (Default to Y)" + 'Y', // "Caller Alarm - Y=On N=Off (Default to Y)" + up.realName || self.client.user.username, // "User Full Name" + up.location || 'Anywhere', // "Calling From" + '123-456-7890', // "Home Phone" + '123-456-7890', // "Work/Data Phone" + 'NOPE', // "Password" (Note: this is never given out or even stored plaintext) + secLevel, // "Security Level" + up.login_count.toString(), // "Total Times On" + now.format('MM/DD/YY'), // "Last Date Called" + '15360', // "Seconds Remaining THIS call (for those that particular)" + '256', // "Minutes Remaining THIS call" + 'GR', // "Graphics Mode - GR=Graph, NG=Non-Graph, 7E=7,E Caller" + self.client.term.termHeight.toString(), // "Page Length" + 'N', // "User Mode - Y = Expert, N = Novice" + '1,2,3,4,5,6,7', // "Conferences/Forums Registered In (ABCDEFG)" + '1', // "Conference Exited To DOOR From (G)" + '01/01/99', // "User Expiration Date (mm/dd/yy)" + self.client.user.userId.toString(), // "User File's Record Number" + 'Z', // "Default Protocol - X, C, Y, G, I, N, Etc." + // :TODO: fix up, down, etc. form user properties + '0', // "Total Uploads" + '0', // "Total Downloads" + '0', // "Daily Download "K" Total" + '999999', // "Daily Download Max. "K" Limit" + moment(up.birthdate).format('MM/DD/YY'), // "Caller's Birthdate" + 'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)" + 'X:\\GEN\\', // "Path to the GEN directory" + Config.general.sysOp.username, // "Sysop's Name (name BBS refers to Sysop as)" + self.client.user.username, // "Alias name" + '00:05', // "Event time (hh:mm)" (note: wat?) + 'Y', // "If its an error correcting connection (Y/N)" + 'Y', // "ANSI supported & caller using NG mode (Y/N)" + 'Y', // "Use Record Locking (Y/N)" + '7', // "BBS Default Color (Standard IBM color code, ie, 1-15)" + // :TODO: fix minutes here also: + '256', // "Time Credits In Minutes (positive/negative)" + '07/07/90', // "Last New Files Scan Date (mm/dd/yy)" + // :TODO: fix last vs now times: + now.format('hh:mm'), // "Time of This Call" + now.format('hh:mm'), // "Time of Last Call (hh:mm)" + '9999', // "Maximum daily files available" + // :TODO: fix these stats: + '0', // "Files d/led so far today" + '0', // "Total "K" Bytes Uploaded" + '0', // "Total "K" Bytes Downloaded" + up.user_comment || 'None', // "User Comment" + '0', // "Total Doors Opened" + '0', // "Total Messages Left" + + ].join('\r\n') + '\r\n', 'cp437'); + }; + + this.getDoorInfoDefBuffer = function() { // :TODO: fix time remaining // @@ -82,22 +154,24 @@ function DropFile(client, fileType) { // // Note that usernames are just used for first/last names here // - var opUn = /[^\s]*/.exec(Config.general.sysOp.username)[0]; - var un = /[^\s]*/.exec(self.client.user.username)[0]; - return new Buffer([ - Config.general.boardName, // "The name of the system." - opUn, // "The sysop's name up to the first space." - opUn, // "The sysop's name following the first space." - 'COM1', // "The serial port the modem is connected to, or 0 if logged in on console." - '57600', // "The current port (DTE) rate." - '0', // "The number "0"" - un, // "The current user's name, up to the first space." - un, // "The current user's name, following the first space." - self.client.user.properties.location || '', // "Where the user lives, or a blank line if unknown." - '1', // "The number "0" if TTY, or "1" if ANSI." - self.client.user.isSysOp() ? '100' : '30', // "The number 5 for problem users, 30 for regular users, 80 for Aides, and 100 for Sysops." - '546', // "The number of minutes left in the current user's account, limited to 546 to keep from overflowing other software." - '-1' // "The number "-1" if using an external serial driver or "0" if using internal serial routines." + var opUn = /[^\s]*/.exec(Config.general.sysOp.username)[0]; + var un = /[^\s]*/.exec(self.client.user.username)[0]; + var secLevel = self.client.user.getLegacySecurityLevel().toString(); + + return new Buffer( [ + Config.general.boardName, // "The name of the system." + opUn, // "The sysop's name up to the first space." + opUn, // "The sysop's name following the first space." + 'COM1', // "The serial port the modem is connected to, or 0 if logged in on console." + '57600', // "The current port (DTE) rate." + '0', // "The number "0"" + un, // "The current user's name, up to the first space." + un, // "The current user's name, following the first space." + self.client.user.properties.location || '', // "Where the user lives, or a blank line if unknown." + '1', // "The number "0" if TTY, or "1" if ANSI." + secLevel, // "The number 5 for problem users, 30 for regular users, 80 for Aides, and 100 for Sysops." + '546', // "The number of minutes left in the current user's account, limited to 546 to keep from overflowing other software." + '-1' // "The number "-1" if using an external serial driver or "0" if using internal serial routines." ].join('\r\n') + '\r\n', 'cp437'); }; diff --git a/core/user.js b/core/user.js index 0d3a0b06..e78a9494 100644 --- a/core/user.js +++ b/core/user.js @@ -51,6 +51,10 @@ function User() { return _.isString(self.groups[groupIdOrName]); }; + this.getLegacySecurityLevel = function() { + return self.isRoot() ? 100 : 30; + }; + } User.PBKDF2 = { diff --git a/mods/abracadabra.js b/mods/abracadabra.js index 8a08d26e..dd013333 100644 --- a/mods/abracadabra.js +++ b/mods/abracadabra.js @@ -4,53 +4,97 @@ var MenuModule = require('../core/menu_module.js').MenuModule; var DropFile = require('../core/dropfile.js').DropFile; var door = require('../core/door.js'); +var theme = require('../core/theme.js'); +var ansi = require('../core/ansi_term.js'); var async = require('async'); var assert = require('assert'); var mkdirp = require('mkdirp'); var paths = require('path'); +var _ = require('lodash'); // :TODO: This should really be a system module... needs a little work to allow for such exports.getModule = AbracadabraModule; +var activeDoorNodeInstances = {}; + exports.moduleInfo = { name : 'Abracadabra', desc : 'External BBS Door Module', author : 'NuSkooler', }; +/* + Example configuration for LORD under DOSEMU: + + { + "config" : { + "name" : "LORD", + "dropFileType" : "DOOR", + "cmd" : "/usr/bin/dosemu", + "args" : [ "-quiet", "-f", "/etc/dosemu/dosemu.conf", "X:\\PW\\START.BAT {dropfile} {node}" ] ], + "nodeMax" : 32, + "tooManyArt" : "toomany-lord.ans" + } + } +*/ function AbracadabraModule(options) { MenuModule.call(this, options); var self = this; - this.config = options.menuConfig.config || { - dropFileType : 'DORINFO', - }; - this.config.args = this.config.args || []; + this.config = options.menuConfig.config; + + assert(_.isString(this.config.name, 'Config \'name\' is required')); + assert(_.isString(this.config.dropFileType, 'Config \'dropFileType\' is required')); + assert(_.isString(this.config.cmd, 'Config \'cmd\' is required')); + + this.config.nodeMax = this.config.nodeMax || 0; + this.config.args = this.config.args || []; /* - { - "config" : { - "name" : "LORD", - "cmd" : "...", - "args" : [ ... ], - "dropFileType" : "dorinfo", - "maxNodes" : 32, default=unlimited - "tooManyArt" : "..." (optional); default = "Too many active" message - ... - "dropFilePath" : "/.../LORD/", || Config.paths.dropFiles - } - } + :TODO: + * disconnecting wile door is open leaves dosemu + */ this.initSequence = function() { async.series( [ function validateNodeCount(callback) { - // :TODO: Check that node count for this door has not been reached - callback(null); + if(self.config.nodeMax > 0 && + _.isNumber(activeDoorNodeInstances[self.config.name]) && + activeDoorNodeInstances[self.config.name] + 1 > self.config.nodeMax) + { + self.client.log.info( + { + name : self.config.name, + activeCount : activeDoorNodeInstances[self.config.name] + }, + 'Too many active instances'); + + if(_.isString(self.config.tooManyArt)) { + theme.displayThemeArt( { client : self.client, name : self.config.tooManyArt }, function displayed() { + callback(new Error('Too many active instances')); + }); + } else { + self.client.term.write('\nToo many active instances. Try again later.\n'); + + setTimeout(function timeout() { + callback(new Error('Too many active instances')); + }, 1000); + } + } else { + // :TODO: JS elegant way to do this? + if(activeDoorNodeInstances[self.config.name]) { + activeDoorNodeInstances[self.config.name] += 1; + } else { + activeDoorNodeInstances[self.config.name] = 1; + } + + callback(null); + } }, function generateDropfile(callback) { self.dropFile = new DropFile(self.client, self.config.dropFileType); @@ -68,14 +112,46 @@ function AbracadabraModule(options) { } ], function complete(err) { - self.finishedLoading(); + if(err) { + self.fallbackModule(); + } else { + self.finishedLoading(); + } } ); }; - this.runDOSEmuDoor = function() { + this.runDosEmuDoor = function() { }; + + this.runDoor = function() { + + var exeInfo = { + cmd : this.config.cmd, + args : this.config.args, + }; + + // :TODO: this system should probably be generic + for(var i = 0; i < exeInfo.args.length; ++i) { + exeInfo.args[i] = exeInfo.args[i].replace(/\{dropfile\}/g, self.dropFile.fileName); + exeInfo.args[i] = exeInfo.args[i].replace(/\{node\}/g, self.client.node.toString()); + } + + var doorInstance = new door.Door(this.client, exeInfo); + + doorInstance.on('finished', function doorFinished() { + self.fallbackModule(); + }); + + self.client.term.write(ansi.resetScreen()); + + doorInstance.run(); + } + + this.fallbackModule = function() { + self.client.gotoMenuModule( { name : self.menuConfig.fallback } ); + } } require('util').inherits(AbracadabraModule, MenuModule); @@ -86,24 +162,12 @@ AbracadabraModule.prototype.enter = function(client) { }; AbracadabraModule.prototype.leave = function() { - Abracadabra.super_.prototype.leave.call(this); + AbracadabraModule.super_.prototype.leave.call(this); + activeDoorNodeInstances[this.config.name] -= 1; }; AbracadabraModule.prototype.finishedLoading = function() { - var self = this; - - var exeInfo = { - cmd : this.config.cmd, - args : this.config.args, - }; - - // :TODO: this system should probably be generic - for(var i = 0; i < exeInfo.args.length; ++i) { - exeInfo.args[i] = exeInfo.args[i].replace(/\{dropfile\}/g, self.dropFile.fileName); - exeInfo.args[i] = exeInfo.args[i].replace(/\{node\}/g, self.client.node.toString()); - } - - var doorInstance = new door.Door(this.client, exeInfo); - doorInstance.run(); + + this.runDoor(); }; \ No newline at end of file diff --git a/mods/last_callers.js b/mods/last_callers.js index 57696f8d..5daa62eb 100644 --- a/mods/last_callers.js +++ b/mods/last_callers.js @@ -50,7 +50,7 @@ LastCallersModule.prototype.enter = function(client) { // we need the client to init this for theming if(!_.isString(this.dateTimeFormat)) { - this.dateTimeFormat = this.client.currentTheme.helpers.getDateFormat('short') + + this.dateTimeFormat = this.client.currentTheme.helpers.getDateFormat('short') + ' ' + this.client.currentTheme.helpers.getTimeFormat('short'); } }; diff --git a/mods/menu.json b/mods/menu.json index 1488576b..7af2dfa9 100644 --- a/mods/menu.json +++ b/mods/menu.json @@ -197,6 +197,9 @@ "module" : "last_callers", "art" : "LASTCALL", "options" : { "cls" : true, "pause" : true }, + "config" : { + "dateTimeFormat" : "ddd MMM Do h:mm a" + }, "action" : "@menu:fullLoginSequenceUserStats" }, "fullLoginSequenceUserStats" : { @@ -211,8 +214,8 @@ }, "currentUserStats" : { "art" : "userstats", - "options" : { "cls" : true, "pause" : true }, - "action" : "@menu:lastCallers" + "options" : { "cls" : true, "pause" : true } + //"action" : "@menu:lastCallers" }, "mainMenu" : { "art" : "MAINMENU", @@ -239,6 +242,10 @@ "value" : { "1" : "d" }, "action" : "@menu:doorPimpWars" }, + { + "value" : { "1" : "l" }, + "action" : "@menu:doorLORD" + }, { "value" : 1, "action" : "@menu:mainMenu" @@ -251,10 +258,27 @@ }, "doorPimpWars" : { "module" : "abracadabra", + "fallback" : "mainMenu", "config" : { - "name" : "PimpWars", - "cmd" : "/usr/bin/dosemu", - "args" : [ "-quiet", "-f", "/home/nuskooler/DOS/X/LORD/dosemu.conf", "X:\\PW\\START.BAT {dropfile} {node}" ]//, "X:\\PW\\PIMPWARS.EXE", "X:\\DROP\\{dropfile}", "{node}" ] + "name" : "PimpWars", + "dropFileType" : "DORINFO", + "cmd" : "/usr/bin/dosemu", + "args" : [ + "-quiet", "-f", "/home/nuskooler/DOS/X/LORD/dosemu.conf", "X:\\PW\\START.BAT {dropfile} {node}" + ], + "nodeMax" : 1 + } + }, + "doorLORD" : { + "module" : "abracadabra", + "fallback" : "mainMenu", + "config" : { + "name" : "LORD", + "dropFileType" : "DOOR", + "cmd" : "/usr/bin/dosemu", + "args" : [ + "-quiet", "-f", "/home/nuskooler/DOS/X/LORD/dosemu.conf", "X:\\LORD\\START.BAT {node}" + ] } }, ////////////////////////////////////////////////////////////////////////