diff --git a/core/abracadabra.js b/core/abracadabra.js index 3a1507b6..48b80d65 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -2,18 +2,14 @@ 'use strict'; const MenuModule = require('./menu_module.js').MenuModule; -const DropFile = require('./dropfile.js').DropFile; -const door = require('./door.js'); +const DropFile = require('./dropfile.js'); +const Door = require('./door.js'); const theme = require('./theme.js'); const ansi = require('./ansi_term.js'); const async = require('async'); const assert = require('assert'); -const paths = require('path'); const _ = require('lodash'); -const mkdirs = require('fs-extra').mkdirs; - -// :TODO: This should really be a system module... needs a little work to allow for such const activeDoorNodeInstances = {}; @@ -65,6 +61,7 @@ exports.getModule = class AbracadabraModule extends MenuModule { this.config = options.menuConfig.config; // :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts! -- { key : type, key2 : type2, ... } + // .. and/or EnigAssert 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')); @@ -122,19 +119,17 @@ exports.getModule = class AbracadabraModule extends MenuModule { callback(null); } }, + function prepareDoor(callback) { + self.doorInstance = new Door(self.client); + return self.doorInstance.prepare(self.config.io || 'stdio', callback); + }, function generateDropfile(callback) { - self.dropFile = new DropFile(self.client, self.config.dropFileType); - var fullPath = self.dropFile.fullPath; + const dropFileOpts = { + fileType : self.config.dropFileType, + }; - mkdirs(paths.dirname(fullPath), function dirCreated(err) { - if(err) { - callback(err); - } else { - self.dropFile.createFile(function created(err) { - callback(err); - }); - } - }); + self.dropFile = new DropFile(self.client, dropFileOpts); + return self.dropFile.createFile(callback); } ], function complete(err) { @@ -150,20 +145,19 @@ exports.getModule = class AbracadabraModule extends MenuModule { } runDoor() { + this.client.term.write(ansi.resetScreen()); const exeInfo = { - cmd : this.config.cmd, - args : this.config.args, - io : this.config.io || 'stdio', - encoding : this.config.encoding || this.client.term.outputEncoding, - dropFile : this.dropFile.fileName, - node : this.client.node, - //inhSocket : this.client.output._handle.fd, + cmd : this.config.cmd, + args : this.config.args, + io : this.config.io || 'stdio', + encoding : this.config.encoding || this.client.term.outputEncoding, + dropFile : this.dropFile.fileName, + dropFilePath : this.dropFile.fullPath, + node : this.client.node, }; - const doorInstance = new door.Door(this.client, exeInfo); - - doorInstance.once('finished', () => { + this.doorInstance.run(exeInfo, () => { // // Try to clean up various settings such as scroll regions that may // have been set within the door @@ -178,10 +172,6 @@ exports.getModule = class AbracadabraModule extends MenuModule { this.prevMenu(); }); - - this.client.term.write(ansi.resetScreen()); - - doorInstance.run(); } leave() { diff --git a/core/door.js b/core/door.js index ffd57b48..79d79b22 100644 --- a/core/door.js +++ b/core/door.js @@ -1,149 +1,122 @@ /* jslint node: true */ 'use strict'; - const stringFormat = require('./string_format.js'); +const { Errors } = require('./enig_error.js'); -const events = require('events'); -const _ = require('lodash'); const pty = require('node-pty'); const decode = require('iconv-lite').decode; const createServer = require('net').createServer; -exports.Door = Door; +module.exports = class Door { + constructor(client) { + this.client = client; + this.restored = false; + } -function Door(client, exeInfo) { - events.EventEmitter.call(this); + prepare(ioType, cb) { + this.io = ioType; - const self = this; - this.client = client; - this.exeInfo = exeInfo; - this.exeInfo.encoding = (this.exeInfo.encoding || 'cp437').toLowerCase(); - let restored = false; - - // - // Members of exeInfo: - // cmd - // args[] - // env{} - // cwd - // io - // encoding - // dropFile - // node - // inhSocket - // - - this.doorDataHandler = function(data) { - self.client.term.write(decode(data, self.exeInfo.encoding)); - }; - - this.restoreIo = function(piped) { - if(!restored && self.client.term.output) { - self.client.term.output.unpipe(piped); - self.client.term.output.resume(); - restored = true; - } - }; - - this.prepareSocketIoServer = function(cb) { - if('socket' === self.exeInfo.io) { - const sockServer = createServer(conn => { - - sockServer.getConnections( (err, count) => { - - // We expect only one connection from our DOOR/emulator/etc. - if(!err && count <= 1) { - self.client.term.output.pipe(conn); - - conn.on('data', self.doorDataHandler); - - conn.once('end', () => { - return self.restoreIo(conn); - }); - - conn.once('error', err => { - self.client.log.info( { error : err.toString() }, 'Door socket server connection'); - return self.restoreIo(conn); - }); - } - }); - }); - - sockServer.listen(0, () => { - return cb(null, sockServer); - }); - } else { + // we currently only have to do any real setup for 'socket' + if('socket' !== ioType) { return cb(null); } - }; - this.doorExited = function() { - self.emit('finished'); - }; -} + this.sockServer = createServer(conn => { + this.sockServer.getConnections( (err, count) => { -require('util').inherits(Door, events.EventEmitter); + // We expect only one connection from our DOOR/emulator/etc. + if(!err && count <= 1) { + this.client.term.output.pipe(conn); -Door.prototype.run = function() { - const self = this; + conn.on('data', this.doorDataHandler.bind(this)); - this.prepareSocketIoServer( (err, sockServer) => { - if(err) { - this.client.log.warn( { error : err.toString() }, 'Failed executing door'); - return self.doorExited(); - } + conn.once('end', () => { + return this.restoreIo(conn); + }); - // Expand arg strings, e.g. {dropFile} -> DOOR32.SYS - // :TODO: Use .map() here - let args = _.clone(self.exeInfo.args); // we need a copy so the original is not modified - - for(let i = 0; i < args.length; ++i) { - args[i] = stringFormat(self.exeInfo.args[i], { - dropFile : self.exeInfo.dropFile, - node : self.exeInfo.node.toString(), - srvPort : sockServer ? sockServer.address().port.toString() : '-1', - userId : self.client.user.userId.toString(), + conn.once('error', err => { + this.client.log.info( { error : err.message }, 'Door socket server connection'); + return this.restoreIo(conn); + }); + } }); + }); + + this.sockServer.listen(0, () => { + return cb(null); + }); + } + + run(exeInfo, cb) { + this.encoding = (exeInfo.encoding || 'cp437').toLowerCase(); + + if('socket' === this.io && !this.sockServer) { + return cb(Errors.UnexpectedState('Socket server is not running')); } - const door = pty.spawn(self.exeInfo.cmd, args, { - cols : self.client.term.termWidth, - rows : self.client.term.termHeight, + const formatObj = { + dropFile : exeInfo.dropFile, + dropFilePath : exeInfo.dropFilePath, + node : exeInfo.node.toString(), + srvPort : this.sockServer ? this.sockServer.address().port.toString() : '-1', + userId : this.client.user.userId.toString(), + }; + + const args = exeInfo.args.map( arg => stringFormat(arg, formatObj) ); + + const door = pty.spawn(exeInfo.cmd, args, { + cols : this.client.term.termWidth, + rows : this.client.term.termHeight, // :TODO: cwd - env : self.exeInfo.env, + env : exeInfo.env, encoding : null, // we want to handle all encoding ourself }); - if('stdio' === self.exeInfo.io) { - self.client.log.debug('Using stdio for door I/O'); + if('stdio' === this.io) { + this.client.log.debug('Using stdio for door I/O'); - self.client.term.output.pipe(door); + this.client.term.output.pipe(door); - door.on('data', self.doorDataHandler); + door.on('data', this.doorDataHandler.bind(this)); door.once('close', () => { - return self.restoreIo(door); + return this.restoreIo(door); }); - } else if('socket' === self.exeInfo.io) { - self.client.log.debug( { port : sockServer.address().port }, 'Using temporary socket server for door I/O'); + } else if('socket' === this.io) { + this.client.log.debug( + { srvPort : this.sockServer.address().port, srvSocket : this.sockServerSocket }, + 'Using temporary socket server for door I/O' + ); } door.once('exit', exitCode => { - self.client.log.info( { exitCode : exitCode }, 'Door exited'); + this.client.log.info( { exitCode : exitCode }, 'Door exited'); - if(sockServer) { - sockServer.close(); + if(this.sockServer) { + this.sockServer.close(); } // we may not get a close - if('stdio' === self.exeInfo.io) { - self.restoreIo(door); + if('stdio' === this.io) { + this.restoreIo(door); } door.removeAllListeners(); - return self.doorExited(); + return cb(null); }); - }); + } + + doorDataHandler(data) { + this.client.term.write(decode(data, this.encoding)); + } + + restoreIo(piped) { + if(!this.restored && this.client.term.output) { + this.client.term.output.unpipe(piped); + this.client.term.output.resume(); + this.restored = true; + } + } }; diff --git a/core/dropfile.js b/core/dropfile.js index 76977eaf..345a031a 100644 --- a/core/dropfile.js +++ b/core/dropfile.js @@ -1,16 +1,17 @@ /* jslint node: true */ 'use strict'; -var Config = require('./config.js').get; +// ENiGMA½ +const Config = require('./config.js').get; const StatLog = require('./stat_log.js'); -var fs = require('graceful-fs'); -var paths = require('path'); -var _ = require('lodash'); -var moment = require('moment'); -var iconv = require('iconv-lite'); - -exports.DropFile = DropFile; +// deps +const fs = require('graceful-fs'); +const paths = require('path'); +const _ = require('lodash'); +const moment = require('moment'); +const iconv = require('iconv-lite'); +const { mkdirs } = require('fs-extra'); // // Resources @@ -18,55 +19,57 @@ exports.DropFile = DropFile; // * https://en.wikipedia.org/wiki/Talk%3ADropfile // * http://thoughtproject.com/libraries/bbs/Sysop/Doors/DropFiles/index.htm // * http://thebbs.org/bbsfaq/ch06.02.htm +// * http://lord.lordlegacy.com/dosemu/ +// +module.exports = class DropFile { + constructor(client, { fileType = 'DORINFO', baseDir = Config().paths.dropFiles } = {} ) { + this.client = client; + this.fileType = fileType.toUpperCase(); + this.baseDir = baseDir; + } -// http://lord.lordlegacy.com/dosemu/ + get fullPath() { + return paths.join(this.baseDir, ('node' + this.client.node), this.fileName); + } -function DropFile(client, fileType) { + get fileName() { + return { + DOOR : 'DOOR.SYS', // GAP BBS, many others + DOOR32 : 'DOOR32.SYS', // EleBBS / Mystic, Syncronet, Maximus, Telegard, AdeptXBBS, ... + CALLINFO : 'CALLINFO.BBS', // Citadel? + DORINFO : this.getDoorInfoFileName(), // RBBS, RemoteAccess, QBBS, ... + CHAIN : 'CHAIN.TXT', // WWIV + CURRUSER : 'CURRUSER.BBS', // RyBBS + SFDOORS : 'SFDOORS.DAT', // Spitfire + PCBOARD : 'PCBOARD.SYS', // PCBoard + TRIBBS : 'TRIBBS.SYS', // TriBBS + USERINFO : 'USERINFO.DAT', // Wildcat! 3.0+ + JUMPER : 'JUMPER.DAT', // 2AM BBS + SXDOOR : 'SXDOOR.' + _.pad(this.client.node.toString(), 3, '0'), // System/X, dESiRE + INFO : 'INFO.BBS', // Phoenix BBS + }[this.fileType]; + } - var self = this; - this.client = client; - this.fileType = (fileType || 'DORINFO').toUpperCase(); + isSupported() { + return this.getHandler() ? true : false; + } - Object.defineProperty(this, 'fullPath', { - get : function() { - return paths.join(Config().paths.dropFiles, ('node' + self.client.node), self.fileName); - } - }); + getHandler() { + return { + DOOR : this.getDoorSysBuffer, + DOOR32 : this.getDoor32Buffer, + DORINFO : this.getDoorInfoDefBuffer, + }[this.fileType]; + } - Object.defineProperty(this, 'fileName', { - get : function() { - return { - DOOR : 'DOOR.SYS', // GAP BBS, many others - DOOR32 : 'DOOR32.SYS', // EleBBS / Mystic, Syncronet, Maximus, Telegard, AdeptXBBS, ... - CALLINFO : 'CALLINFO.BBS', // Citadel? - DORINFO : self.getDoorInfoFileName(), // RBBS, RemoteAccess, QBBS, ... - CHAIN : 'CHAIN.TXT', // WWIV - CURRUSER : 'CURRUSER.BBS', // RyBBS - SFDOORS : 'SFDOORS.DAT', // Spitfire - PCBOARD : 'PCBOARD.SYS', // PCBoard - TRIBBS : 'TRIBBS.SYS', // TriBBS - USERINFO : 'USERINFO.DAT', // Wildcat! 3.0+ - JUMPER : 'JUMPER.DAT', // 2AM BBS - SXDOOR : // System/X, dESiRE - 'SXDOOR.' + _.pad(self.client.node.toString(), 3, '0'), - INFO : 'INFO.BBS', // Phoenix BBS - }[self.fileType]; - } - }); + getContents() { + const handler = this.getHandler().bind(this); + return handler(); + } - Object.defineProperty(this, 'dropFileContents', { - get : function() { - return { - DOOR : self.getDoorSysBuffer(), - DOOR32 : self.getDoor32Buffer(), - DORINFO : self.getDoorInfoDefBuffer(), - }[self.fileType]; - } - }); - - this.getDoorInfoFileName = function() { - var x; - var node = self.client.node; + getDoorInfoFileName() { + let x; + const node = this.client.node; if(10 === node) { x = 0; } else if(node < 10) { @@ -75,54 +78,53 @@ function DropFile(client, fileType) { x = String.fromCharCode('a'.charCodeAt(0) + (node - 11)); } return 'DORINFO' + x + '.DEF'; - }; + } - this.getDoorSysBuffer = function() { - var up = self.client.user.properties; - var now = moment(); - var secLevel = self.client.user.getLegacySecurityLevel().toString(); + getDoorSysBuffer() { + const prop = this.client.user.properties; + const now = moment(); + const secLevel = this.client.user.getLegacySecurityLevel().toString(); // :TODO: fix time remaining // :TODO: fix default protocol -- user prop: transfer_protocol - return iconv.encode( [ '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" + this.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.real_name || self.client.user.username, // "User Full Name" - up.location || 'Anywhere', // "Calling From" + prop.real_name || this.client.user.username, // "User Full Name" + prop.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" + prop.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" + this.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" + this.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" + moment(prop.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" StatLog.getSystemStat('sysop_username'), // "Sysop's Name (name BBS refers to Sysop as)" - self.client.user.username, // "Alias name" + this.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)" @@ -136,40 +138,46 @@ function DropFile(client, fileType) { 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', // "Files d/led so far today" '0', // "Total "K" Bytes Uploaded" '0', // "Total "K" Bytes Downloaded" - up.user_comment || 'None', // "User Comment" + prop.user_comment || 'None', // "User Comment" '0', // "Total Doors Opened" '0', // "Total Messages Left" ].join('\r\n') + '\r\n', 'cp437'); - }; + } - this.getDoor32Buffer = function() { + getDoor32Buffer() { // // Resources: // * http://wiki.bbses.info/index.php/DOOR32.SYS // // :TODO: local/serial/telnet need to be configurable -- which also changes socket handle! + const Door32CommTypes = { + Local : 0, + Serial : 1, + Telnet : 2, + }; + + const commType = Door32CommTypes.Telnet; + return iconv.encode([ - '2', // :TODO: This needs to be configurable! - // :TODO: Completely broken right now -- This need to be configurable & come from temp socket server most likely - '-1', // self.client.output._handle.fd.toString(), // :TODO: ALWAYS -1 on Windows! - '57600', + commType.toString(), + '-1', + '115200', Config().general.boardName, - self.client.user.userId.toString(), - self.client.user.properties.real_name || self.client.user.username, - self.client.user.username, - self.client.user.getLegacySecurityLevel().toString(), + this.client.user.userId.toString(), + this.client.user.properties.real_name || this.client.user.username, + this.client.user.username, + this.client.user.getLegacySecurityLevel().toString(), '546', // :TODO: Minutes left! '1', // ANSI - self.client.node.toString(), + this.client.node.toString(), ].join('\r\n') + '\r\n', 'cp437'); + } - }; - - this.getDoorInfoDefBuffer = function() { + getDoorInfoDefBuffer() { // :TODO: fix time remaining // @@ -178,34 +186,33 @@ function DropFile(client, fileType) { // // Note that usernames are just used for first/last names here // - var opUn = /[^\s]*/.exec(StatLog.getSystemStat('sysop_username'))[0]; - var un = /[^\s]*/.exec(self.client.user.username)[0]; - var secLevel = self.client.user.getLegacySecurityLevel().toString(); + const opUserName = /[^\s]*/.exec(StatLog.getSystemStat('sysop_username'))[0]; + const userName = /[^\s]*/.exec(this.client.user.username)[0]; + const secLevel = this.client.user.getLegacySecurityLevel().toString(); return iconv.encode( [ 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." + opUserName, // "The sysop's name up to the first space." + opUserName, // "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." + userName, // "The current user's name, up to the first space." + userName, // "The current user's name, following the first space." + this.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'); - }; + } -} - -DropFile.fileTypes = [ 'DORINFO' ]; - -DropFile.prototype.createFile = function(cb) { - fs.writeFile(this.fullPath, this.dropFileContents, function written(err) { - cb(err); - }); + createFile(cb) { + mkdirs(paths.dirname(this.fullPath), err => { + if(err) { + return cb(err); + } + return fs.writeFile(this.fullPath, this.getContents(), cb); + }); + } }; - diff --git a/core/enig_error.js b/core/enig_error.js index 63c90cb9..3f189dd7 100644 --- a/core/enig_error.js +++ b/core/enig_error.js @@ -35,6 +35,7 @@ exports.Errors = { MissingConfig : (reason, reasonCode) => new EnigError('Missing configuration', -32006, reason, reasonCode), UnexpectedState : (reason, reasonCode) => new EnigError('Unexpected state', -32007, reason, reasonCode), MissingParam : (reason, reasonCode) => new EnigError('Missing paramater(s)', -32008, reason, reasonCode), + MissingMci : (reason, reasonCode) => new EnigError('Missing required MCI code(s)', -32009, reason, reasonCode), }; exports.ErrorReasons = {