Bring in some of the good parts of dropfile-updates branch

This commit is contained in:
Bryan Ashby 2018-06-22 23:06:01 -06:00
commit ef12cc0cfb
4 changed files with 207 additions and 236 deletions

View File

@ -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() {

View File

@ -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;
}
}
};

View File

@ -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);
});
}
};

View File

@ -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 = {