diff --git a/README.md b/README.md index 66680240..e099dcdb 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ ENiGMA has been tested with many terminals. However, the following are suggested ## Installation On *nix type systems: ``` -curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/master/misc/install.sh | bash +curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/0.0.11-beta/misc/install.sh | bash ``` Please see [Installation Methods](https://nuskooler.github.io/enigma-bbs/installation/installation-methods.html) for Windows, Docker, and so on... diff --git a/UPGRADE.md b/UPGRADE.md index bd0a47b0..3fcd35a8 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -40,6 +40,9 @@ npm install Report your issue on Xibalba BBS, hop in #enigma-bbs on FreeNode and chat, or [file a issue on GitHub](https://github.com/NuSkooler/enigma-bbs/issues). +# 0.0.10-alpha to 0.0.11-beta +* Node.js 12.x LTS is now in use. Follow standard Node.js upgrade procedures (e.g.: `nvm install 12 && nvm use 12`). + # 0.0.9-alpha to 0.0.10-alpha * Security related files such as private keys and certs are now looked for in `config/security` by default. * Default archive handler for zip files has switched to InfoZip due to a bug in the latest p7Zip packages causing "volume not found" errors. Ensure you have the InfoZip `zip` and `unzip` commands in ENiGMA's path. You can switch back to 7Zip by overriding `archiveHandler` for `application/zip` in your `config.hjson` under `fileTypes` to `7Zip`. diff --git a/WHATSNEW.md b/WHATSNEW.md index bc8cc9f8..21626d92 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -1,6 +1,13 @@ # Whats New This document attempts to track **major** changes and additions in ENiGMA½. For details, see GitHub. +## 0.0.11-beta +* Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point! +* Development is now against Node.js 12.x LTS. Other versions may work but are not currently supported! +* [QWK support](/docs/messageareas/qwk.md) +* `oputil fb scan *areaTagWildcard*` scans all areas in which wildcard is matched. +* The archiver configuration `escapeTelnet` has been renamed `escapeIACs`. Support for the old value will be removed in the future. + ## 0.0.10-alpha + `oputil.js user rename USERNAME NEWNAME` + `my_messages.js` module (defaulted to "m" at the message menu) to list public messages addressed to the currently logged in user. Takes into account their username and `real_name` property. diff --git a/art/general/CONNECT1.ANS b/art/general/CONNECT1.ANS deleted file mode 100644 index d1a870bc..00000000 Binary files a/art/general/CONNECT1.ANS and /dev/null differ diff --git a/art/general/NEWSCAN.ANS b/art/general/NEWSCAN.ANS index 96371880..2762c9f1 100644 Binary files a/art/general/NEWSCAN.ANS and b/art/general/NEWSCAN.ANS differ diff --git a/art/themes/luciano_blocktronics/offline_mail.ans b/art/themes/luciano_blocktronics/offline_mail.ans new file mode 100644 index 00000000..ffd21e9e Binary files /dev/null and b/art/themes/luciano_blocktronics/offline_mail.ans differ diff --git a/art/themes/luciano_blocktronics/qwk_export_progress.ans b/art/themes/luciano_blocktronics/qwk_export_progress.ans new file mode 100644 index 00000000..dcde7d82 Binary files /dev/null and b/art/themes/luciano_blocktronics/qwk_export_progress.ans differ diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 6d8490dc..d812a9c3 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -288,6 +288,17 @@ } } + qwkExportPacketCurrentConfig: { + mci: { + TL1: { + width: 70 + } + TL2: { + width: 70 + } + } + } + mailMenuCreateMessage: { 0: { mci: { diff --git a/core/ansi_escape_parser.js b/core/ansi_escape_parser.js index 9786ea4c..88063bef 100644 --- a/core/ansi_escape_parser.js +++ b/core/ansi_escape_parser.js @@ -87,6 +87,7 @@ function ANSIEscapeParser(options) { let pos = 0; let start = 0; let charCode; + let lastCharCode; while(pos < len) { charCode = text.charCodeAt(pos) & 0xff; // 8bit clean @@ -102,6 +103,12 @@ function ANSIEscapeParser(options) { break; case LF : + // Handle ANSI saved with UNIX-style LF's only + // vs the CRLF pairs + if (lastCharCode !== CR) { + self.column = 1; + } + self.emit('literal', text.slice(start, pos)); start = pos; @@ -126,6 +133,7 @@ function ANSIEscapeParser(options) { } ++pos; + lastCharCode = charCode; } // diff --git a/core/archive_util.js b/core/archive_util.js index 8549cd12..47291860 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -204,23 +204,37 @@ module.exports = class ArchiveUtil { }); } - compressTo(archType, archivePath, files, cb) { + compressTo(archType, archivePath, files, workDir, cb) { const archiver = this.getArchiver(archType, paths.extname(archivePath)); if(!archiver) { return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); } + if (!cb && _.isFunction(workDir)) { + cb = workDir; + workDir = null; + } + const fmtObj = { archivePath : archivePath, fileList : files.join(' '), // :TODO: probably need same hack as extractTo here! }; - const args = archiver.compress.args.map( arg => stringFormat(arg, fmtObj) ); + // :TODO: DRY with extractTo() + const args = archiver.compress.args.map( arg => { + return '{fileList}' === arg ? arg : stringFormat(arg, fmtObj); + }); + + const fileListPos = args.indexOf('{fileList}'); + if(fileListPos > -1) { + // replace {fileList} with 0:n sep file list arguments + args.splice.apply(args, [fileListPos, 1].concat(files)); + } let proc; try { - proc = pty.spawn(archiver.compress.cmd, args, this.getPtyOpts()); + proc = pty.spawn(archiver.compress.cmd, args, this.getPtyOpts(workDir)); } catch(e) { return cb(Errors.ExternalProcess( `Error spawning archiver process "${archiver.compress.cmd}" with args "${args.join(' ')}": ${e.message}`) @@ -332,15 +346,15 @@ module.exports = class ArchiveUtil { }); } - getPtyOpts(extractPath) { + getPtyOpts(cwd) { const opts = { name : 'enigma-archiver', cols : 80, rows : 24, env : process.env, }; - if(extractPath) { - opts.cwd = extractPath; + if(cwd) { + opts.cwd = cwd; } // :TODO: set cwd to supplied temp path if not sepcific extract return opts; diff --git a/core/art.js b/core/art.js index 38594985..0ff4835f 100644 --- a/core/art.js +++ b/core/art.js @@ -49,7 +49,7 @@ function getFontNameFromSAUCE(sauce) { function sliceAtEOF(data, eofMarker) { let eof = data.length; - const stopPos = Math.max(data.length - (256), 0); // 256 = 2 * sizeof(SAUCE) + const stopPos = Math.max(data.length - 256, 0); // 256 = 2 * sizeof(SAUCE) for(let i = eof - 1; i > stopPos; i--) { if(eofMarker === data[i]) { @@ -57,9 +57,16 @@ function sliceAtEOF(data, eofMarker) { break; } } - if(eof === data.length || eof < 128) { + + if (eof === data.length) { + return data; // nothing to do + } + + // try to prevent goofs + if (eof < 128 && 'SAUCE00' !== data.slice(eof + 1, eof + 8).toString()) { return data; } + return data.slice(0, eof); } diff --git a/core/bbs.js b/core/bbs.js index 98ba1b7a..e29b8d27 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -189,8 +189,12 @@ function initialize(cb) { function basicInit(callback) { logger.init(); logger.log.info( - { version : require('../package.json').version }, - '**** ENiGMA½ Bulletin Board System Starting Up! ****'); + { + version : require('../package.json').version, + nodeVersion : process.version, + }, + '**** ENiGMA½ Bulletin Board System Starting Up! ****' + ); process.on('SIGINT', shutdownSystem); diff --git a/core/client.js b/core/client.js index 566aec5d..88c9ea0f 100644 --- a/core/client.js +++ b/core/client.js @@ -84,7 +84,7 @@ function Client(/*input, output*/) { this.user = new User(); this.currentTheme = { info : { name : 'N/A', description : 'None' } }; - this.lastKeyPressMs = Date.now(); + this.lastActivityTime = Date.now(); this.menuStack = new MenuStack(this); this.acs = new ACS( { client : this, user : this.user } ); this.mciCache = {}; @@ -96,7 +96,7 @@ function Client(/*input, output*/) { Object.defineProperty(this, 'node', { get : function() { - return self.session.id + 1; + return self.session.id; } }); @@ -107,11 +107,13 @@ function Client(/*input, output*/) { }); this.setTemporaryDirectDataHandler = function(handler) { + this.dataPassthrough = true; // let implementations do with what they will here this.input.removeAllListeners('data'); this.input.on('data', handler); }; this.restoreDataHandler = function() { + this.dataPassthrough = false; this.input.removeAllListeners('data'); this.input.on('data', this.dataHandler); }; @@ -406,7 +408,7 @@ function Client(/*input, output*/) { self.log.trace( { key : key, ch : escape(ch) }, 'User keyboard input'); // jshint ignore:line } - self.lastKeyPressMs = Date.now(); + self.lastActivityTime = Date.now(); if(!self.ignoreInput) { self.emit('key press', ch, key); @@ -438,7 +440,7 @@ Client.prototype.startIdleMonitor = function() { this.stopIdleMonitor(); } - this.lastKeyPressMs = Date.now(); + this.lastActivityTime = Date.now(); // // Every 1m, check for idle. @@ -476,7 +478,7 @@ Client.prototype.startIdleMonitor = function() { // use override value if set idleLogoutSeconds = this.idleLogoutSecondsOverride || idleLogoutSeconds; - if(idleLogoutSeconds > 0 && (nowMs - this.lastKeyPressMs >= (idleLogoutSeconds * 1000))) { + if(idleLogoutSeconds > 0 && (nowMs - this.lastActivityTime >= (idleLogoutSeconds * 1000))) { this.emit('idle timeout'); } }, 1000 * 60); @@ -489,6 +491,10 @@ Client.prototype.stopIdleMonitor = function() { } }; +Client.prototype.explicitActivityTimeUpdate = function() { + this.lastActivityTime = Date.now(); +} + Client.prototype.overrideIdleLogoutSeconds = function(seconds) { this.idleLogoutSecondsOverride = seconds; }; diff --git a/core/client_connections.js b/core/client_connections.js index e2c8d577..d1a6be6e 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -61,27 +61,27 @@ function getActiveConnectionList(authUsersOnly) { function addNewClient(client, clientSock) { // - // Assign ID/client ID to next lowest & available # + // Find a node ID "slot" // - let id = 0; - for(let i = 0; i < clientConnections.length; ++i) { - if(clientConnections[i].id > id) { - break; + let nodeId; + for (nodeId = 1; nodeId < Number.MAX_SAFE_INTEGER; ++nodeId) { + const existing = clientConnections.find(client => nodeId === client.node); + if (!existing) { + break; // available slot } - id++; } - client.session.id = id; - const remoteAddress = client.remoteAddress = clientSock.remoteAddress; + client.session.id = nodeId; + const remoteAddress = client.remoteAddress = clientSock.remoteAddress; // create a unique identifier one-time ID for this session - client.session.uniqueId = new hashids('ENiGMA½ClientSession').encode([ id, moment().valueOf() ]); + client.session.uniqueId = new hashids('ENiGMA½ClientSession').encode([ nodeId, moment().valueOf() ]); clientConnections.push(client); clientConnections.sort( (c1, c2) => c1.session.id - c2.session.id); // Create a client specific logger // Note that this will be updated @ login with additional information - client.log = logger.log.child( { clientId : id, sessionId : client.session.uniqueId } ); + client.log = logger.log.child( { nodeId, sessionId : client.session.uniqueId } ); const connInfo = { remoteAddress : remoteAddress, @@ -101,7 +101,7 @@ function addNewClient(client, clientSock) { { client : client, connectionCount : clientConnections.length } ); - return id; + return nodeId; } function removeClient(client) { @@ -114,7 +114,7 @@ function removeClient(client) { logger.log.info( { connectionCount : clientConnections.length, - clientId : client.session.id + nodeId : client.node, }, 'Client disconnected' ); diff --git a/core/config.js b/core/config.js index 44b5f4cc..7e467709 100644 --- a/core/config.js +++ b/core/config.js @@ -678,7 +678,7 @@ function getDefaultConfig() { }, decompress : { cmd : 'unzip', - args : [ '{archivePath}', '-d', '{extractPath}' ], + args : [ '-n', '{archivePath}', '-d', '{extractPath}' ], }, list : { cmd : 'unzip', @@ -688,7 +688,7 @@ function getDefaultConfig() { }, extract : { cmd : 'unzip', - args : [ '{archivePath}', '{fileList}', '-d', '{extractPath}' ], + args : [ '-n', '{archivePath}', '{fileList}', '-d', '{extractPath}' ], } }, @@ -865,8 +865,7 @@ function getDefaultConfig() { recvArgs : [ '--zmodem', '--binary', '--restricted', '--keep-uppercase', // dumps to CWD which is set to {uploadDir} ], - // :TODO: can we not just use --escape ? - escapeTelnet : true, // set to true to escape Telnet codes such as IAC + processIACs : true, // escape/de-escape IACs (0xff) } } }, diff --git a/core/cp437util.js b/core/cp437util.js new file mode 100644 index 00000000..32425d3a --- /dev/null +++ b/core/cp437util.js @@ -0,0 +1,55 @@ + + +const CP437UnicodeTable = [ + '\u0000', '\u0001', '\u0002', '\u0003', '\u0004', '\u0005', '\u0006', + '\u0007', '\u0008', '\u0009', '\u000A', '\u000B', '\u000C', '\u000D', + '\u000E', '\u000F', '\u0010', '\u0011', '\u0012', '\u0013', '\u0014', + '\u0015', '\u0016', '\u0017', '\u0018', '\u0019', '\u001A', '\u001B', + '\u001C', '\u001D', '\u001E', '\u001F', '\u0020', '\u0021', '\u0022', + '\u0023', '\u0024', '\u0025', '\u0026', '\u0027', '\u0028', '\u0029', + '\u002A', '\u002B', '\u002C', '\u002D', '\u002E', '\u002F', '\u0030', + '\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037', + '\u0038', '\u0039', '\u003A', '\u003B', '\u003C', '\u003D', '\u003E', + '\u003F', '\u0040', '\u0041', '\u0042', '\u0043', '\u0044', '\u0045', + '\u0046', '\u0047', '\u0048', '\u0049', '\u004A', '\u004B', '\u004C', + '\u004D', '\u004E', '\u004F', '\u0050', '\u0051', '\u0052', '\u0053', + '\u0054', '\u0055', '\u0056', '\u0057', '\u0058', '\u0059', '\u005A', + '\u005B', '\u005C', '\u005D', '\u005E', '\u005F', '\u0060', '\u0061', + '\u0062', '\u0063', '\u0064', '\u0065', '\u0066', '\u0067', '\u0068', + '\u0069', '\u006A', '\u006B', '\u006C', '\u006D', '\u006E', '\u006F', + '\u0070', '\u0071', '\u0072', '\u0073', '\u0074', '\u0075', '\u0076', + '\u0077', '\u0078', '\u0079', '\u007A', '\u007B', '\u007C', '\u007D', + '\u007E', '\u007F', '\u00C7', '\u00FC', '\u00E9', '\u00E2', '\u00E4', + '\u00E0', '\u00E5', '\u00E7', '\u00EA', '\u00EB', '\u00E8', '\u00EF', + '\u00EE', '\u00EC', '\u00C4', '\u00C5', '\u00C9', '\u00E6', '\u00C6', + '\u00F4', '\u00F6', '\u00F2', '\u00FB', '\u00F9', '\u00FF', '\u00D6', + '\u00DC', '\u00A2', '\u00A3', '\u00A5', '\u20A7', '\u0192', '\u00E1', + '\u00ED', '\u00F3', '\u00FA', '\u00F1', '\u00D1', '\u00AA', '\u00BA', + '\u00BF', '\u2310', '\u00AC', '\u00BD', '\u00BC', '\u00A1', '\u00AB', + '\u00BB', '\u2591', '\u2592', '\u2593', '\u2502', '\u2524', '\u2561', + '\u2562', '\u2556', '\u2555', '\u2563', '\u2551', '\u2557', '\u255D', + '\u255C', '\u255B', '\u2510', '\u2514', '\u2534', '\u252C', '\u251C', + '\u2500', '\u253C', '\u255E', '\u255F', '\u255A', '\u2554', '\u2569', + '\u2566', '\u2560', '\u2550', '\u256C', '\u2567', '\u2568', '\u2564', + '\u2565', '\u2559', '\u2558', '\u2552', '\u2553', '\u256B', '\u256A', + '\u2518', '\u250C', '\u2588', '\u2584', '\u258C', '\u2590', '\u2580', + '\u03B1', '\u00DF', '\u0393', '\u03C0', '\u03A3', '\u03C3', '\u00B5', + '\u03C4', '\u03A6', '\u0398', '\u03A9', '\u03B4', '\u221E', '\u03C6', + '\u03B5', '\u2229', '\u2261', '\u00B1', '\u2265', '\u2264', '\u2320', + '\u2321', '\u00F7', '\u2248', '\u00B0', '\u2219', '\u00B7', '\u221A', + '\u207F', '\u00B2', '\u25A0', '\u00A0' +]; + +const NonCP437EncodableRegExp = /[^\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\u000A\u000B\u000C\u000D\u000E\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F\u0020\u0021\u0022\u0023\u0024\u0025\u0026\u0027\u0028\u0029\u002A\u002B\u002C\u002D\u002E\u002F\u0030\u0031\u0032\u0033\u0034\u0035\u0036\u0037\u0038\u0039\u003A\u003B\u003C\u003D\u003E\u003F\u0040\u0041\u0042\u0043\u0044\u0045\u0046\u0047\u0048\u0049\u004A\u004B\u004C\u004D\u004E\u004F\u0050\u0051\u0052\u0053\u0054\u0055\u0056\u0057\u0058\u0059\u005A\u005B\u005C\u005D\u005E\u005F\u0060\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u0068\u0069\u006A\u006B\u006C\u006D\u006E\u006F\u0070\u0071\u0072\u0073\u0074\u0075\u0076\u0077\u0078\u0079\u007A\u007B\u007C\u007D\u007E\u007F\u00C7\u00FC\u00E9\u00E2\u00E4\u00E0\u00E5\u00E7\u00EA\u00EB\u00E8\u00EF\u00EE\u00EC\u00C4\u00C5\u00C9\u00E6\u00C6\u00F4\u00F6\u00F2\u00FB\u00F9\u00FF\u00D6\u00DC\u00A2\u00A3\u00A5\u20A7\u0192\u00E1\u00ED\u00F3\u00FA\u00F1\u00D1\u00AA\u00BA\u00BF\u2310\u00AC\u00BD\u00BC\u00A1\u00AB\u00BB\u2591\u2592\u2593\u2502\u2524\u2561\u2562\u2556\u2555\u2563\u2551\u2557\u255D\u255C\u255B\u2510\u2514\u2534\u252C\u251C\u2500\u253C\u255E\u255F\u255A\u2554\u2569\u2566\u2560\u2550\u256C\u2567\u2568\u2564\u2565\u2559\u2558\u2552\u2553\u256B\u256A\u2518\u250C\u2588\u2584\u258C\u2590\u2580\u03B1\u00DF\u0393\u03C0\u03A3\u03C3\u00B5\u03C4\u03A6\u0398\u03A9\u03B4\u221E\u03C6\u03B5\u2229\u2261\u00B1\u2265\u2264\u2320\u2321\u00F7\u2248\u00B0\u2219\u00B7\u221A\u207F\u00B2\u25A0\u00A0]/; +const isCP437Encodable = (s) => { + if (!s.length) { + return true; + } + + return !NonCP437EncodableRegExp.test(s); +} + +module.exports = { + CP437UnicodeTable, + isCP437Encodable, +} \ No newline at end of file diff --git a/core/door_util.js b/core/door_util.js index 6517f1be..24c2cd80 100644 --- a/core/door_util.js +++ b/core/door_util.js @@ -16,6 +16,10 @@ function trackDoorRunBegin(client, doorTag) { } function trackDoorRunEnd(trackInfo) { + if (!trackInfo) { + return; + } + const { startTime, client, doorTag } = trackInfo; const diff = moment.duration(moment().diff(startTime)); diff --git a/core/download_queue.js b/core/download_queue.js index 28ca3aac..4380ded1 100644 --- a/core/download_queue.js +++ b/core/download_queue.js @@ -1,11 +1,12 @@ /* jslint node: true */ 'use strict'; -const FileEntry = require('./file_entry.js'); -const UserProps = require('./user_property.js'); +const FileEntry = require('./file_entry'); +const UserProps = require('./user_property'); +const Events = require('./events'); // deps -const { partition } = require('lodash'); +const _ = require('lodash'); module.exports = class DownloadQueue { constructor(client) { @@ -20,6 +21,10 @@ module.exports = class DownloadQueue { } } + static get(client) { + return new DownloadQueue(client); + } + get items() { return this.client.user.downloadQueue; } @@ -52,7 +57,7 @@ module.exports = class DownloadQueue { fileIds = [ fileIds ]; } - const [ remain, removed ] = partition(this.client.user.downloadQueue, e => ( -1 === fileIds.indexOf(e.fileId) )); + const [ remain, removed ] = _.partition(this.client.user.downloadQueue, e => ( -1 === fileIds.indexOf(e.fileId) )); this.client.user.downloadQueue = remain; return removed; } @@ -76,4 +81,23 @@ module.exports = class DownloadQueue { this.client.log.error( { error : e.message, property : prop }, 'Failed parsing download queue property'); } } + + addTemporaryDownload(entry) { + this.add(entry, true); // true=systemFile + + // clean up after ourselves when the session ends + const thisUniqueId = this.client.session.uniqueId; + Events.once(Events.getSystemEvents().ClientDisconnected, evt => { + if(thisUniqueId === _.get(evt, 'client.session.uniqueId')) { + FileEntry.removeEntry(entry, { removePhysFile : true }, err => { + const Log = require('./logger').log; + if(err) { + Log.warn( { fileId : entry.fileId, path : entry.filePath }, 'Failed removing temporary session download' ); + } else { + Log.debug( { fileId : entry.fileId, path : entry.filePath }, 'Removed temporary session download item' ); + } + }); + } + }); + } }; diff --git a/core/enig_error.js b/core/enig_error.js index 08a3312e..4d88cfa1 100644 --- a/core/enig_error.js +++ b/core/enig_error.js @@ -37,6 +37,8 @@ exports.Errors = { MissingParam : (reason, reasonCode) => new EnigError('Missing paramter(s)', -32008, reason, reasonCode), MissingMci : (reason, reasonCode) => new EnigError('Missing required MCI code(s)', -32009, reason, reasonCode), BadLogin : (reason, reasonCode) => new EnigError('Bad login attempt', -32010, reason, reasonCode), + UserInterrupt : (reason, reasonCode) => new EnigError('User interrupted', -32011, reason, reasonCode), + NothingToDo : (reason, reasonCode) => new EnigError('Nothing to do', -32012, reason, reasonCode), }; exports.ErrorReasons = { diff --git a/core/file_base_area.js b/core/file_base_area.js index e3887b84..e9926111 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -17,6 +17,7 @@ const StatLog = require('./stat_log.js'); const UserProps = require('./user_property.js'); const SysProps = require('./system_property.js'); const SAUCE = require('./sauce.js'); +const { wildcardMatch } = require('./string_util'); // deps const _ = require('lodash'); @@ -40,6 +41,7 @@ exports.getAreaDefaultStorageDirectory = getAreaDefaultStorageDirectory; exports.getAreaStorageLocations = getAreaStorageLocations; exports.getDefaultFileAreaTag = getDefaultFileAreaTag; exports.getFileAreaByTag = getFileAreaByTag; +exports.getFileAreasByTagWildcardRule = getFileAreasByTagWildcardRule; exports.getFileEntryPath = getFileEntryPath; exports.changeFileAreaWithOptions = changeFileAreaWithOptions; exports.scanFile = scanFile; @@ -143,6 +145,15 @@ function getFileAreaByTag(areaTag) { } } +function getFileAreasByTagWildcardRule(rule) { + const areaTags = Object.keys(Config().fileBase.areas) + .filter(areaTag => { + return !isInternalArea(areaTag) && wildcardMatch(areaTag, rule); + }); + + return areaTags.map(areaTag => getFileAreaByTag(areaTag)); +} + function changeFileAreaWithOptions(client, areaTag, options, cb) { async.waterfall( [ diff --git a/core/file_base_filter.js b/core/file_base_filter.js index d72b3eea..ecf857fa 100644 --- a/core/file_base_filter.js +++ b/core/file_base_filter.js @@ -5,7 +5,7 @@ const UserProps = require('./user_property.js'); // deps const _ = require('lodash'); -const uuidV4 = require('uuid/v4'); +const { v4 : UUIDv4 } = require('uuid'); module.exports = class FileBaseFilters { constructor(client) { @@ -41,7 +41,7 @@ module.exports = class FileBaseFilters { } add(filterInfo) { - const filterUuid = uuidV4(); + const filterUuid = UUIDv4(); filterInfo.tags = this.cleanTags(filterInfo.tags); diff --git a/core/file_base_user_list_export.js b/core/file_base_user_list_export.js index 3c00d167..c3f3b6f8 100644 --- a/core/file_base_user_list_export.js +++ b/core/file_base_user_list_export.js @@ -7,8 +7,6 @@ const FileEntry = require('./file_entry.js'); const FileArea = require('./file_base_area.js'); const { renderSubstr } = require('./string_util.js'); const { Errors } = require('./enig_error.js'); -const Events = require('./events.js'); -const Log = require('./logger.js').log; const DownloadQueue = require('./download_queue.js'); const { exportFileList } = require('./file_base_list_export.js'); @@ -19,7 +17,7 @@ const fs = require('graceful-fs'); const fse = require('fs-extra'); const paths = require('path'); const moment = require('moment'); -const uuidv4 = require('uuid/v4'); +const { v4 : UUIDv4 } = require('uuid'); const yazl = require('yazl'); /* @@ -28,7 +26,7 @@ const yazl = require('yazl'); tsFormat - timestamp format (theme 'short') descWidth - max desc width (45) progBarChar - progress bar character (▒) - compressThreshold - threshold to kick in comrpession for lists (1.44 MiB) + compressThreshold - threshold to kick in compression for lists (1.44 MiB) templates - object containing: header - filename of header template (misc/file_list_header.asc) entry - filename of entry template (misc/file_list_entry.asc) @@ -190,7 +188,7 @@ exports.getModule = class FileBaseListExport extends MenuModule { const outputFileName = paths.join( sysTempDownloadDir, - `file_list_${uuidv4().substr(-8)}_${moment().format('YYYY-MM-DD')}.txt` + `file_list_${UUIDv4().substr(-8)}_${moment().format('YYYY-MM-DD')}.txt` ); fs.writeFile(outputFileName, listBody, 'utf8', err => { @@ -222,28 +220,14 @@ exports.getModule = class FileBaseListExport extends MenuModule { newEntry.persist(err => { if(!err) { // queue it! - const dlQueue = new DownloadQueue(self.client); - dlQueue.add(newEntry, true); // true=systemFile - - // clean up after ourselves when the session ends - const thisClientId = self.client.session.id; - Events.once(Events.getSystemEvents().ClientDisconnected, evt => { - if(thisClientId === _.get(evt, 'client.session.id')) { - FileEntry.removeEntry(newEntry, { removePhysFile : true }, err => { - if(err) { - Log.warn( { fileId : newEntry.fileId, path : outputFileName }, 'Failed removing temporary session download' ); - } else { - Log.debug( { fileId : newEntry.fileId, path : outputFileName }, 'Removed temporary session download item' ); - } - }); - } - }); + DownloadQueue.get(self.client).addTemporaryDownload(newEntry); } return callback(err); }); }, function done(callback) { // re-enable idle monitor + // :TODO: this should probably be moved down below at the end of the full waterfall self.client.startIdleMonitor(); updateStatus('Exported list has been added to your download queue'); diff --git a/core/file_transfer.js b/core/file_transfer.js index 5f0138e2..68daf2db 100644 --- a/core/file_transfer.js +++ b/core/file_transfer.js @@ -364,6 +364,16 @@ exports.getModule = class TransferFileModule extends MenuModule { const external = this.protocolConfig.external; const cmd = external[`${this.direction}Cmd`]; + // support for handlers that need IACs taken care of over Telnet/etc. + const processIACs = + external.processIACs || + external.escapeTelnet; // deprecated name + + // :TODO: we should only do this when over Telnet (or derived, such as WebSockets)? + + const IAC = Buffer.from([255]); + const EscapedIAC = Buffer.from([255, 255]); + this.client.log.debug( { cmd : cmd, args : args, tempDir : this.recvDirectory, direction : this.direction }, 'Executing external protocol' @@ -378,21 +388,68 @@ exports.getModule = class TransferFileModule extends MenuModule { const externalProc = pty.spawn(cmd, args, spawnOpts); + let dataHits = 0; + const updateActivity = () => { + if (0 === (dataHits++ % 4)) { + this.client.explicitActivityTimeUpdate(); + } + }; + this.client.setTemporaryDirectDataHandler(data => { + updateActivity(); + // needed for things like sz/rz - if(external.escapeTelnet) { - const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape - externalProc.write(Buffer.from(tmp, 'binary')); + if(processIACs) { + let iacPos = data.indexOf(EscapedIAC); + if (-1 === iacPos) { + return externalProc.write(data); + } + + // at least one double (escaped) IAC + let lastPos = 0; + while (iacPos > -1) { + let rem = iacPos - lastPos; + if (rem >= 0) { + externalProc.write(data.slice(lastPos, iacPos + 1)); + } + lastPos = iacPos + 2; + iacPos = data.indexOf(EscapedIAC, lastPos); + } + + if (lastPos < data.length) { + externalProc.write(data.slice(lastPos)); + } + // const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape + // externalProc.write(Buffer.from(tmp, 'binary')); } else { externalProc.write(data); } }); externalProc.on('data', data => { + updateActivity(); + // needed for things like sz/rz - if(external.escapeTelnet) { - const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape - this.client.term.rawWrite(Buffer.from(tmp, 'binary')); + if(processIACs) { + let iacPos = data.indexOf(IAC); + if (-1 === iacPos) { + return this.client.term.rawWrite(data); + } + + // Has at least a single IAC + let lastPos = 0; + while (iacPos !== -1) { + if (iacPos - lastPos > 0) { + this.client.term.rawWrite(data.slice(lastPos, iacPos)); + } + this.client.term.rawWrite(EscapedIAC); + lastPos = iacPos + 1; + iacPos = data.indexOf(IAC, lastPos); + } + + if (lastPos < data.length) { + this.client.term.rawWrite(data.slice(lastPos)); + } } else { this.client.term.rawWrite(data); } diff --git a/core/ftn_mail_packet.js b/core/ftn_mail_packet.js index 92e28270..781ed929 100644 --- a/core/ftn_mail_packet.js +++ b/core/ftn_mail_packet.js @@ -165,6 +165,74 @@ exports.PacketHeader = PacketHeader; // * Writeup on differences between type 2, 2.2, and 2+: // http://walon.org/pub/fidonet/FTSC-nodelists-etc./pkt-types.txt // +const PacketHeaderParser = new Parser() + .uint16le('origNode') + .uint16le('destNode') + .uint16le('year') + .uint16le('month') + .uint16le('day') + .uint16le('hour') + .uint16le('minute') + .uint16le('second') + .uint16le('baud') + .uint16le('packetType') + .uint16le('origNet') + .uint16le('destNet') + .int8('prodCodeLo') + .int8('prodRevLo') // aka serialNo + .buffer('password', { length : 8 }) // can't use string; need CP437 - see https://github.com/keichi/binary-parser/issues/33 + .uint16le('origZone') + .uint16le('destZone') + // + // The following is "filler" in FTS-0001, specifics in + // FSC-0045 and FSC-0048 + // + .uint16le('auxNet') + .uint16le('capWordValidate') + .int8('prodCodeHi') + .int8('prodRevHi') + .uint16le('capWord') + .uint16le('origZone2') + .uint16le('destZone2') + .uint16le('origPoint') + .uint16le('destPoint') + .uint32le('prodData'); + +const MessageHeaderParser = new Parser() + .uint16le('messageType') + .uint16le('ftn_msg_orig_node') + .uint16le('ftn_msg_dest_node') + .uint16le('ftn_msg_orig_net') + .uint16le('ftn_msg_dest_net') + .uint16le('ftn_attr_flags') + .uint16le('ftn_cost') + // + // It would be nice to just string() these, but we want CP437 which requires + // iconv. Another option would be to use a formatter, but until issue 33 + // (https://github.com/keichi/binary-parser/issues/33) is fixed, this is cumbersome. + // + .array('modDateTime', { + type : 'uint8', + length : 20, // FTS-0001.016: 20 bytes + }) + .array('toUserName', { + type : 'uint8', + // :TODO: array needs some soft of 'limit' field + readUntil : b => 0x00 === b, + }) + .array('fromUserName', { + type : 'uint8', + readUntil : b => 0x00 === b, + }) + .array('subject', { + type : 'uint8', + readUntil : b => 0x00 === b, + }) + .array('message', { + type : 'uint8', + readUntil : b => 0x00 === b, + }); + function Packet(options) { var self = this; @@ -175,39 +243,7 @@ function Packet(options) { let packetHeader; try { - packetHeader = new Parser() - .uint16le('origNode') - .uint16le('destNode') - .uint16le('year') - .uint16le('month') - .uint16le('day') - .uint16le('hour') - .uint16le('minute') - .uint16le('second') - .uint16le('baud') - .uint16le('packetType') - .uint16le('origNet') - .uint16le('destNet') - .int8('prodCodeLo') - .int8('prodRevLo') // aka serialNo - .buffer('password', { length : 8 }) // can't use string; need CP437 - see https://github.com/keichi/binary-parser/issues/33 - .uint16le('origZone') - .uint16le('destZone') - // - // The following is "filler" in FTS-0001, specifics in - // FSC-0045 and FSC-0048 - // - .uint16le('auxNet') - .uint16le('capWordValidate') - .int8('prodCodeHi') - .int8('prodRevHi') - .uint16le('capWord') - .uint16le('origZone2') - .uint16le('destZone2') - .uint16le('origPoint') - .uint16le('destPoint') - .uint32le('prodData') - .parse(packetBuffer); + packetHeader = PacketHeaderParser.parse(packetBuffer); } catch(e) { return Errors.Invalid(`Unable to parse FTN packet header: ${e.message}`); } @@ -544,41 +580,7 @@ function Packet(options) { let msgData; try { - msgData = new Parser() - .uint16le('messageType') - .uint16le('ftn_msg_orig_node') - .uint16le('ftn_msg_dest_node') - .uint16le('ftn_msg_orig_net') - .uint16le('ftn_msg_dest_net') - .uint16le('ftn_attr_flags') - .uint16le('ftn_cost') - // - // It would be nice to just string() these, but we want CP437 which requires - // iconv. Another option would be to use a formatter, but until issue 33 - // (https://github.com/keichi/binary-parser/issues/33) is fixed, this is cumbersome. - // - .array('modDateTime', { - type : 'uint8', - length : 20, // FTS-0001.016: 20 bytes - }) - .array('toUserName', { - type : 'uint8', - // :TODO: array needs some soft of 'limit' field - readUntil : b => 0x00 === b, - }) - .array('fromUserName', { - type : 'uint8', - readUntil : b => 0x00 === b, - }) - .array('subject', { - type : 'uint8', - readUntil : b => 0x00 === b, - }) - .array('message', { - type : 'uint8', - readUntil : b => 0x00 === b, - }) - .parse(packetBuffer); + msgData = MessageHeaderParser.parse(packetBuffer); } catch(e) { return cb(Errors.Invalid(`Failed to parse FTN message header: ${e.message}`)); } diff --git a/core/ftn_util.js b/core/ftn_util.js index e4637554..9d53c9a3 100644 --- a/core/ftn_util.js +++ b/core/ftn_util.js @@ -375,26 +375,28 @@ function getCharacterSetIdentifierByEncoding(encodingName) { return value ? `${value[0]} ${value[1]}` : encodingName.toUpperCase(); } +// http://ftsc.org/docs/fts-5003.001 +// http://www.unicode.org/L2/L1999/99325-N.htm function getEncodingFromCharacterSetIdentifier(chrs) { const ident = chrs.split(' ')[0].toUpperCase(); // :TODO: fill in the rest!!! return { // level 1 - 'ASCII' : 'iso-646-1', - 'DUTCH' : 'iso-646', - 'FINNISH' : 'iso-646-10', - 'FRENCH' : 'iso-646', - 'CANADIAN' : 'iso-646', - 'GERMAN' : 'iso-646', - 'ITALIAN' : 'iso-646', - 'NORWEIG' : 'iso-646', - 'PORTU' : 'iso-646', + 'ASCII' : 'ascii', // ISO-646-1 + 'DUTCH' : 'ascii', // ISO-646 + 'FINNISH' : 'ascii', // ISO-646-10 + 'FRENCH' : 'ascii', // ISO-646 + 'CANADIAN' : 'ascii', // ISO-646 + 'GERMAN' : 'ascii', // ISO-646 + 'ITALIAN' : 'ascii', // ISO-646 + 'NORWEIG' : 'ascii', // ISO-646 + 'PORTU' : 'ascii', // ISO-646 'SPANISH' : 'iso-656', - 'SWEDISH' : 'iso-646-10', - 'SWISS' : 'iso-646', - 'UK' : 'iso-646', - 'ISO-10' : 'iso-646-10', + 'SWEDISH' : 'ascii', // ISO-646-10 + 'SWISS' : 'ascii', // ISO-646 + 'UK' : 'ascii', // ISO-646 + 'ISO-10' : 'ascii', // ISO-646-10 // level 2 'CP437' : 'cp437', diff --git a/core/login_server_module.js b/core/login_server_module.js index a08abfe9..da3e06de 100644 --- a/core/login_server_module.js +++ b/core/login_server_module.js @@ -72,12 +72,12 @@ module.exports = class LoginServerModule extends ServerModule { }); client.on('error', err => { - logger.log.info({ clientId : client.session.id, error : err.message }, 'Connection error'); + logger.log.info({ nodeId : client.node, error : err.message }, 'Connection error'); }); client.on('close', err => { const logFunc = err ? logger.log.info : logger.log.debug; - logFunc( { clientId : client.session.id }, 'Connection closed'); + logFunc( { nodeId : client.node }, 'Connection closed'); clientConns.removeClient(client); }); diff --git a/core/mbf.js b/core/mbf.js new file mode 100644 index 00000000..9c3b2f6d --- /dev/null +++ b/core/mbf.js @@ -0,0 +1,59 @@ +const { Errors } = require('./enig_error'); + +// +// Utils for dealing with Microsoft Binary Format (MBF) used +// in various BASIC languages, etc. +// +// - https://en.wikipedia.org/wiki/Microsoft_Binary_Format +// - https://stackoverflow.com/questions/2268191/how-to-convert-from-ieee-python-float-to-microsoft-basic-float +// + +// Number to 32bit MBF +numToMbf32 = (v) => { + const mbf = Buffer.alloc(4); + + if (0 === v) { + return mbf; + } + + const ieee = Buffer.alloc(4); + ieee.writeFloatLE(v, 0); + + const sign = ieee[3] & 0x80; + let exp = (ieee[3] << 1) | (ieee[2] >> 7); + + if (exp === 0xfe) { + throw Errors.Invalid(`${v} cannot be converted to mbf`); + } + + exp += 2; + + mbf[3] = exp; + mbf[2] = sign | (ieee[2] & 0x7f); + mbf[1] = ieee[1]; + mbf[0] = ieee[0]; + + return mbf; +} + +mbf32ToNum = (mbf) => { + if (0 === mbf[3]) { + return 0.0; + } + + const ieee = Buffer.alloc(4); + const sign = mbf[2] & 0x80; + const exp = mbf[3] - 2; + + ieee[3] = sign | (exp >> 1); + ieee[2] = (exp << 7) | (mbf[2] & 0x7f); + ieee[1] = mbf[1]; + ieee[0] = mbf[0]; + + return ieee.readFloatLE(0); +} + +module.exports = { + numToMbf32, + mbf32ToNum, +} \ No newline at end of file diff --git a/core/message.js b/core/message.js index 5291b82a..b45e868c 100644 --- a/core/message.js +++ b/core/message.js @@ -11,6 +11,9 @@ const { sanitizeString, getISOTimestampString } = require('./database.js'); +const { isCP437Encodable } = require('./cp437util'); +const { containsNonLatinCodepoints } = require('./string_util'); + const { isAnsi, isFormattedLine, splitTextAtTerms, @@ -49,7 +52,8 @@ const SYSTEM_META_NAMES = { const ADDRESS_FLAVOR = { Local : 'local', // local / non-remote addressing FTN : 'ftn', // FTN style - Email : 'email', + Email : 'email', // From email + QWK : 'qwk', // QWK packet }; const STATE_FLAGS0 = { @@ -87,6 +91,13 @@ const FTN_PROPERTY_NAMES = { FtnSeenBy : 'ftn_seen_by', // http://ftsc.org/docs/fts-0004.001 }; +const QWKPropertyNames = { + MessageNumber : 'qwk_msg_num', + MessageStatus : 'qwk_msg_status', // See http://wiki.synchro.net/ref:qwk for a decent list + ConferenceNumber : 'qwk_conf_num', + InReplyToNum : 'qwk_in_reply_to_num', // note that we prefer the 'InReplyToMsgId' kludge if available +}; + // :TODO: this is a ugly hack due to bad variable names - clean it up & just _.camelCase(k)! const MESSAGE_ROW_MAP = { reply_to_message_id : 'replyToMsgId', @@ -96,15 +107,23 @@ const MESSAGE_ROW_MAP = { module.exports = class Message { constructor( { - messageId = 0, areaTag = Message.WellKnownAreaTags.Invalid, uuid, replyToMsgId = 0, - toUserName = '', fromUserName = '', subject = '', message = '', modTimestamp = moment(), - meta, hashTags = [], + messageId = 0, + areaTag = Message.WellKnownAreaTags.Invalid, + uuid, + replyToMsgId = 0, + toUserName = '', + fromUserName = '', + subject = '', + message = '', + modTimestamp = moment(), + meta, + hashTags = [], } = { } ) { this.messageId = messageId; this.areaTag = areaTag; - this.uuid = uuid; + this.messageUuid = uuid; this.replyToMsgId = replyToMsgId; this.toUserName = toUserName; this.fromUserName = fromUserName; @@ -123,6 +142,10 @@ module.exports = class Message { this.hashTags = hashTags; } + get uuid() { // deprecated, will be removed in the near future + return this.messageUuid; + } + isValid() { return true; } // :TODO: obviously useless; look into this or remove it static isPrivateAreaTag(areaTag) { @@ -137,6 +160,20 @@ module.exports = class Message { return null !== _.get(this, 'meta.System.remote_from_user', null); } + isCP437Encodable() { + return isCP437Encodable(this.toUserName) && + isCP437Encodable(this.fromUserName) && + isCP437Encodable(this.subject) && + isCP437Encodable(this.message); + } + + containsNonLatinCodepoints() { + return containsNonLatinCodepoints(this.toUserName) || + containsNonLatinCodepoints(this.fromUserName) || + containsNonLatinCodepoints(this.subject) || + containsNonLatinCodepoints(this.message); + } + /* :TODO: finish me static checkUserHasDeleteRights(user, messageIdOrUuid, cb) { @@ -183,6 +220,10 @@ module.exports = class Message { return FTN_PROPERTY_NAMES; } + static get QWKPropertyNames() { + return QWKPropertyNames; + } + setLocalToUserId(userId) { this.meta.System = this.meta.System || {}; this.meta.System[Message.SystemMetaNames.LocalToUserID] = userId; @@ -259,7 +300,7 @@ module.exports = class Message { filter.extraFields = [] filter.privateTagUserId = - if set, only private messages belonging to are processed - - any other areaTag or confTag filters will be ignored + - areaTags filter ignored - if NOT present, private areas are skipped *=NYI @@ -335,20 +376,23 @@ module.exports = class Message { )`); } else { if(filter.areaTag && filter.areaTag.length > 0) { - if(Array.isArray(filter.areaTag)) { - const areaList = filter.areaTag - .filter(t => t != Message.WellKnownAreaTags.Private) - .map(t => `"${t}"`).join(', '); - if(areaList.length > 0) { - appendWhereClause(`m.area_tag IN(${areaList})`); - } - } else if(_.isString(filter.areaTag) && Message.WellKnownAreaTags.Private !== filter.areaTag) { - appendWhereClause(`m.area_tag = "${filter.areaTag}"`); + if (!Array.isArray(filter.areaTag)) { + filter.areaTag = [ filter.areaTag ]; } - } - // explicit exclude of Private - appendWhereClause(`m.area_tag != "${Message.WellKnownAreaTags.Private}"`, 'AND'); + const areaList = filter.areaTag + .filter(t => t !== Message.WellKnownAreaTags.Private) + .map(t => `"${t}"`).join(', '); + if(areaList.length > 0) { + appendWhereClause(`m.area_tag IN(${areaList})`); + } else { + // nothing to do; no areas remain + return cb(null, []); + } + } else { + // explicit exclude of Private + appendWhereClause(`m.area_tag != "${Message.WellKnownAreaTags.Private}"`, 'AND'); + } } if(_.isNumber(filter.replyToMessageId)) { @@ -652,8 +696,8 @@ module.exports = class Message { function storeMessage(trans, callback) { // generate a UUID for this message if required (general case) const msgTimestamp = moment(); - if(!self.uuid) { - self.uuid = Message.createMessageUUID( + if(!self.messageUuid) { + self.messageUuid = Message.createMessageUUID( self.areaTag, msgTimestamp, self.subject, @@ -664,7 +708,10 @@ module.exports = class Message { trans.run( `INSERT INTO message (area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?);`, - [ self.areaTag, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, getISOTimestampString(msgTimestamp) ], + [ + self.areaTag, self.messageUuid, self.replyToMsgId, self.toUserName, + self.fromUserName, self.subject, self.message, getISOTimestampString(msgTimestamp) + ], function inserted(err) { // use non-arrow function for 'this' scope if(!err) { self.messageId = this.lastID; diff --git a/core/message_area.js b/core/message_area.js index 579f1c9b..30ad14ed 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -24,11 +24,13 @@ exports.getAvailableMessageConferences = getAvailableMessageConferences; exports.getSortedAvailMessageConferences = getSortedAvailMessageConferences; exports.getAvailableMessageAreasByConfTag = getAvailableMessageAreasByConfTag; exports.getSortedAvailMessageAreasByConfTag = getSortedAvailMessageAreasByConfTag; +exports.getAllAvailableMessageAreaTags = getAllAvailableMessageAreaTags; exports.getDefaultMessageConferenceTag = getDefaultMessageConferenceTag; exports.getDefaultMessageAreaTagByConfTag = getDefaultMessageAreaTagByConfTag; exports.getSuitableMessageConfAndAreaTags = getSuitableMessageConfAndAreaTags; exports.getMessageConferenceByTag = getMessageConferenceByTag; exports.getMessageAreaByTag = getMessageAreaByTag; +exports.getMessageConfTagByAreaTag = getMessageConfTagByAreaTag; exports.changeMessageConference = changeMessageConference; exports.changeMessageArea = changeMessageArea; exports.hasMessageConfAndAreaRead = hasMessageConfAndAreaRead; @@ -139,6 +141,20 @@ function getSortedAvailMessageAreasByConfTag(confTag, options) { return areas; } +function getAllAvailableMessageAreaTags(client, options) { + const areaTags = []; + + // mask over older messy APIs for now + const confOpts = Object.assign({}, options, { noClient : client ? false : true }); + const areaOpts = Object.assign({}, options, { client }); + + Object.keys(getAvailableMessageConferences(client, confOpts)).forEach(confTag => { + areaTags.push(...Object.keys(getAvailableMessageAreasByConfTag(confTag, areaOpts))); + }); + + return areaTags; +} + function getDefaultMessageConferenceTag(client, disableAcsCheck) { // // Find the first conference marked 'default'. If found, diff --git a/core/message_base_qwk_export.js b/core/message_base_qwk_export.js new file mode 100644 index 00000000..2f89c1bd --- /dev/null +++ b/core/message_base_qwk_export.js @@ -0,0 +1,428 @@ +// ENiGMA½ +const { MenuModule } = require('./menu_module'); +const Message = require('./message'); +const { Errors } = require('./enig_error'); +const { + getMessageAreaByTag, + getMessageConferenceByTag, + hasMessageConfAndAreaRead, + getAllAvailableMessageAreaTags, +} = require('./message_area'); +const FileArea = require('./file_base_area'); +const { QWKPacketWriter } = require('./qwk_mail_packet'); +const { renderSubstr } = require('./string_util'); +const Config = require('./config').get; +const FileEntry = require('./file_entry'); +const DownloadQueue = require('./download_queue'); +const { getISOTimestampString } = require('./database'); + +// deps +const async = require('async'); +const _ = require('lodash'); +const fse = require('fs-extra'); +const temptmp = require('temptmp'); +const paths = require('path'); +const { v4 : UUIDv4 } = require('uuid'); +const moment = require('moment'); + +const FormIds = { + main : 0, +}; + +const MciViewIds = { + main : { + status : 1, + progressBar : 2, + + customRangeStart : 10, + } +}; + +const UserProperties = { + ExportOptions : 'qwk_export_options', + ExportAreas : 'qwk_export_msg_areas', +}; + +exports.moduleInfo = { + name : 'QWK Export', + desc : 'Exports a QWK Packet for download', + author : 'NuSkooler', +}; + +exports.getModule = class MessageBaseQWKExport extends MenuModule { + constructor(options) { + super(options); + + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); + + this.config.progBarChar = renderSubstr( (this.config.progBarChar || '▒'), 0, 1); + this.config.bbsID = this.config.bbsID || _.get(Config(), 'messageNetworks.qwk.bbsID', 'ENIGMA'); + + this.tempName = `${UUIDv4().substr(-8).toUpperCase()}.QWK`; + this.sysTempDownloadArea = FileArea.getFileAreaByTag(FileArea.WellKnownAreaTags.TempDownloads); + } + + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if (err) { + return cb(err); + } + + async.waterfall( + [ + (callback) => { + this.prepViewController('main', FormIds.main, mciData.menu, err => { + return callback(err); + }); + }, + (callback) => { + this.temptmp = temptmp.createTrackedSession('qwkuserexp'); + this.temptmp.mkdir({ prefix : 'enigqwkwriter-'}, (err, tempDir) => { + if (err) { + return callback(err); + } + + this.tempPacketDir = tempDir; + + const sysTempDownloadDir = FileArea.getAreaDefaultStorageDirectory(this.sysTempDownloadArea); + + // ensure dir exists + fse.mkdirs(sysTempDownloadDir, err => { + return callback(err, sysTempDownloadDir); + }); + }); + }, + (sysTempDownloadDir, callback) => { + this._performExport(sysTempDownloadDir, err => { + return callback(err); + }); + }, + ], + err => { + this.temptmp.cleanup(); + + if (err) { + // :TODO: doesn't do anything currently: + if ('NORESULTS' === err.reasonCode) { + return this.gotoMenu(this.menuConfig.config.noResultsMenu || 'qwkExportNoResults'); + } + + return this.prevMenu(); + } + return cb(err); + } + ); + }); + } + + finishedLoading() { + this.prevMenu(); + } + + _getUserQWKExportOptions() { + let qwkOptions = this.client.user.getProperty(UserProperties.ExportOptions); + try { + qwkOptions = JSON.parse(qwkOptions); + } catch(e) { + qwkOptions = { + enableQWKE : true, + enableHeadersExtension : true, + enableAtKludges : true, + archiveFormat : 'application/zip', + }; + } + return qwkOptions; + } + + _getUserQWKExportAreas() { + let qwkExportAreas = this.client.user.getProperty(UserProperties.ExportAreas); + try { + qwkExportAreas = JSON.parse(qwkExportAreas).map(exportArea => { + if (exportArea.newerThanTimestamp) { + exportArea.newerThanTimestamp = moment(exportArea.newerThanTimestamp); + } + return exportArea; + }); + } catch(e) { + // default to all public and private without 'since' + qwkExportAreas = getAllAvailableMessageAreaTags(this.client).map(areaTag => { + return { areaTag }; + }); + + // Include user's private area + qwkExportAreas.push({ + areaTag : Message.WellKnownAreaTags.Private, + }); + } + + return qwkExportAreas; + } + + _performExport(sysTempDownloadDir, cb) { + const statusView = this.viewControllers.main.getView(MciViewIds.main.status); + const updateStatus = (status) => { + if (statusView) { + statusView.setText(status); + } + }; + + const progBarView = this.viewControllers.main.getView(MciViewIds.main.progressBar); + const updateProgressBar = (curr, total) => { + if (progBarView) { + const prog = Math.floor( (curr / total) * progBarView.dimens.width ); + progBarView.setText(this.config.progBarChar.repeat(prog)); + } + }; + + let cancel = false; + + let lastProgUpdate = 0; + const progressHandler = (state, next) => { + // we can produce a TON of updates; only update progress at most every 3/4s + if (Date.now() - lastProgUpdate > 750) { + switch (state.step) { + case 'next_area' : + updateStatus(state.status); + updateProgressBar(0, 0); + this.updateCustomViewTextsWithFilter('main', MciViewIds.main.customRangeStart, state); + break; + + case 'message' : + updateStatus(state.status); + updateProgressBar(state.current, state.total); + this.updateCustomViewTextsWithFilter('main', MciViewIds.main.customRangeStart, state); + break; + + default : + break; + } + lastProgUpdate = Date.now(); + } + + return next(cancel ? Errors.UserInterrupt('User canceled') : null); + }; + + const keyPressHandler = (ch, key) => { + if('escape' === key.name) { + cancel = true; + this.client.removeListener('key press', keyPressHandler); + } + }; + + let totalExported = 0; + const processMessagesWithFilter = (filter, cb) => { + Message.findMessages(filter, (err, messageIds) => { + if (err) { + return cb(err); + } + + let current = 1; + async.eachSeries(messageIds, (messageId, nextMessageId) => { + const message = new Message(); + message.load({ messageId }, err => { + if (err) { + return nextMessageId(err); + } + + const progress = { + message, + step : 'message', + total : ++totalExported, + areaCurrent : current, + areaCount : messageIds.length, + status : `${_.truncate(message.subject, { length : 25 })} (${current} / ${messageIds.length})`, + }; + + progressHandler(progress, err => { + if (err) { + return nextMessageId(err); + } + + packetWriter.appendMessage(message); + current += 1; + + return nextMessageId(null); + }); + }); + }, + err => { + return cb(err); + }); + }); + }; + + const packetWriter = new QWKPacketWriter( + Object.assign(this._getUserQWKExportOptions(), { + user : this.client.user, + bbsID : this.config.bbsID, + }) + ); + + packetWriter.on('warning', warning => { + this.client.log.warn( { warning }, 'QWK packet writer warning'); + }); + + async.waterfall( + [ + (callback) => { + // don't count idle monitor while processing + this.client.stopIdleMonitor(); + + // let user cancel + this.client.on('key press', keyPressHandler); + + packetWriter.once('ready', () => { + return callback(null); + }); + + packetWriter.once('error', err => { + this.client.log.error( { error : err.message }, 'QWK packet writer error'); + cancel = true; + }); + + packetWriter.init(); + }, + (callback) => { + // For each public area -> for each message + const userExportAreas = this._getUserQWKExportAreas(); + + const publicExportAreas = userExportAreas + .filter(exportArea => { + return exportArea.areaTag !== Message.WellKnownAreaTags.Private; + }); + async.eachSeries(publicExportAreas, (exportArea, nextExportArea) => { + const area = getMessageAreaByTag(exportArea.areaTag); + const conf = getMessageConferenceByTag(area.confTag); + if (!area || !conf) { + // :TODO: remove from user properties - this area does not exist + this.client.log.warn({ areaTag : exportArea.areaTag }, 'Cannot QWK export area as it does not exist'); + return nextExportArea(null); + } + + if (!hasMessageConfAndAreaRead(this.client, area)) { + this.client.log.warn({ areaTag : area.areaTag }, 'Cannot QWK export area due to ACS'); + return nextExportArea(null); + } + + const progress = { + conf, + area, + step : 'next_area', + status : `Gathering in ${conf.name} - ${area.name}...`, + }; + + progressHandler(progress, err => { + if (err) { + return nextExportArea(err); + } + + const filter = { + resultType : 'id', + areaTag : exportArea.areaTag, + newerThanTimestamp : exportArea.newerThanTimestamp + }; + + processMessagesWithFilter(filter, err => { + return nextExportArea(err); + }); + }); + }, + err => { + return callback(err, userExportAreas); + }); + }, + (userExportAreas, callback) => { + // Private messages to current user if the user has + // elected to export private messages + const privateExportArea = userExportAreas.find(exportArea => exportArea.areaTag === Message.WellKnownAreaTags.Private); + if (!privateExportArea) { + return callback(null); + } + + const filter = { + resultType : 'id', + privateTagUserId : this.client.user.userId, + newerThanTimestamp : privateExportArea.newerThanTimestamp, + }; + return processMessagesWithFilter(filter, callback); + }, + (callback) => { + let packetInfo; + packetWriter.once('packet', info => { + packetInfo = info; + }); + + packetWriter.once('finished', () => { + return callback(null, packetInfo); + }); + + packetWriter.finish(this.tempPacketDir); + }, + (packetInfo, callback) => { + if (0 === totalExported) { + return callback(Errors.NothingToDo('No messages exported')); + } + + const sysDownloadPath = paths.join(sysTempDownloadDir, this.tempName); + fse.move(packetInfo.path, sysDownloadPath, err => { + return callback(null, sysDownloadPath, packetInfo); + }); + }, + (sysDownloadPath, packetInfo, callback) => { + const newEntry = new FileEntry({ + areaTag : this.sysTempDownloadArea.areaTag, + fileName : paths.basename(sysDownloadPath), + storageTag : this.sysTempDownloadArea.storageTags[0], + meta : { + upload_by_username : this.client.user.username, + upload_by_user_id : this.client.user.userId, + byte_size : packetInfo.stats.size, + session_temp_dl : 1, // download is valid until session is over + + // :TODO: something like this: allow to override the displayed/downloaded as filename + // separate from the actual on disk filename. E.g. we could always download as "ENIGMA.QWK" + //visible_filename : paths.basename(packetInfo.path), + } + }); + + newEntry.desc = 'QWK Export'; + + newEntry.persist(err => { + if(!err) { + // queue it! + DownloadQueue.get(this.client).addTemporaryDownload(newEntry); + } + return callback(err); + }); + }, + (callback) => { + // update user's export area dates; they can always change/reset them again + const updatedUserExportAreas = this._getUserQWKExportAreas().map(exportArea => { + return Object.assign(exportArea, { + newerThanTimestamp : getISOTimestampString(), + }); + }); + + return this.client.user.persistProperty( + UserProperties.ExportAreas, + JSON.stringify(updatedUserExportAreas), + callback + ); + }, + ], + err => { + this.client.startIdleMonitor(); // re-enable + this.client.removeListener('key press', keyPressHandler); + + if (!err) { + updateStatus('A QWK packet has been placed in your download queue'); + } else if (err.code === Errors.NothingToDo().code) { + updateStatus('No messages to export with current criteria'); + err = null; + } + + return cb(err); + } + ); + } +}; \ No newline at end of file diff --git a/core/mime_util.js b/core/mime_util.js index 857b967c..ddf44432 100644 --- a/core/mime_util.js +++ b/core/mime_util.js @@ -35,7 +35,7 @@ function startup(cb) { function resolveMimeType(query) { if(mimeTypes.extensions[query]) { - return query; // alreaed a mime-type + return query; // already a mime-type } return mimeTypes.lookup(query) || undefined; // lookup() returns false; we want undefined diff --git a/core/msg_area_post_fse.js b/core/msg_area_post_fse.js index f47717b2..0c92bbfb 100644 --- a/core/msg_area_post_fse.js +++ b/core/msg_area_post_fse.js @@ -50,7 +50,7 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule { } else { // note: not logging 'from' here as it's part of client.log.xxxx() self.client.log.info( - { to : msg.toUserName, subject : msg.subject, uuid : msg.uuid }, + { to : msg.toUserName, subject : msg.subject, uuid : msg.messageUuid }, 'Message persisted' ); } diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index 67962fc0..308d3581 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -521,7 +521,24 @@ function scanFileAreas() { }); }, function scanAreas(callback) { - fileArea = require('../../core/file_base_area.js'); + fileArea = require('../../core/file_base_area'); + + // Further expand any wildcards + let areaAndStorageInfoExpanded = []; + options.areaAndStorageInfo.forEach(info => { + if (info.areaTag.indexOf('*') > -1) { + const areas = fileArea.getFileAreasByTagWildcardRule(info.areaTag); + areas.forEach(area => { + areaAndStorageInfoExpanded.push(Object.assign({}, info, { + areaTag : area.areaTag, + })); + }); + } else { + areaAndStorageInfoExpanded.push(info); + } + }); + + options.areaAndStorageInfo = areaAndStorageInfoExpanded; async.eachSeries(options.areaAndStorageInfo, (areaAndStorage, nextAreaTag) => { const areaInfo = fileArea.getFileAreaByTag(areaAndStorage.areaTag); diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index abcf61fe..50bc3b5e 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -59,6 +59,15 @@ Actions: group USERNAME [+|-]GROUP Adds (+) or removes (-) user from a group + list [FILTER] List users with optional FILTER. + + Valid filters: + all : All users (default). + disabled : Disabled users. + inactive : Inactive users. + active : Active (regular) users. + locked : Locked users. + info arguments: --security Include security information in output @@ -92,8 +101,12 @@ cat arguments: Actions: scan AREA_TAG[@STORAGE_TAG] Scan specified area - May contain optional GLOB as last parameter. - Example: ./oputil.js fb scan d0pew4r3z *.zip + Tips: + - May contain optional GLOB as last parameter. + Example: ./oputil.js fb scan d0pew4r3z *.zip + + - AREA_TAG may contain simple wildcards. + Example: ./oputil.js fb scan *warez* info CRITERIA Display information about areas and/or files @@ -170,6 +183,11 @@ Actions: import-areas PATH Import areas using FidoNet *.NA or AREAS.BBS file + qwk-dump PATH Dumps a QWK packet to stdout. + qwk-export [AREA_TAGS] PATH Exports one or more configured message area to a QWK + packet in the directory specified by PATH. The QWK + BBS ID will be obtained by the final component of PATH. + import-areas arguments: --conf CONF_TAG Conference tag in which to import areas --network NETWORK Network name/key to associate FTN areas @@ -177,6 +195,13 @@ import-areas arguments: --type TYPE Area import type Valid types are "bbs" and "na". + +qwk-export arguments: + --user USER User in which to export for. Defaults to the SysOp. + --after TIMESTAMP Export only messages with a timestamp later than + TIMESTAMP. + --no-qwke Disable QWKE extensions. + --no-synchronet Disable Synchronet style extensions. ` }; diff --git a/core/oputil/oputil_message_base.js b/core/oputil/oputil_message_base.js index 0f1b5cfb..1790b1e5 100644 --- a/core/oputil/oputil_message_base.js +++ b/core/oputil/oputil_message_base.js @@ -10,17 +10,19 @@ const { initConfigAndDatabases, getAnswers, writeConfig, -} = require('./oputil_common.js'); -const getHelpFor = require('./oputil_help.js').getHelpFor; -const Address = require('../ftn_address.js'); -const Errors = require('../enig_error.js').Errors; +} = require('./oputil_common.js'); + +const getHelpFor = require('./oputil_help.js').getHelpFor; +const Address = require('../ftn_address.js'); +const Errors = require('../enig_error.js').Errors; // deps -const async = require('async'); -const paths = require('path'); -const fs = require('fs'); -const hjson = require('hjson'); -const _ = require('lodash'); +const async = require('async'); +const paths = require('path'); +const fs = require('fs'); +const hjson = require('hjson'); +const _ = require('lodash'); +const moment = require('moment'); exports.handleMessageBaseCommand = handleMessageBaseCommand; @@ -434,6 +436,200 @@ function getImportEntries(importType, importData) { return importEntries; } +function dumpQWKPacket() { + const packetPath = argv._[argv._.length - 1]; + if(argv._.length < 3 || !packetPath || 0 === packetPath.length) { + return printUsageAndSetExitCode(getHelpFor('MessageBase'), ExitCodes.ERROR); + } + + async.waterfall( + [ + (callback) => { + return initConfigAndDatabases(callback); + }, + (callback) => { + const { QWKPacketReader } = require('../qwk_mail_packet'); + const reader = new QWKPacketReader(packetPath); + + reader.on('error', err => { + console.error(`ERROR: ${err.message}`); + return callback(err); + }); + + reader.on('done', () => { + return callback(null); + }); + + reader.on('archive type', archiveType => { + console.info(`-> Archive type: ${archiveType}`); + }); + + reader.on('creator', creator => { + console.info(`-> Creator: ${creator}`); + }); + + reader.on('message', message => { + console.info('--- message ---'); + console.info(`To: ${message.toUserName}`); + console.info(`From: ${message.fromUserName}`); + console.info(`Subject: ${message.subject}`); + console.info(`Message:\r\n${message.message}`); + }); + + reader.read(); + } + ], + err => { + + } + ) +} + +function exportQWKPacket() { + let packetPath = argv._[argv._.length - 1]; + if(argv._.length < 3 || !packetPath || 0 === packetPath.length) { + return printUsageAndSetExitCode(getHelpFor('MessageBase'), ExitCodes.ERROR); + } + + // oputil mb qwk-export TAGS PATH [--user USER] [--after TIMESTAMP] + // [areaTag1,areaTag2,...] PATH --user USER --after TIMESTAMP + let bbsID = 'ENIGMA'; + const filename = paths.basename(packetPath); + if (filename) { + const ext = paths.extname(filename); + bbsID = paths.basename(filename, ext); + } + + packetPath = paths.dirname(packetPath); + + const posArgLen = argv._.length; + + let areaTags; + if (4 === posArgLen) { + areaTags = argv._[posArgLen - 2].split(','); + } else { + areaTags = []; + } + + let newerThanTimestamp = null; + if (argv.after) { + const ts = moment(argv.after); + if (ts.isValid()) { + newerThanTimestamp = ts.format(); + } + } + + const userName = argv.user || '-'; + + const writerOptions = { + enableQWKE : !(false === argv.qwke), + enableHeadersExtension : !(false === argv.synchronet), + enableAtKludges : !(false === argv.synchronet), + archiveFormat : argv.format || 'application/zip' + }; + + let totalExported = 0; + async.waterfall( + [ + (callback) => { + return initConfigAndDatabases(callback); + }, + (callback) => { + const User = require('../../core/user.js'); + + User.getUserIdAndName(userName, (err, userId) => { + if (err) { + if ('-' === userName) { + userId = 1; + } else { + return callback(err); + } + } + return User.getUser(userId, callback); + }); + }, + (user, callback) => { + // populate area tags with all available to user + // if they were not explicitly supplied + if (!areaTags.length) { + const { + getAllAvailableMessageAreaTags + } = require('../../core/message_area'); + + areaTags = getAllAvailableMessageAreaTags(); + } + return callback(null, user); + }, + (user, callback) => { + const Message = require('../message'); + + const filter = { + resultType : 'id', + areaTag : areaTags, + newerThanTimestamp, + }; + + // public + Message.findMessages(filter, (err, publicMessageIds) => { + if (err) { + return callback(err); + } + + delete filter.areaTag; + filter.privateTagUserId = user.userId; + + Message.findMessages(filter, (err, privateMessageIds) => { + return callback(err, user, Message, privateMessageIds.concat(publicMessageIds)); + }); + }); + }, + (user, Message, messageIds, callback) => { + const { QWKPacketWriter } = require('../qwk_mail_packet'); + const writer = new QWKPacketWriter(Object.assign(writerOptions, { + bbsID, + user, + })); + + writer.on('ready', () => { + async.eachSeries(messageIds, (messageId, nextMessageId) => { + const message = new Message(); + message.load( { messageId }, err => { + if (!err) { + writer.appendMessage(message); + ++totalExported; + } + return nextMessageId(err); + }); + }, + (err) => { + writer.finish(packetPath); + if (err) { + console.error(`Failed to write one or more messages: ${err.message}`); + } + }); + }); + + writer.on('warning', err => { + console.warn(`!!! ${err.reason ? err.reason : err.message}`); + }); + + writer.on('finished', () => { + return callback(null); + }); + + writer.init(); + } + ], + err => { + if(err) { + return console.error(err.reason ? err.reason : err.message); + } + + console.info(`-> Exported ${totalExported} messages`); + } + ); +} + function handleMessageBaseCommand() { function errUsage() { @@ -452,5 +648,7 @@ function handleMessageBaseCommand() { return({ areafix : areaFix, 'import-areas' : importAreas, + 'qwk-dump' : dumpQWKPacket, + 'qwk-export' : exportQWKPacket, }[action] || errUsage)(); } \ No newline at end of file diff --git a/core/oputil/oputil_user.js b/core/oputil/oputil_user.js index 78ea08ca..07f9227a 100644 --- a/core/oputil/oputil_user.js +++ b/core/oputil/oputil_user.js @@ -460,6 +460,66 @@ function twoFactorAuthOTP(user) { ); } +function listUsers() { + // oputil user list [disabled|inactive|active|locked|all] + // :TODO: --created-since SPEC and --last-called SPEC + // --created-since SPEC + // SPEC can be TIMESTAMP or e.g. "-1hour" or "-90days" + // :TODO: --sort name|id + let listWhat; + if (argv._.length > 2) { + listWhat = argv._[argv._.length - 1]; + } else { + listWhat = 'all'; + } + + const User = require('../../core/user'); + if (![ 'all' ].concat(Object.keys(User.AccountStatus)).includes(listWhat)) { + return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); + } + + async.waterfall( + [ + (callback) => { + const UserProps = require('../../core/user_property'); + + const userListOpts = { + properties : [ + UserProps.AccountStatus, + ], + }; + + User.getUserList(userListOpts, (err, userList) => { + if (err) { + return callback(err); + } + + if ('all' === listWhat) { + return callback(null, userList); + } + + const accountStatusFilter = User.AccountStatus[listWhat].toString(); + + return callback(null, userList.filter(user => { + return user[UserProps.AccountStatus] === accountStatusFilter; + })); + }); + }, + (userList, callback) => { + userList.forEach(user => { + + console.info(`${user.userId}: ${user.userName}`); + }); + }, + ], + err => { + if(err) { + return console.error(err.reason ? err.reason : err.message); + } + } + ); +} + function handleUserCommand() { function errUsage() { return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); @@ -470,20 +530,25 @@ function handleUserCommand() { } const action = argv._[1]; - const usernameIdx = [ - 'pw', 'pass', 'passwd', 'password', - 'group', - 'mv', 'rename', - '2fa-otp', 'otp' - ].includes(action) ? argv._.length - 2 : argv._.length - 1; - const userName = argv._[usernameIdx]; + const userRequired = ![ 'list' ].includes(action); - if(!userName) { + let userName; + if (userRequired) { + const usernameIdx = [ + 'pw', 'pass', 'passwd', 'password', + 'group', + 'mv', 'rename', + '2fa-otp', 'otp' + ].includes(action) ? argv._.length - 2 : argv._.length - 1; + userName = argv._[usernameIdx]; + } + + if(!userName && userRequired) { return errUsage(); } initAndGetUser(userName, (err, user) => { - if(err) { + if(userName && err) { process.exitCode = ExitCodes.ERROR; return console.error(err.message); } @@ -512,6 +577,7 @@ function handleUserCommand() { '2fa-otp' : twoFactorAuthOTP, otp : twoFactorAuthOTP, + list : listUsers, }[action] || errUsage)(user, action); }); } \ No newline at end of file diff --git a/core/qwk_mail_packet.js b/core/qwk_mail_packet.js new file mode 100644 index 00000000..de42abf8 --- /dev/null +++ b/core/qwk_mail_packet.js @@ -0,0 +1,1538 @@ +const ArchiveUtil = require('./archive_util'); +const { Errors } = require('./enig_error'); +const Message = require('./message'); +const { splitTextAtTerms } = require('./string_util'); +const { + getMessageConfTagByAreaTag, + getMessageAreaByTag, + getMessageConferenceByTag, + getAllAvailableMessageAreaTags, +} = require('./message_area'); +const StatLog = require('./stat_log'); +const Config = require('./config').get; +const SysProps = require('./system_property'); +const UserProps = require('./user_property'); +const { numToMbf32 } = require('./mbf'); +const { getEncodingFromCharacterSetIdentifier } = require('./ftn_util'); + +const { EventEmitter } = require('events'); +const temptmp = require('temptmp'); +const async = require('async'); +const fs = require('graceful-fs'); +const paths = require('path'); +const { Parser } = require('binary-parser'); +const iconv = require('iconv-lite'); +const moment = require('moment'); +const _ = require('lodash'); +const IniConfigParser = require('ini-config-parser'); + +const enigmaVersion = require('../package.json').version; + +// Synchronet smblib TZ to a UTC offset +// see https://github.com/kvadevack/synchronet/blob/master/src/smblib/smbdefs.h +const SMBTZToUTCOffset = { + // US Standard + '40F0' : '-04:00', // Atlantic + '412C' : '-05:00', // Eastern + '4168' : '-06:00', // Central + '41A4' : '-07:00', // Mountain + '41E0' : '-08:00', // Pacific + '421C' : '-09:00', // Yukon + '4258' : '-10:00', // Hawaii/Alaska + '4294' : '-11:00', // Bering + + // US Daylight + 'C0F0' : '-03:00', // Atlantic + 'C12C' : '-04:00', // Eastern + 'C168' : '-05:00', // Central + 'C1A4' : '-06:00', // Mountain + 'C1E0' : '-07:00', // Pacific + 'C21C' : '-08:00', // Yukon + 'C258' : '-09:00', // Hawaii/Alaska + 'C294' : '-10:00', // Bering + + // "Non-Standard" + '2294' : '-11:00', // Midway + '21E0' : '-08:00', // Vancouver + '21A4' : '-07:00', // Edmonton + '2168' : '-06:00', // Winnipeg + '212C' : '-05:00', // Bogota + '20F0' : '-04:00', // Caracas + '20B4' : '-03:00', // Rio de Janeiro + '2078' : '-02:00', // Fernando de Noronha + '203C' : '-01:00', // Azores + '1000' : '+00:00', // London + '103C' : '+01:00', // Berlin + '1078' : '+02:00', // Athens + '10B4' : '+03:00', // Moscow + '10F0' : '+04:00', // Dubai + '110E' : '+04:30', // Kabul + '112C' : '+05:00', // Karachi + '114A' : '+05:30', // Bombay + '1159' : '+05:45', // Kathmandu + '1168' : '+06:00', // Dhaka + '11A4' : '+07:00', // Bangkok + '11E0' : '+08:00', // Hong Kong + '121C' : '+09:00', // Tokyo + '1258' : '+10:00', // Sydney + '1294' : '+11:00', // Noumea + '12D0' : '+12:00', // Wellington +}; + +const UTCOffsetToSMBTZ = _.invert(SMBTZToUTCOffset); + +const QWKMessageBlockSize = 128; +const QWKHeaderTimestampFormat = 'MM-DD-YYHH:mm'; +const QWKLF = 0xe3; + +const QWKMessageStatusCodes = { + UnreadPublic : ' ', + ReadPublic : '-', + UnreadPrivate : '+', + ReadPrivate : '*', + UnreadCommentToSysOp : '~', + ReadCommentToSysOp : '`', + UnreadSenderPWProtected : '%', + ReadSenderPWProtected : '^', + UnreadGroupPWProtected : '!', + ReadGroupPWProtected : '#', + PWProtectedToAll : '$', + Vote : 'V', +}; + +const QWKMessageActiveStatus = { + Active : 255, + Deleted : 226, +}; + +const QWKNetworkTagIndicator = { + Present : '*', + NotPresent : ' ', +}; + +// See the following: +// - http://fileformats.archiveteam.org/wiki/QWK +// - http://wiki.synchro.net/ref:qwk +// +const MessageHeaderParser = new Parser() + .endianess('little') + .string('status', { + encoding : 'ascii', + length : 1, + }) + .string('num', { // message num or conf num for REP's + encoding : 'ascii', + length : 7, + formatter : n => { + return parseInt(n); + } + }) + .string('timestamp', { + encoding : 'ascii', + length : 13, + }) + // these fields may be encoded in something other than ascii/CP437 + .array('toName', { + type : 'uint8', + length : 25, + }) + .array('fromName', { + type : 'uint8', + length : 25, + }) + .array('subject', { + type : 'uint8', + length : 25, + }) + .string('password', { + encoding : 'ascii', + length : 12, + }) + .string('replyToNum', { + encoding : 'ascii', + length : 8, + formatter : n => { + return parseInt(n); + } + }) + .string('numBlocks', { + encoding : 'ascii', + length : 6, + formatter : n => { + return parseInt(n); + } + }) + .uint8('status2') + .uint16('confNum') + .uint16('relNum') + .uint8('netTag'); + +const replaceCharInBuffer = (buffer, search, replace) => { + let i = 0; + search = Buffer.from([search]); + while (i < buffer.length) { + i = buffer.indexOf(search, i); + if (-1 === i) { + break; + } + buffer[i] = replace; + ++i; + } +} + +class QWKPacketReader extends EventEmitter { + constructor( + packetPath, + { mode = QWKPacketReader.Modes.Guess, keepTearAndOrigin = true } = { mode : QWKPacketReader.Modes.Guess, keepTearAndOrigin : true }) + { + super(); + + this.packetPath = packetPath; + this.options = { mode, keepTearAndOrigin }; + this.temptmp = temptmp.createTrackedSession('qwkpacketreader'); + } + + static get Modes() { + return { + Guess : 'guess', // try to guess + QWK : 'qwk', // standard incoming packet + REP : 'rep', // a reply packet + }; + } + + read() { + // + // A general overview: + // + // - Find out what kind of archive we're dealing with + // - Extract to temporary location + // - Process various files + // - Emit messages we find, information about the packet, so on + // + async.waterfall( + [ + // determine packet archive type + (callback) => { + const archiveUtil = ArchiveUtil.getInstance(); + archiveUtil.detectType(this.packetPath, (err, archiveType) => { + if (err) { + return callback(err); + } + this.emit('archive type', archiveType); + return callback(null, archiveType); + }); + }, + // create a temporary location to do processing + (archiveType, callback) => { + this.temptmp.mkdir( { prefix : 'enigqwkreader-'}, (err, tempDir) => { + if (err) { + return callback(err); + } + + return callback(null, archiveType, tempDir); + }); + }, + // extract it + (archiveType, tempDir, callback) => { + const archiveUtil = ArchiveUtil.getInstance(); + archiveUtil.extractTo(this.packetPath, tempDir, archiveType, err => { + if (err) { + return callback(err); + } + + return callback(null, tempDir); + }); + }, + // gather extracted file list + (tempDir, callback) => { + fs.readdir(tempDir, (err, files) => { + if (err) { + return callback(err); + } + + // Discover basic information about well known files + async.reduce( + files, + {}, + (out, filename, next) => { + const key = filename.toUpperCase(); + + switch (key) { + case 'MESSAGES.DAT' : // QWK + if (this.options.mode === QWKPacketReader.Modes.Guess) { + this.options.mode = QWKPacketReader.Modes.QWK; + } + if (this.options.mode === QWKPacketReader.Modes.QWK) { + out.messages = { filename }; + } + break; + + case 'ID.MSG' : + if (this.options.mode === QWKPacketReader.Modes.Guess) { + this.options.mode = Modes.REP; + } + + if (this.options.mode === QWKPacketReader.Modes.REP) { + out.messages = { filename }; + } + break; + + case 'HEADERS.DAT' : // Synchronet + out.headers = { filename }; + break; + + case 'VOTING.DAT' : // Synchronet + out.voting = { filename }; + break; + + case 'CONTROL.DAT' : // QWK + out.control = { filename }; + break; + + case 'DOOR.ID' : // QWK + out.door = { filename }; + break; + + case 'NETFLAGS.DAT' : // QWK + out.netflags = { filename }; + break; + + case 'NEWFILES.DAT' : // QWK + out.newfiles = { filename }; + break; + + case 'PERSONAL.NDX' : // QWK + out.personal = { filename }; + break; + + case '000.NDX' : // QWK + out.inbox = { filename }; + break; + + case 'TOREADER.EXT' : // QWKE + out.toreader = { filename }; + break; + + case 'QLR.DAT' : + out.qlr = { filename }; + break; + + default : + if (/[0-9]+\.NDX/.test(key)) { // QWK + out.pointers = out.pointers || { filenames: [] }; + out.pointers.filenames.push(filename); + } else { + out[key] = { filename }; + } + break; + } + + return next(null, out); + }, + (err, packetFileInfo) => { + this.packetInfo = Object.assign( + {}, + packetFileInfo, + { + tempDir, + } + ); + return callback(null); + } + ); + }); + }, + (callback) => { + return this.processPacketFiles(callback); + }, + ], + err => { + this.temptmp.cleanup(); + + if (err) { + return this.emit('error', err); + } + + this.emit('done'); + } + ); + } + + processPacketFiles(cb) { + async.series( + [ + (callback) => { + return this.readControl(callback); + }, + (callback) => { + return this.readHeadersExtension(callback); + }, + (callback) => { + return this.readMessages(callback); + } + ], + err => { + return cb(err); + } + ) + } + + readControl(cb) { + // + // CONTROL.DAT is a CRLF text file containing information about + // the originating BBS, conf number <> name mapping, etc. + // + // References: + // - http://fileformats.archiveteam.org/wiki/QWK + // + if (!this.packetInfo.control) { + return cb(Errors.DoesNotExist('No control file found within QWK packet')); + } + + const path = paths.join(this.packetInfo.tempDir, this.packetInfo.control.filename); + + // note that we read as UTF-8. Legacy says it should be CP437/ASCII + // but this seems safer for now so conference names and the like + // can be non-English for example. + fs.readFile(path, { encoding : 'utf8' }, (err, controlLines) => { + if (err) { + return cb(err); + } + + controlLines = splitTextAtTerms(controlLines); + + let state = 'header'; + const control = { confMap : {} }; + let currConfNumber; + for (let lineNumber = 0; lineNumber < controlLines.length; ++lineNumber) { + const line = controlLines[lineNumber].trim(); + switch (lineNumber) { + // first set of lines is header info + case 0 : control.bbsName = line; break; + case 1 : control.bbsLocation = line; break; + case 2 : control.bbsPhone = line; break; + case 3 : control.bbsSysOp = line; break; + case 4 : control.doorRegAndBoardID = line; break; + case 5 : control.packetCreationTime = line; break; + case 6 : control.toUser = line; break; + case 7 : break; // Qmail menu + case 8 : break; // unknown, always 0? + case 9 : break; // total messages in packet (often set to 0) + case 10 : + control.totalMessages = (parseInt(line) + 1); + state = 'confNumber'; + break; + + default : + switch (state) { + case 'confNumber' : + currConfNumber = parseInt(line); + if (isNaN(currConfNumber)) { + state = 'news'; + + control.welcomeFile = line; + } else { + state = 'confName'; + } + break; + + case 'confName' : + control.confMap[currConfNumber] = line; + state = 'confNumber'; + break; + + case 'news' : + control.newsFile = line; + state = 'logoff'; + break; + + case 'logoff' : + control.logoffFile = line; + state = 'footer'; + break; + + case 'footer' : + // some systems append additional info; we don't care. + break; + } + } + } + + return cb(null); + }); + } + + readHeadersExtension(cb) { + if (!this.packetInfo.headers) { + return cb(null); // nothing to do + } + + const path = paths.join(this.packetInfo.tempDir, this.packetInfo.headers.filename); + fs.readFile(path, { encoding : 'utf8' }, (err, iniData) => { + if (err) { + this.emit('warning', Errors.Invalid(`Problem reading HEADERS.DAT: ${err.message}`)); + return cb(null); // non-fatal + } + + try { + const parserOptions = { + lineComment : false, // no line comments; consume full lines + nativeType : false, // just keep everything as strings + dotKey : false, // 'a.b.c = value' stays 'a.b.c = value' + }; + this.packetInfo.headers.ini = IniConfigParser.parse(iniData, parserOptions); + } catch (e) { + this.emit('warning', Errors.Invalid(`HEADERS.DAT file appears to be invalid: ${e.message}`)); + } + + return cb(null); + }); + } + + readMessages(cb) { + if (!this.packetInfo.messages) { + return cb(Errors.DoesNotExist('No messages file found within QWK packet')); + } + + const encodingToSpec = 'cp437'; + let encoding; + + const path = paths.join(this.packetInfo.tempDir, this.packetInfo.messages.filename); + fs.open(path, 'r', (err, fd) => { + if (err) { + return cb(err); + } + + // Some mappings/etc. used in loops below.... + // Sync sets these in HEADERS.DAT: http://wiki.synchro.net/ref:qwk + const FTNPropertyMapping = { + 'X-FTN-AREA' : Message.FtnPropertyNames.FtnArea, + 'X-FTN-SEEN-BY' : Message.FtnPropertyNames.FtnSeenBy, + }; + + const FTNKludgeMapping = { + 'X-FTN-PATH' : 'PATH', + 'X-FTN-MSGID' : 'MSGID', + 'X-FTN-REPLY' : 'REPLY', + 'X-FTN-PID' : 'PID', + 'X-FTN-FLAGS' : 'FLAGS', + 'X-FTN-TID' : 'TID', + 'X-FTN-CHRS' : 'CHRS', + // :TODO: X-FTN-KLUDGE - not sure what this is? + }; + + // + // Various kludge tags defined by QWKE, etc. + // See the following: + // - ftp://vert.synchro.net/main/BBS/qwke.txt + // - http://wiki.synchro.net/ref:qwk + // + const Kludges = { + // QWKE + To : 'To:', + From : 'From:', + Subject : 'Subject:', + + // Synchronet + Via : '@VIA:', + MsgID : '@MSGID:', + Reply : '@REPLY:', + TZ : '@TZ:', // https://github.com/kvadevack/synchronet/blob/master/src/smblib/smbdefs.h + ReplyTo : '@REPLYTO:', + + // :TODO: Look into other non-standards + // https://github.com/wmcbrine/MultiMail/blob/master/mmail/qwk.cc + // title, @subject, etc. + }; + + let blockCount = 0; + let currMessage = { }; + let state; + let messageBlocksRemain; + const buffer = Buffer.alloc(QWKMessageBlockSize); + + const readNextBlock = () => { + fs.read(fd, buffer, 0, QWKMessageBlockSize, null, (err, read) => { + if (err) { + return cb(err); + } + + if (0 == read) { + // we're done consuming all blocks + return fs.close(fd, err => { + return cb(err); + }); + } + + if (QWKMessageBlockSize !== read) { + return cb(Errors.Invalid(`Invalid QWK message block size. Expected ${QWKMessageBlockSize} got ${read}`)); + } + + if (0 === blockCount) { + // first 128 bytes is a space padded ID + const id = buffer.toString('ascii').trim(); + this.emit('creator', id); + state = 'header'; + } else { + switch (state) { + case 'header' : + const header = MessageHeaderParser.parse(buffer); + encoding = encodingToSpec; // reset per message + + // massage into something a little more sane (things we can't quite do in the parser directly) + ['toName', 'fromName', 'subject'].forEach(field => { + // note: always use to-spec encoding here + header[field] = iconv.decode(header[field], encodingToSpec).trim(); + }); + + header.timestamp = moment(header.timestamp, QWKHeaderTimestampFormat); + + currMessage = { + header, + // these may be overridden + toName : header.toName, + fromName : header.fromName, + subject : header.subject, + }; + + if (_.has(this.packetInfo, 'headers.ini')) { + // Sections for a message in HEADERS.DAT are by current byte offset. + // 128 = first message header = 0x80 = section [80] + const headersSectionId = (blockCount * QWKMessageBlockSize).toString(16); + currMessage.headersExtension = this.packetInfo.headers.ini[headersSectionId]; + } + + // if we have HEADERS.DAT with a 'Utf8' override for this message, + // the overridden to/from/subject/message fields are UTF-8 + if (currMessage.headersExtension && 'true' === currMessage.headersExtension.Utf8.toLowerCase()) { + encoding = 'utf8'; + } + + // remainder of blocks until the end of this message + messageBlocksRemain = header.numBlocks - 1; + state = 'message'; + break; + + case 'message' : + if (!currMessage.body) { + currMessage.body = Buffer.from(buffer); + } else { + currMessage.body = Buffer.concat([currMessage.body, buffer]); + } + messageBlocksRemain -= 1; + + if (0 === messageBlocksRemain) { + // 1:n buffers to make up body. Decode: + // First, replace QWK style line feeds (0xe3) unless the message is UTF-8. + // If the message is UTF-8, we assume it's using standard line feeds. + if (encoding !== 'utf8') { + replaceCharInBuffer(currMessage.body, QWKLF, 0x0a); + } + + // + // Decode the message based on our final message encoding. Split the message + // into lines so we can extract various bits such as QWKE headers, origin, tear + // lines, etc. + // + const messageLines = splitTextAtTerms(iconv.decode(currMessage.body, encoding).trimEnd()); + const bodyLines = []; + + let bodyState = 'kludge'; + + const MessageTrailers = { + // While technically FTN oriented, these can come from any network + // (though we'll be processing a lot of messages that routed through FTN + // at some point) + Origin : /^[ ]{1,2}\* Origin: /, + Tear : /^--- /, + }; + + const qwkKludge = {}; + const ftnProperty = {}; + const ftnKludge = {}; + + messageLines.forEach(line => { + if (0 === line.length) { + return bodyLines.push(''); + } + + switch (bodyState) { + case 'kludge' : + // :TODO: Update these to use the well known consts: + if (line.startsWith(Kludges.To)) { + currMessage.toName = line.substring(Kludges.To.length).trim(); + } else if (line.startsWith(Kludges.From)) { + currMessage.fromName = line.substring(Kludges.From.length).trim(); + } else if (line.startsWith(Kludges.Subject)) { + currMessage.subject = line.substring(Kludges.Subject.length).trim(); + } else if (line.startsWith(Kludges.Via)) { + qwkKludge['@VIA'] = line; + } else if (line.startsWith(Kludges.MsgID)) { + qwkKludge['@MSGID'] = line.substring(Kludges.MsgID.length).trim(); + } else if (line.startsWith(Kludges.Reply)) { + qwkKludge['@REPLY'] = line.substring(Kludges.Reply.length).trim(); + } else if (line.startsWith(Kludges.TZ)) { + qwkKludge['@TZ'] = line.substring(Kludges.TZ.length).trim(); + } else if (line.startsWith(Kludges.ReplyTo)) { + qwkKludge['@REPLYTO'] = line.substring(Kludges.ReplyTo.length).trim(); + } else { + bodyState = 'body'; // past this point and up to any tear/origin/etc., is the real message body + bodyLines.push(line); + } + break; + + case 'body' : + case 'trailers' : + if (MessageTrailers.Origin.test(line)) { + ftnProperty.ftn_origin = line; + bodyState = 'trailers'; + } else if (MessageTrailers.Tear.test(line)) { + ftnProperty.ftn_tear_line = line; + bodyState = 'trailers'; + } else if ('body' === bodyState) { + bodyLines.push(line); + } + } + }); + + let messageTimestamp = currMessage.header.timestamp; + + // HEADERS.DAT support. + let useTZKludge = true; + if (currMessage.headersExtension) { + const ext = currMessage.headersExtension; + + // to and subject can be overridden yet again if entries are present + currMessage.toName = ext.To || currMessage.toName; + currMessage.subject = ext.Subject || currMessage.subject; + currMessage.from = ext.Sender || currMessage.fromName; // why not From? Who the fuck knows. + + // possibly override message ID kludge + qwkKludge['@MSGID'] = ext['Message-ID'] || qwkKludge['@MSGID']; + + // WhenWritten contains a ISO-8601-ish timestamp and a Synchronet/SMB style TZ offset: + // 20180101174837-0600 4168 + // We can use this to get a very slightly better precision on the timestamp (addition of seconds) + // over the headers value. Why not milliseconds? Who the fuck knows. + if (ext.WhenWritten) { + const whenWritten = moment(ext.WhenWritten, 'YYYYMMDDHHmmssZ'); + if (whenWritten.isValid()) { + messageTimestamp = whenWritten; + useTZKludge = false; + } + } + + if (ext.Tags) { + currMessage.hashTags = (ext.Tags).toString().split(' '); + } + + // FTN style properties/kludges represented as X-FTN-XXXX + for (let [extName, propName] of Object.entries(FTNPropertyMapping)) { + const v = ext[extName]; + if (v) { + ftnProperty[propName] = v; + } + } + + for (let [extName, kludgeName] of Object.entries(FTNKludgeMapping)) { + const v = ext[extName]; + if (v) { + ftnKludge[kludgeName] = v; + } + } + } + + const message = new Message({ + toUserName : currMessage.toName, + fromUserName : currMessage.fromName, + subject : currMessage.subject, + modTimestamp : messageTimestamp, + message : bodyLines.join('\n'), + hashTags : currMessage.hashTags, + }); + + // Indicate this message was imported from a QWK packet + message.meta.System[Message.SystemMetaNames.ExternalFlavor] = Message.AddressFlavor.QWK; + + if (!_.isEmpty(qwkKludge)) { + message.meta.QwkKludge = qwkKludge; + } + + if (!_.isEmpty(ftnProperty)) { + message.meta.FtnProperty = ftnProperty; + } + + if (!_.isEmpty(ftnKludge)) { + message.meta.FtnKludge = ftnKludge; + } + + // Add in tear line and origin if requested + if (this.options.keepTearAndOrigin) { + if (ftnProperty.ftn_tear_line) { + message.message += `\r\n${ftnProperty.ftn_tear_line}\r\n`; + } + + if (ftnProperty.ftn_origin) { + message.message += `${ftnProperty.ftn_origin}\r\n`; + } + } + + // Update the timestamp if we have a valid TZ + if (useTZKludge && qwkKludge['@TZ']) { + const tzOffset = SMBTZToUTCOffset[qwkKludge['@TZ']]; + if (tzOffset) { + message.modTimestamp.utcOffset(tzOffset); + } + } + + message.meta.QwkProperty = { + qwk_msg_status : currMessage.header.status, + qwk_in_reply_to_num : currMessage.header.replyToNum, + }; + + if (this.options.mode === QWKPacketReader.Modes.QWK) { + message.meta.QwkProperty.qwk_msg_num = currMessage.header.num; + message.meta.QwkProperty.qwk_conf_num = currMessage.header.confNum; + } else { + // For REP's, prefer the larger field. + message.meta.QwkProperty.qwk_conf_num = currMessage.header.num || currMessage.header.confNum; + } + + // Another quick HEADERS.DAT fix-up + if (currMessage.headersExtension) { + message.meta.QwkProperty.qwk_conf_num = currMessage.headersExtension.Conference || message.meta.QwkProperty.qwk_conf_num; + } + + this.emit('message', message); + state = 'header'; + } + break; + } + } + + ++blockCount; + readNextBlock(); + }); + }; + + // start reading blocks + readNextBlock(); + }); + } +}; + +class QWKPacketWriter extends EventEmitter { + constructor( + { + mode = QWKPacketWriter.Modes.User, + enableQWKE = true, + enableHeadersExtension = true, + enableAtKludges = true, + systemDomain = 'enigma-bbs', + bbsID = 'ENIGMA', + user = null, + archiveFormat = 'application/zip', + forceEncoding = null, + } = QWKPacketWriter.DefaultOptions) + { + super(); + + this.options = { + mode, + enableQWKE, + enableHeadersExtension, + enableAtKludges, + systemDomain, + bbsID, + user, + archiveFormat, + forceEncoding : forceEncoding ? forceEncoding.toLowerCase() : null, + }; + + this.temptmp = temptmp.createTrackedSession('qwkpacketwriter'); + + this.areaTagConfMap = {}; + } + + static get DefaultOptions() { + return { + mode : QWKPacketWriter.Modes.User, + enableQWKE : true, + enableHeadersExtension : true, + enableAtKludges : true, + systemDomain : 'enigma-bbs', + bbsID : 'ENIGMA', + user : null, + archiveFormat :'application/zip', + forceEncoding : null, + }; + } + + static get Modes() { + return { + User : 'user', // creation of a packet for a user (non-network); non-mapped confs allowed + Network : 'network', // creation of a packet for QWK network + }; + } + + init() { + async.series( + [ + (callback) => { + return StatLog.init(callback); + }, + (callback) => { + this.temptmp.mkdir( { prefix : 'enigqwkwriter-'}, (err, workDir) => { + this.workDir = workDir; + return callback(err); + }); + }, + (callback) => { + // + // Prepare areaTag -> conference number mapping: + // - In User mode, areaTags's that are not explicitly configured + // will have their conference number auto-generated. + // - In Network mode areaTags's missing a configuration will not + // be mapped, and thus skipped. + // + const configuredAreas = _.get(Config(), 'messageNetworks.qwk.areas'); + if (configuredAreas) { + Object.keys(configuredAreas).forEach(areaTag => { + const confNumber = configuredAreas[areaTag].conference; + if (confNumber) { + this.areaTagConfMap[areaTag] = confNumber; + } + }); + } + + if (this.options.mode === QWKPacketWriter.Modes.User) { + // All the rest + // Start at 1000 to work around what seems to be a bug with some readers + let confNumber = 1000; + const usedConfNumbers = new Set(Object.values(this.areaTagConfMap)); + getAllAvailableMessageAreaTags().forEach(areaTag => { + if (this.areaTagConfMap[areaTag]) { + return; + } + + while (confNumber < 10001 && usedConfNumbers.has(confNumber)) { + ++confNumber; + } + + // we can go up to 65535 for some things, but NDX files are limited to 9999 + if (confNumber === 10000) { // sanity... + this.emit('warning', Errors.General(`To many conferences`)); + } else { + this.areaTagConfMap[areaTag] = confNumber; + ++confNumber; + } + }); + } + + return callback(null); + }, + (callback) => { + this.messagesStream = fs.createWriteStream(paths.join(this.workDir, 'messages.dat')); + + if (this.options.enableHeadersExtension) { + this.headersDatStream = fs.createWriteStream(paths.join(this.workDir, 'headers.dat')); + } + + // First block is a space padded ID + const id = `Created with ENiGMA 1/2 BBS v${enigmaVersion} Copyright (c) 2015-2020 Bryan Ashby`; + this.messagesStream.write(id.padEnd(QWKMessageBlockSize, ' '), 'ascii'); + this.currentMessageOffset = QWKMessageBlockSize; + + this.totalMessages = 0; + this.areaTagsSeen = new Set(); + this.personalIndex = []; // messages addressed to 'user' + this.inboxIndex = []; // private messages for 'user' + this.publicIndex = new Map(); + + return callback(null); + }, + ], + err => { + if (err) { + return this.emit('error', err); + } + + this.emit('ready'); + } + ) + } + + makeMessageIdentifier(message) { + return `<${message.messageId}.${message.messageUuid}@${this.options.systemDomain}>`; + } + + _encodeWithFallback(s, encoding) { + try { + return iconv.encode(s, encoding); + } catch (e) { + this.emit('warning', Errors.General(`Failed to encode buffer using ${encoding}; Falling back to 'ascii'`)); + return iconv.encode(s, 'ascii'); + } + } + + appendMessage(message) { + // + // Each message has to: + // - Append to MESSAGES.DAT + // - Append to HEADERS.DAT if enabled + // + // If this is a personal (ie: non-network) packet: + // - Produce PERSONAL.NDX + // - Produce 000.NDX with pointers to the users personal "inbox" mail + // - Produce ####.NDX with pointers to the public/conference mail + // - Produce TOREADER.EXT if QWKE support is enabled + // + + let fullMessageBody = ''; + + // Start of body is kludges if enabled + if (this.options.enableQWKE) { + if (message.toUserName.length > 25) { + fullMessageBody += `To: ${message.toUserName}\n`; + } + if (message.fromUserName.length > 25) { + fullMessageBody += `From: ${message.fromUserName}\n`; + } + if (message.subject.length > 25) { + fullMessageBody += `Subject: ${message.subject}\n`; + } + } + + if (this.options.enableAtKludges) { + // Add in original kludges (perhaps in a different order) if + // they were originally imported + if (Message.AddressFlavor.QWK == message.meta.System[Message.SystemMetaNames.ExternalFlavor]) { + if (message.meta.QwkKludge) { + for (let [kludge, value] of Object.entries(message.meta.QwkKludge)) { + fullMessageBody += `${kludge}: ${value}\n`; + }; + } + } else { + fullMessageBody += `@MSGID: ${this.makeMessageIdentifier(message)}\n`; + fullMessageBody += `@TZ: ${UTCOffsetToSMBTZ[moment().format('Z')]}\n`; + // :TODO: REPLY and REPLYTO + } + } + + // Sanitize line feeds (e.g. CRLF -> LF, and possibly -> QWK style below) + splitTextAtTerms(message.message).forEach(line => { + fullMessageBody += `${line}\n`; + }); + + const encoding = this._getEncoding(message); + + const encodedMessage = this._encodeWithFallback(fullMessageBody, encoding); + + // + // QWK spec wants line feeds as 0xe3 for some reason, so we'll have + // to replace the \n's. If we're going against the spec and using UTF-8 + // we can just leave them be. + // + if ('utf8' !== encoding) { + replaceCharInBuffer(encodedMessage, 0x0a, QWKLF); + } + + // Messages must comprise of multiples of 128 bit blocks with the last + // block padded by spaces or nulls (we use nulls) + const fullBlocks = Math.trunc(encodedMessage.length / QWKMessageBlockSize); + const remainBytes = QWKMessageBlockSize - (encodedMessage.length % QWKMessageBlockSize); + const totalBlocks = fullBlocks + 1 + (remainBytes ? 1 : 0); + + // The first block is always a header + if (!this._writeMessageHeader( + message, + totalBlocks + )) + { + // we can't write this message + return; + } + + this.messagesStream.write(encodedMessage); + + if (remainBytes) { + this.messagesStream.write(Buffer.alloc(remainBytes, ' ')); + } + + this._updateIndexTracking(message); + + if (this.options.enableHeadersExtension) { + this._appendHeadersExtensionData(message, encoding); + } + + // next message starts at this block + this.currentMessageOffset += totalBlocks * QWKMessageBlockSize; + + this.totalMessages += 1; + this.areaTagsSeen.add(message.areaTag); + } + + _getEncoding(message) { + if (this.options.forceEncoding) { + return this.options.forceEncoding; + } + + // If the system has stored an explicit encoding, use that. + let encoding = _.get(message.meta, 'System.explicit_encoding'); + if (encoding) { + return encoding; + } + + // If the message is already tagged with a supported encoding + // indicator such as FTN-style CHRS, try to use that. + encoding = _.get(message.meta, 'FtnKludge.CHRS'); + if (encoding) { + // convert from CHRS to something standard + encoding = getEncodingFromCharacterSetIdentifier(encoding); + if (encoding) { + return encoding; + } + } + + // The to-spec default is CP437/ASCII. If it can be encoded as + // such then do so. + if (message.isCP437Encodable()) { + return 'cp437'; + } + + // Something more modern... + return 'utf8'; + } + + _messageAddressedToUser(message) { + if (_.isUndefined(this.cachedCompareNames)) { + if (this.options.user) { + this.cachedCompareNames = [ + this.options.user.username.toLowerCase() + ]; + const realName = this.options.user.getProperty(UserProps.RealName); + if (realName) { + this.cachedCompareNames.push(realName.toLowerCase()); + } + } else { + this.cachedCompareNames = []; + } + }; + + return this.cachedCompareNames.includes(message.toUserName.toLowerCase()); + } + + _updateIndexTracking(message) { + // index points at start of *message* not the header for... reasons? + const index = (this.currentMessageOffset / QWKMessageBlockSize) + 1; + if (message.isPrivate()) { + this.inboxIndex.push(index); + } else { + if (this._messageAddressedToUser(message)) { + // :TODO: add to both indexes??? + this.personalIndex.push(index); + } + + const areaTag = message.areaTag; + if (!this.publicIndex.has(areaTag)) { + this.publicIndex.set(areaTag, [index]); + } else { + this.publicIndex.get(areaTag).push(index); + } + } + } + + appendNewFile() { + + } + + finish(packetDirectory) { + async.series( + [ + (callback) => { + this.messagesStream.on('close', () => { + return callback(null); + }); + this.messagesStream.end(); + }, + (callback) => { + if (!this.headersDatStream) { + return callback(null); + } + this.headersDatStream.on('close', () => { + return callback(null); + }); + this.headersDatStream.end(); + }, + (callback) => { + return this._createControlData(callback); + }, + (callback) => { + return this._createIndexes(callback); + }, + (callback) => { + return this._producePacketArchive(packetDirectory, callback); + } + ], + err => { + this.temptmp.cleanup(); + + if (err) { + return this.emit('error', err); + } + + this.emit('finished'); + } + ) + } + + _getNextAvailPacketFileName(packetDirectory, cb) { + // + // According to http://wiki.synchro.net/ref:qwk filenames should + // start with .QWK -> .QW1 ... .QW9 -> .Q10 ... .Q99 + // + let digits = 0; + async.doWhilst( callback => { + let ext; + if (0 === digits) { + ext = 'QWK'; + } else if (digits < 10) { + ext = `QW${digits}`; + } else if (digits < 100) { + ext = `Q${digits}`; + } else { + return callback(Errors.UnexpectedState(`Unable to choose a valid QWK output filename`)); + } + + ++digits; + + const filename = `${this.options.bbsID}.${ext}`; + fs.stat(paths.join(packetDirectory, filename), (err, stats) => { + if (err && 'ENOENT' === err.code) { + return callback(null, filename); + } else { + return callback(null, null); + } + }); + }, + (filename, callback) => { + return callback(null, filename ? false : true); + }, + (err, filename) => { + return cb(err, filename); + }); + } + + _producePacketArchive(packetDirectory, cb) { + const archiveUtil = ArchiveUtil.getInstance(); + + fs.readdir(this.workDir, (err, files) => { + if (err) { + return cb(err); + } + + this._getNextAvailPacketFileName(packetDirectory, (err, filename) => { + if (err) { + return cb(err); + } + + const packetPath = paths.join(packetDirectory, filename); + archiveUtil.compressTo( + this.options.archiveFormat, + packetPath, + files, + this.workDir, + err => { + fs.stat(packetPath, (err, stats) => { + if (stats) { + this.emit('packet', { stats, path : packetPath } ); + } + return cb(err); + }); + } + ); + }); + }); + } + + _qwkMessageStatus(message) { + // - Public vs Private + // - Look at message pointers for read status + // - If +op is exporting and this message is to +op + // - + // :TODO: this needs addressed - handle unread vs read, +op, etc. + // ....see getNewMessagesInAreaForUser(); Variant with just IDs, or just a way to get first new message ID per area? + + if (message.isPrivate()) { + return QWKMessageStatusCodes.UnreadPrivate; + } + return QWKMessageStatusCodes.UnreadPublic; + } + + _writeMessageHeader(message, totalBlocks) { + const asciiNum = (n, l) => { + if (isNaN(n)) { + return ''; + } + return n.toString().substr(0, l); + }; + + const asciiTotalBlocks = asciiNum(totalBlocks, 6); + if (asciiTotalBlocks.length > 6) { + this.emit('warning', Errors.General('Message too large for packet'), message); + return false; + } + + const conferenceNumber = this._getMessageConferenceNumberByAreaTag(message.areaTag); + if (isNaN(conferenceNumber)) { + this.emit('warning', Errors.MissingConfig(`No QWK conference mapping for areaTag ${message.areaTag}`)); + return false; + } + + const header = Buffer.alloc(QWKMessageBlockSize, ' '); + header.write(this._qwkMessageStatus(message), 0, 1, 'ascii'); + header.write(asciiNum(message.messageId), 1, 'ascii'); + header.write(message.modTimestamp.format(QWKHeaderTimestampFormat), 8, 13, 'ascii'); + header.write(message.toUserName.substr(0, 25), 21, 'ascii'); + header.write(message.fromUserName.substr(0, 25), 46, 'ascii'); + header.write(message.subject.substr(0, 25), 71, 'ascii'); + header.write(' '.repeat(12), 96, 'ascii'); // we don't use the password field + header.write(asciiNum(message.replyToMsgId), 108, 'ascii'); + header.write(asciiTotalBlocks, 116, 'ascii'); + header.writeUInt8(QWKMessageActiveStatus.Active, 122); + header.writeUInt16LE(conferenceNumber, 123); + header.writeUInt16LE(this.totalMessages + 1, 125); + header.write(QWKNetworkTagIndicator.NotPresent, 127, 1, 'ascii'); // :TODO: Present if for network output? + + this.messagesStream.write(header); + + return true; + } + + _getMessageConferenceNumberByAreaTag(areaTag) { + if (Message.isPrivateAreaTag(areaTag)) { + return 0; + } + + return this.areaTagConfMap[areaTag]; + } + + _getExportForUsername() { + return _.get(this.options, 'user.username', 'Any'); + } + + _getExportSysOpUsername() { + return StatLog.getSystemStat(SysProps.SysOpUsername) || 'SysOp'; + } + + _createControlData(cb) { + const areas = Array.from(this.areaTagsSeen).map(areaTag => { + if (Message.isPrivateAreaTag(areaTag)) { + return { + areaTag : Message.WellKnownAreaTags.Private, + name : 'Private', + desc : 'Private Messages', + }; + } + return getMessageAreaByTag(areaTag); + }); + + const controlStream = fs.createWriteStream(paths.join(this.workDir, 'control.dat')); + controlStream.setDefaultEncoding('ascii'); + + controlStream.on('close', () => { + return cb(null); + }); + + controlStream.on('error', err => { + return cb(err); + }); + + const initialControlData = [ + Config().general.boardName, + 'Earth', + 'XXX-XXX-XXX', + `${this._getExportSysOpUsername()}, Sysop`, + `0000,${this.options.bbsID}`, + moment().format('MM-DD-YYYY,HH:mm:ss'), + this._getExportForUsername(), + '', // name of Qmail menu + '0', // uh, OK + this.totalMessages.toString(), + // this next line is total conferences - 1: + // We have areaTag <> conference mapping, so the number should work out + (this.areaTagsSeen.size - 1).toString(), + ]; + + initialControlData.forEach(line => { + controlStream.write(`${line}\r\n`); + }); + + // map areas as conf #\r\nDescription\r\n pairs + areas.forEach(area => { + const conferenceNumber = this._getMessageConferenceNumberByAreaTag(area.areaTag); + const conf = getMessageConferenceByTag(area.confTag); + const desc = `${conf.name} - ${area.name}`; + + controlStream.write(`${conferenceNumber}\r\n`); + controlStream.write(`${desc}\r\n`); + }); + + // :TODO: do we ever care here?! + ['HELLO', 'BBSNEWS', 'GOODBYE'].forEach(trailer => { + controlStream.write(`${trailer}\r\n`); + }); + + controlStream.end(); + } + + _createIndexes(cb) { + const appendIndexData = (stream, offset) => { + const msb = numToMbf32(offset); + stream.write(msb); + + // technically, the conference #, but only as a byte, so pretty much useless + // AND the filename itself is the conference number... dafuq. + stream.write(Buffer.from([0x00])); + }; + + async.series( + [ + (callback) => { + // Create PERSONAL.NDX + if (!this.personalIndex.length) { + return callback(null); + } + + const indexStream = fs.createWriteStream(paths.join(this.workDir, 'personal.ndx')); + this.personalIndex.forEach(offset => appendIndexData(indexStream, offset)); + + indexStream.on('close', err => { + return callback(err); + }); + + indexStream.end(); + }, + (callback) => { + // 000.NDX of private mails + if (!this.inboxIndex.length) { + return callback(null); + } + + const indexStream = fs.createWriteStream(paths.join(this.workDir, '000.ndx')); + this.inboxIndex.forEach(offset => appendIndexData(indexStream, offset)); + + indexStream.on('close', err => { + return callback(err); + }); + + indexStream.end(); + }, + (callback) => { + // ####.NDX + async.eachSeries(this.publicIndex.keys(), (areaTag, nextArea) => { + const offsets = this.publicIndex.get(areaTag); + const conferenceNumber = this._getMessageConferenceNumberByAreaTag(areaTag); + const indexStream = fs.createWriteStream(paths.join(this.workDir, `${conferenceNumber.toString().padStart(4, '0')}.ndx`)); + offsets.forEach(offset => appendIndexData(indexStream, offset)); + + indexStream.on('close', err => { + return nextArea(err); + }); + + indexStream.end(); + }, + err => { + return callback(err); + }); + } + ], + err => { + return cb(err); + } + ); + } + + _makeSynchronetTimestamp(ts) { + const syncTimestamp = ts.format('YYYYMMDDHHmmssZZ'); + const syncTZ = UTCOffsetToSMBTZ[ts.format('Z')] || '0000'; // :TODO: what if we don't have a map? + return `${syncTimestamp} ${syncTZ}`; + } + + _appendHeadersExtensionData(message, encoding) { + const messageData = { + // Synchronet style + Utf8 : ('utf8' === encoding ? 'true' : 'false'), + 'Message-ID' : this.makeMessageIdentifier(message), + + WhenWritten : this._makeSynchronetTimestamp(message.modTimestamp), + // WhenImported : '', // :TODO: only if we have a imported time from another external system? + ExportedFrom : `${this.options.systemID} ${message.areaTag} ${message.messageId}`, + Sender : message.fromUserName, + + // :TODO: if exporting for QWK-Net style/etc. + //SenderNetAddr + + SenderIpAddr : '127.0.0.1', // no sir, that's private. + SenderHostName : this.options.systemDomain, + // :TODO: if exported: + //SenderProtocol + Organization : 'BBS', + + //'Reply-To' : :TODO: "address to direct replies".... ?! + Subject : message.subject, + To : message.toUserName, + //ToNetAddr : :TODO: net addr to?! + + // :TODO: Only set if not imported: + Tags : message.hashTags.join(' '), + + // :TODO: Needs tested with Sync/etc.; Sync wants Conference *numbers* + Conference : message.isPrivate() ? '0' : getMessageConfTagByAreaTag(message.areaTag), + + // ENiGMA Headers + MessageUUID : message.messageUuid, + ModTimestamp : message.modTimestamp.format('YYYY-MM-DDTHH:mm:ss.SSSZ'), + AreaTag : message.areaTag, + }; + + const externalFlavor = message.meta.System[Message.SystemMetaNames.ExternalFlavor]; + if (externalFlavor === Message.AddressFlavor.FTN) { + // Add FTN properties if it came from such an origin + if (message.meta.FtnProperty) { + const ftnProp = message.meta.FtnProperty; + messageData['X-FTN-AREA'] = ftnProp[Message.FtnPropertyNames.FtnArea]; + messageData['X-FTN-SEEN-BY'] = ftnProp[Message.FtnPropertyNames.FtnSeenBy]; + } + + if (message.meta.FtnKludge) { + const ftnKludge = message.meta.FtnKludge; + messageData['X-FTN-PATH'] = ftnKludge.PATH; + messageData['X-FTN-MSGID'] = ftnKludge.MSGID; + messageData['X-FTN-REPLY'] = ftnKludge.REPLY; + messageData['X-FTN-PID'] = ftnKludge.PID; + messageData['X-FTN-FLAGS'] = ftnKludge.FLAGS; + messageData['X-FTN-TID'] = ftnKludge.TID; + messageData['X-FTN-CHRS'] = ftnKludge.CHRS; + } + } else { + messageData.WhenExported = this._makeSynchronetTimestamp(moment()); + messageData.Editor = `ENiGMA 1/2 BBS FSE v${enigmaVersion}`; + } + + this.headersDatStream.write(this._encodeWithFallback(`[${this.currentMessageOffset.toString(16)}]\r\n`, encoding)); + + for (let [name, value] of Object.entries(messageData)) { + if (value) { + this.headersDatStream.write(this._encodeWithFallback(`${name}: ${value}\r\n`, encoding)); + } + } + + this.headersDatStream.write('\r\n'); + } +} + +module.exports = { + QWKPacketReader, + QWKPacketWriter, +} diff --git a/core/sauce.js b/core/sauce.js index 7d5f52fd..40434861 100644 --- a/core/sauce.js +++ b/core/sauce.js @@ -26,6 +26,25 @@ exports.SAUCE_SIZE = SAUCE_SIZE; // const SAUCE_VALID_DATA_TYPES = [0, 1, 2, 3, 4, 5, 6, 7, 8 ]; +const SAUCEParser = new Parser() + .buffer('id', { length : 5 } ) + .buffer('version', { length : 2 } ) + .buffer('title', { length: 35 } ) + .buffer('author', { length : 20 } ) + .buffer('group', { length: 20 } ) + .buffer('date', { length: 8 } ) + .uint32le('fileSize') + .int8('dataType') + .int8('fileType') + .uint16le('tinfo1') + .uint16le('tinfo2') + .uint16le('tinfo3') + .uint16le('tinfo4') + .int8('numComments') + .int8('flags') + // :TODO: does this need to be optional? + .buffer('tinfos', { length: 22 } ); // SAUCE 00.5 + function readSAUCE(data, cb) { if(data.length < SAUCE_SIZE) { return cb(Errors.DoesNotExist('No SAUCE record present')); @@ -33,30 +52,11 @@ function readSAUCE(data, cb) { let sauceRec; try { - sauceRec = new Parser() - .buffer('id', { length : 5 } ) - .buffer('version', { length : 2 } ) - .buffer('title', { length: 35 } ) - .buffer('author', { length : 20 } ) - .buffer('group', { length: 20 } ) - .buffer('date', { length: 8 } ) - .uint32le('fileSize') - .int8('dataType') - .int8('fileType') - .uint16le('tinfo1') - .uint16le('tinfo2') - .uint16le('tinfo3') - .uint16le('tinfo4') - .int8('numComments') - .int8('flags') - // :TODO: does this need to be optional? - .buffer('tinfos', { length: 22 } ) // SAUCE 00.5 - .parse(data.slice(data.length - SAUCE_SIZE)); + sauceRec = SAUCEParser.parse(data.slice(data.length - SAUCE_SIZE)); } catch(e) { return cb(Errors.Invalid('Invalid SAUCE record')); } - if(!SAUCE_ID.equals(sauceRec.id)) { return cb(Errors.DoesNotExist('No SAUCE record present')); } diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 3fec4fc3..99a537ec 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -36,7 +36,7 @@ const assert = require('assert'); const sane = require('sane'); const fse = require('fs-extra'); const iconv = require('iconv-lite'); -const uuidV4 = require('uuid/v4'); +const { v4 : UUIDv4 } = require('uuid'); exports.moduleInfo = { name : 'FTN BSO', @@ -517,8 +517,20 @@ function FTNMessageScanTossModule() { }; - this.hasValidConfiguration = function() { - if(!_.has(this, 'moduleConfig.nodes') || !_.has(Config(), 'messageNetworks.ftn.areas')) { + this.hasValidConfiguration = function({shouldLog = false} = {}) { + const hasNodes = _.has(this, 'moduleConfig.nodes'); + const hasAreas = _.has(Config(), 'messageNetworks.ftn.areas'); + + if(!hasNodes && !hasAreas) { + if (shouldLog) { + Log.warn( + { + 'scannerTossers.ftn_bso.nodes' : hasNodes, + 'messageNetworks.ftn.areas' : hasAreas, + }, + 'Missing one or more required configuration blocks' + ); + } return false; } @@ -1203,7 +1215,7 @@ function FTNMessageScanTossModule() { // if(true === _.get(Config(), [ 'messageNetworks', 'ftn', 'areas', config.localAreaTag, 'allowDupes' ], false)) { // just generate a UUID & therefor always allow for dupes - message.uuid = uuidV4(); + message.messageUuid = UUIDv4(); } return callback(null); @@ -1366,7 +1378,7 @@ function FTNMessageScanTossModule() { } } - message.uuid = Message.createMessageUUID( + message.messageUuid = Message.createMessageUUID( localAreaTag, message.modTimestamp, message.subject, @@ -1386,7 +1398,7 @@ function FTNMessageScanTossModule() { if('SQLITE_CONSTRAINT' === err.code || 'DUPE_MSGID' === err.code) { const msgId = _.has(message.meta, 'FtnKludge.MSGID') ? message.meta.FtnKludge.MSGID : 'N/A'; Log.info( - { area : localAreaTag, subject : message.subject, uuid : message.uuid, MSGID : msgId }, + { area : localAreaTag, subject : message.subject, uuid : message.messageUuid, MSGID : msgId }, 'Not importing non-unique message'); return next(null); @@ -1931,8 +1943,8 @@ function FTNMessageScanTossModule() { `SELECT message_id, message_uuid FROM message m WHERE area_tag = ? AND message_id > ? AND - (SELECT COUNT(message_id) - FROM message_meta + (SELECT COUNT(message_id) + FROM message_meta WHERE message_id = m.message_id AND meta_category = 'System' AND meta_name = 'state_flags0') = 0 ORDER BY message_id;` ; @@ -2012,7 +2024,7 @@ function FTNMessageScanTossModule() { const getNewUuidsSql = `SELECT message_id, message_uuid FROM message m - WHERE area_tag = '${Message.WellKnownAreaTags.Private}' AND message_id > ? AND + WHERE area_tag = '${Message.WellKnownAreaTags.Private}' AND message_id > ? AND (SELECT COUNT(message_id) FROM message_meta WHERE message_id = m.message_id @@ -2023,7 +2035,7 @@ function FTNMessageScanTossModule() { (SELECT COUNT(message_id) FROM message_meta WHERE message_id = m.message_id - AND meta_category = 'System' + AND meta_category = 'System' AND meta_name = '${Message.SystemMetaNames.ExternalFlavor}' AND meta_value = '${Message.AddressFlavor.FTN}' ) = 1 @@ -2151,6 +2163,8 @@ FTNMessageScanTossModule.prototype.processTicFilesInDirectory = function(importD FTNMessageScanTossModule.prototype.startup = function(cb) { Log.info(`${exports.moduleInfo.name} Scanner/Tosser starting up`); + this.hasValidConfiguration({ shouldLog : true }); // just check and log + let importing = false; let self = this; @@ -2287,13 +2301,18 @@ FTNMessageScanTossModule.prototype.shutdown = function(cb) { FTNMessageScanTossModule.prototype.performImport = function(cb) { if(!this.hasValidConfiguration()) { - return cb(new Error('Missing or invalid configuration')); + return cb(Errors.MissingConfig('Invalid or missing configuration')); } const self = this; async.each( [ 'inbound', 'secInbound' ], (inboundType, nextDir) => { - self.importFromDirectory(inboundType, self.moduleConfig.paths[inboundType], () => { + const importDir = self.moduleConfig.paths[inboundType]; + self.importFromDirectory(inboundType, importDir, err => { + if (err) { + Log.trace({ importDir, error : err.message }, 'Cannot perform FTN import for directory'); + } + return nextDir(null); }); }, cb); @@ -2305,7 +2324,7 @@ FTNMessageScanTossModule.prototype.performExport = function(cb) { // and let's find out what messages need exported. // if(!this.hasValidConfiguration()) { - return cb(new Error('Missing or invalid configuration')); + return cb(Errors.MissingConfig('Invalid or missing configuration')); } const self = this; @@ -2313,7 +2332,7 @@ FTNMessageScanTossModule.prototype.performExport = function(cb) { async.eachSeries( [ 'EchoMail', 'NetMail' ], (type, nextType) => { self[`perform${type}Export`]( err => { if(err) { - Log.warn( { error : err.message, type : type }, 'Error(s) during export' ); + Log.warn( { type, error : err.message }, 'Error(s) during export' ); } return nextType(null); // try next, always }); @@ -2330,7 +2349,7 @@ FTNMessageScanTossModule.prototype.record = function(message) { return; } - const info = { uuid : message.uuid, subject : message.subject }; + const info = { uuid : message.messageUuid, subject : message.subject }; function exportLog(err) { if(err) { @@ -2344,7 +2363,7 @@ FTNMessageScanTossModule.prototype.record = function(message) { Object.assign(info, { type : 'NetMail' } ); if(this.exportingStart()) { - this.exportNetMailMessagesToUplinks( [ message.uuid ], err => { + this.exportNetMailMessagesToUplinks( [ message.messageUuid ], err => { this.exportingEnd( () => exportLog(err) ); }); } @@ -2357,7 +2376,7 @@ FTNMessageScanTossModule.prototype.record = function(message) { } if(this.exportingStart()) { - this.exportEchoMailMessagesToUplinks( [ message.uuid ], areaConfig, err => { + this.exportEchoMailMessagesToUplinks( [ message.messageUuid ], areaConfig, err => { this.exportingEnd( () => exportLog(err) ); }); } diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js index 67ca9dc6..34991de7 100644 --- a/core/servers/chat/mrc_multiplexer.js +++ b/core/servers/chat/mrc_multiplexer.js @@ -249,11 +249,10 @@ exports.getModule = class MrcModule extends ServerModule { receiveFromClient(username, message) { try { message = JSON.parse(message); + this.sendToMrcServer(message.from_user, message.from_room, message.to_user, message.to_site, message.to_room, message.body); } catch (e) { Log.debug({ server : 'MRC', user : username, message : message }, 'Dodgy message received from client'); } - - this.sendToMrcServer(message.from_user, message.from_room, message.to_user, message.to_site, message.to_room, message.body); } /** @@ -264,11 +263,11 @@ exports.getModule = class MrcModule extends ServerModule { const line = [ fromUser, this.boardName, - sanitiseRoomName(fromRoom), + sanitiseRoomName(fromRoom || ''), sanitiseName(toUser || ''), sanitiseName(toSite || ''), sanitiseRoomName(toRoom || ''), - sanitiseMessage(messageBody) + sanitiseMessage(messageBody || '') ].join('~') + '~'; // Log.debug({ server : 'MRC', data : line }, 'Sending data'); diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index 1b892e71..240ce06b 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -1,838 +1,238 @@ -/* jslint node: true */ -'use strict'; - // ENiGMA½ -const baseClient = require('../../client.js'); -const Log = require('../../logger.js').log; -const LoginServerModule = require('../../login_server_module.js'); -const Config = require('../../config.js').get; -const EnigAssert = require('../../enigma_assert.js'); -const { stringFromNullTermBuffer } = require('../../string_util.js'); -const { Errors } = require('../../enig_error.js'); +const LoginServerModule = require('../../login_server_module'); +const { Client } = require('../../client'); +const Config = require('../../config').get; +const { log: Log } = require('../../logger'); // deps -const net = require('net'); -const buffers = require('buffers'); -const { Parser } = require('binary-parser'); -const util = require('util'); +const net = require('net'); +const { + TelnetSocket, + TelnetSpec: { Options, Commands } +} = require('telnet-socket'); +const { inherits } = require('util'); const ModuleInfo = exports.moduleInfo = { name : 'Telnet', - desc : 'Telnet Server', + desc : 'Telnet Server v2', author : 'NuSkooler', isSecure : false, - packageName : 'codes.l33t.enigma.telnet.server', + packageName : 'codes.l33t.enigma.telnet.server.v2', }; -exports.TelnetClient = TelnetClient; +class TelnetClient { + constructor(socket) { + Client.apply(this, socket, socket); -// -// Telnet Protocol Resources -// * http://pcmicro.com/netfoss/telnet.html -// * http://mud-dev.wikidot.com/telnet:negotiation -// + this.socket = new TelnetSocket(socket); + this.setInputOutput(this.socket, this.socket); -/* - TODO: - * Various (much lesser used) Telnet command coverage -*/ - -const COMMANDS = { - SE : 240, // End of Sub-Negotation Parameters - NOP : 241, // No Operation - DM : 242, // Data Mark - BRK : 243, // Break - IP : 244, // Interrupt Process - AO : 245, // Abort Output - AYT : 246, // Are You There? - EC : 247, // Erase Character - EL : 248, // Erase Line - GA : 249, // Go Ahead - SB : 250, // Start Sub-Negotiation Parameters - WILL : 251, // - WONT : 252, - DO : 253, - DONT : 254, - IAC : 255, // (Data Byte) -}; - -// -// Resources: -// * http://www.faqs.org/rfcs/rfc1572.html -// -const SB_COMMANDS = { - IS : 0, - SEND : 1, - INFO : 2, -}; - -// -// Telnet Options -// -// Resources -// * http://mars.netanya.ac.il/~unesco/cdrom/booklet/HTML/NETWORKING/node300.html -// * http://www.networksorcery.com/enp/protocol/telnet.htm -// -const OPTIONS = { - TRANSMIT_BINARY : 0, // http://tools.ietf.org/html/rfc856 - ECHO : 1, // http://tools.ietf.org/html/rfc857 - // RECONNECTION : 2 - SUPPRESS_GO_AHEAD : 3, // aka 'SGA': RFC 858 @ http://tools.ietf.org/html/rfc858 - //APPROX_MESSAGE_SIZE : 4 - STATUS : 5, // http://tools.ietf.org/html/rfc859 - TIMING_MARK : 6, // http://tools.ietf.org/html/rfc860 - //RC_TRANS_AND_ECHO : 7, // aka 'RCTE' @ http://www.rfc-base.org/txt/rfc-726.txt - //OUPUT_LINE_WIDTH : 8, - //OUTPUT_PAGE_SIZE : 9, // - //OUTPUT_CARRIAGE_RETURN_DISP : 10, // RFC 652 - //OUTPUT_HORIZ_TABSTOPS : 11, // RFC 653 - //OUTPUT_HORIZ_TAB_DISP : 12, // RFC 654 - //OUTPUT_FORMFEED_DISP : 13, // RFC 655 - //OUTPUT_VERT_TABSTOPS : 14, // RFC 656 - //OUTPUT_VERT_TAB_DISP : 15, // RFC 657 - //OUTPUT_LF_DISP : 16, // RFC 658 - //EXTENDED_ASCII : 17, // RFC 659 - //LOGOUT : 18, // RFC 727 - //BYTE_MACRO : 19, // RFC 753 - //DATA_ENTRY_TERMINAL : 20, // RFC 1043 - //SUPDUP : 21, // RFC 736 - //SUPDUP_OUTPUT : 22, // RFC 749 - SEND_LOCATION : 23, // RFC 779 - TERMINAL_TYPE : 24, // aka 'TTYPE': RFC 1091 @ http://tools.ietf.org/html/rfc1091 - //END_OF_RECORD : 25, // RFC 885 - //TACACS_USER_ID : 26, // RFC 927 - //OUTPUT_MARKING : 27, // RFC 933 - //TERMINCAL_LOCATION_NUMBER : 28, // RFC 946 - //TELNET_3270_REGIME : 29, // RFC 1041 - WINDOW_SIZE : 31, // aka 'NAWS': RFC 1073 @ http://tools.ietf.org/html/rfc1073 - TERMINAL_SPEED : 32, // RFC 1079 @ http://tools.ietf.org/html/rfc1079 - REMOTE_FLOW_CONTROL : 33, // RFC 1072 @ http://tools.ietf.org/html/rfc1372 - LINEMODE : 34, // RFC 1184 @ http://tools.ietf.org/html/rfc1184 - X_DISPLAY_LOCATION : 35, // aka 'XDISPLOC': RFC 1096 @ http://tools.ietf.org/html/rfc1096 - NEW_ENVIRONMENT_DEP : 36, // aka 'NEW-ENVIRON': RFC 1408 @ http://tools.ietf.org/html/rfc1408 (note: RFC 1572 is an update to this) - AUTHENTICATION : 37, // RFC 2941 @ http://tools.ietf.org/html/rfc2941 - ENCRYPT : 38, // RFC 2946 @ http://tools.ietf.org/html/rfc2946 - NEW_ENVIRONMENT : 39, // aka 'NEW-ENVIRON': RFC 1572 @ http://tools.ietf.org/html/rfc1572 (note: update to RFC 1408) - //TN3270E : 40, // RFC 2355 - //XAUTH : 41, - //CHARSET : 42, // RFC 2066 - //REMOTE_SERIAL_PORT : 43, - //COM_PORT_CONTROL : 44, // RFC 2217 - //SUPRESS_LOCAL_ECHO : 45, - //START_TLS : 46, - //KERMIT : 47, // RFC 2840 - //SEND_URL : 48, - //FORWARD_X : 49, - - //PRAGMA_LOGON : 138, - //SSPI_LOGON : 139, - //PRAGMA_HEARTBEAT : 140 - - ARE_YOU_THERE : 246, // aka 'AYT' RFC 854 @ https://tools.ietf.org/html/rfc854 - - EXTENDED_OPTIONS_LIST : 255, // RFC 861 (STD 32) -}; - -// Commands used within NEW_ENVIRONMENT[_DEP] -const NEW_ENVIRONMENT_COMMANDS = { - VAR : 0, - VALUE : 1, - ESC : 2, - USERVAR : 3, -}; - -const IAC_BUF = Buffer.from([ COMMANDS.IAC ]); -const IAC_SE_BUF = Buffer.from([ COMMANDS.IAC, COMMANDS.SE ]); - -const COMMAND_NAMES = Object.keys(COMMANDS).reduce(function(names, name) { - names[COMMANDS[name]] = name.toLowerCase(); - return names; -}, {}); - -const COMMAND_IMPLS = {}; -[ 'do', 'dont', 'will', 'wont', 'sb' ].forEach(function(command) { - const code = COMMANDS[command.toUpperCase()]; - COMMAND_IMPLS[code] = function(bufs, i, event) { - if(bufs.length < (i + 1)) { - return MORE_DATA_REQUIRED; - } - return parseOption(bufs, i, event); - }; -}); - -// :TODO: See TooTallNate's telnet.js: Handle COMMAND_IMPL for IAC in binary mode - -// Create option names such as 'transmit binary' -> OPTIONS.TRANSMIT_BINARY -const OPTION_NAMES = Object.keys(OPTIONS).reduce(function(names, name) { - names[OPTIONS[name]] = name.toLowerCase().replace(/_/g, ' '); - return names; -}, {}); - -function unknownOption(bufs, i, event) { - Log.warn( { bufs : bufs, i : i, event : event }, 'Unknown Telnet option'); - event.buf = bufs.splice(0, i).toBuffer(); - return event; -} - -const OPTION_IMPLS = {}; -// :TODO: fill in the rest... -OPTION_IMPLS.NO_ARGS = -OPTION_IMPLS[OPTIONS.ECHO] = -OPTION_IMPLS[OPTIONS.STATUS] = -OPTION_IMPLS[OPTIONS.LINEMODE] = -OPTION_IMPLS[OPTIONS.TRANSMIT_BINARY] = -OPTION_IMPLS[OPTIONS.AUTHENTICATION] = -OPTION_IMPLS[OPTIONS.TERMINAL_SPEED] = -OPTION_IMPLS[OPTIONS.REMOTE_FLOW_CONTROL] = -OPTION_IMPLS[OPTIONS.X_DISPLAY_LOCATION] = -OPTION_IMPLS[OPTIONS.SEND_LOCATION] = -OPTION_IMPLS[OPTIONS.ARE_YOU_THERE] = -OPTION_IMPLS[OPTIONS.SUPPRESS_GO_AHEAD] = function(bufs, i, event) { - event.buf = bufs.splice(0, i).toBuffer(); - return event; -}; - -OPTION_IMPLS[OPTIONS.TERMINAL_TYPE] = function(bufs, i, event) { - if(event.commandCode !== COMMANDS.SB) { - OPTION_IMPLS.NO_ARGS(bufs, i, event); - } else { - // We need 4 bytes header + data + IAC SE - if(bufs.length < 7) { - return MORE_DATA_REQUIRED; - } - - const end = bufs.indexOf(IAC_SE_BUF, 5); // look past header bytes - if(-1 === end) { - return MORE_DATA_REQUIRED; - } - - let ttypeCmd; - try { - ttypeCmd = new Parser() - .uint8('iac1') - .uint8('sb') - .uint8('opt') - .uint8('is') - .array('ttype', { - type : 'uint8', - readUntil : b => 255 === b, // 255=COMMANDS.IAC - }) - // note we read iac2 above - .uint8('se') - .parse(bufs.toBuffer()); - } catch(e) { - Log.debug( { error : e }, 'Failed parsing TTYP telnet command'); - return event; - } - - EnigAssert(COMMANDS.IAC === ttypeCmd.iac1); - EnigAssert(COMMANDS.SB === ttypeCmd.sb); - EnigAssert(OPTIONS.TERMINAL_TYPE === ttypeCmd.opt); - EnigAssert(SB_COMMANDS.IS === ttypeCmd.is); - EnigAssert(ttypeCmd.ttype.length > 0); - // note we found IAC_SE above - - // some terminals such as NetRunner provide a NULL-terminated buffer - // slice to remove IAC - event.ttype = stringFromNullTermBuffer(ttypeCmd.ttype.slice(0, -1), 'ascii'); - - bufs.splice(0, end); - } - - return event; -}; - -OPTION_IMPLS[OPTIONS.WINDOW_SIZE] = function(bufs, i, event) { - if(event.commandCode !== COMMANDS.SB) { - OPTION_IMPLS.NO_ARGS(bufs, i, event); - } else { - // we need 9 bytes - if(bufs.length < 9) { - return MORE_DATA_REQUIRED; - } - - let nawsCmd; - try { - nawsCmd = new Parser() - .uint8('iac1') - .uint8('sb') - .uint8('opt') - .uint16be('width') - .uint16be('height') - .uint8('iac2') - .uint8('se') - .parse(bufs.splice(0, 9).toBuffer()); - } catch(e) { - Log.debug( { error : e }, 'Failed parsing NAWS telnet command'); - return event; - } - - EnigAssert(COMMANDS.IAC === nawsCmd.iac1); - EnigAssert(COMMANDS.SB === nawsCmd.sb); - EnigAssert(OPTIONS.WINDOW_SIZE === nawsCmd.opt); - EnigAssert(COMMANDS.IAC === nawsCmd.iac2); - EnigAssert(COMMANDS.SE === nawsCmd.se); - - event.cols = event.columns = event.width = nawsCmd.width; - event.rows = event.height = nawsCmd.height; - } - return event; -}; - -// Build an array of delimiters for parsing NEW_ENVIRONMENT[_DEP] -//const NEW_ENVIRONMENT_DELIMITERS = _.values(NEW_ENVIRONMENT_COMMANDS); - -// Handle the deprecated RFC 1408 & the updated RFC 1572: -OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT_DEP] = -OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT] = function(bufs, i, event) { - if(event.commandCode !== COMMANDS.SB) { - OPTION_IMPLS.NO_ARGS(bufs, i, event); - } else { // - // We need 4 bytes header + + IAC SE - // Many terminals send a empty list: - // IAC SB NEW-ENVIRON IS IAC SE + // Wait up to 3s to hear about from our terminal type request + // then go ahead and move on... // - if(bufs.length < 6) { - return MORE_DATA_REQUIRED; - } + setTimeout(() => { + this._clientReady(); + }, 3000); - let end = bufs.indexOf(IAC_SE_BUF, 4); // look past header bytes - if(-1 === end) { - return MORE_DATA_REQUIRED; - } + this.dataHandler = function(data) { + this.emit('data', data); + }.bind(this); - // :TODO: It's likely that we could do all the env name/value parsing directly in Parser. + this.socket.on('data', this.dataHandler); - let envCmd; - try { - envCmd = new Parser() - .uint8('iac1') - .uint8('sb') - .uint8('opt') - .uint8('isOrInfo') // IS=initial, INFO=updates - .array('envBlock', { - type : 'uint8', - readUntil : b => 255 === b, // 255=COMMANDS.IAC - }) - // note we consume IAC above - .uint8('se') - .parse(bufs.splice(0, bufs.length).toBuffer()); - } catch(e) { - Log.debug( { error : e }, 'Failed parsing NEW-ENVIRON telnet command'); - return event; - } + this.socket.on('error', err => { + this._logDebug({ error : err.message }, 'Socket error'); + return this.emit('end'); + }); - EnigAssert(COMMANDS.IAC === envCmd.iac1); - EnigAssert(COMMANDS.SB === envCmd.sb); - EnigAssert(OPTIONS.NEW_ENVIRONMENT === envCmd.opt || OPTIONS.NEW_ENVIRONMENT_DEP === envCmd.opt); - EnigAssert(SB_COMMANDS.IS === envCmd.isOrInfo || SB_COMMANDS.INFO === envCmd.isOrInfo); + this.socket.on('end', () => { + this.emit('end'); + }); - if(OPTIONS.NEW_ENVIRONMENT_DEP === envCmd.opt) { - // :TODO: we should probably support this for legacy clients? - Log.warn('Handling deprecated RFC 1408 NEW-ENVIRON'); - } + this.socket.on('command error', (command, err) => { + this._logDebug({ command, error : err.message }, 'Command error'); + }); - const envBuf = envCmd.envBlock.slice(0, -1); // remove IAC - - if(envBuf.length < 4) { // TYPE + single char name + sep + single char value - // empty env block - return event; - } - - const States = { - Name : 1, - Value : 2, - }; - - let state = States.Name; - const setVars = {}; - const delVars = []; - let varName; - // :TODO: handle ESC type!!! - while(envBuf.length) { - switch(state) { - case States.Name : - { - const type = parseInt(envBuf.splice(0, 1)); - if(![ NEW_ENVIRONMENT_COMMANDS.VAR, NEW_ENVIRONMENT_COMMANDS.USERVAR, NEW_ENVIRONMENT_COMMANDS.ESC ].includes(type)) { - return event; // fail :( - } - - let nameEnd = envBuf.indexOf(NEW_ENVIRONMENT_COMMANDS.VALUE); - if(-1 === nameEnd) { - nameEnd = envBuf.length; - } - - varName = envBuf.splice(0, nameEnd); - if(!varName) { - return event; // something is wrong. - } - - varName = Buffer.from(varName).toString('ascii'); - - const next = parseInt(envBuf.splice(0, 1)); - if(NEW_ENVIRONMENT_COMMANDS.VALUE === next) { - state = States.Value; - } else { - state = States.Name; - delVars.push(varName); // no value; del this var - } - } + this.socket.on('DO', command => { + switch (command.option) { + // We've already stated we WILL do the following via + // the banner - some terminals will ask over and over + // if we respond to a DO with a WILL, so just don't + // do anything... + case Options.SGA : + case Options.ECHO : + case Options.TRANSMIT_BINARY : break; - case States.Value : - { - let valueEnd = envBuf.indexOf(NEW_ENVIRONMENT_COMMANDS.VAR); - if(-1 === valueEnd) { - valueEnd = envBuf.indexOf(NEW_ENVIRONMENT_COMMANDS.USERVAR); - } - if(-1 === valueEnd) { - valueEnd = envBuf.length; - } - - let value = envBuf.splice(0, valueEnd); - if(value) { - value = Buffer.from(value).toString('ascii'); - setVars[varName] = value; - } - state = States.Name; - } - break; - } - } - - // :TODO: Handle deleting previously set vars via delVars - event.type = envCmd.isOrInfo; - event.envVars = setVars; - } - - return event; -}; - -const MORE_DATA_REQUIRED = 0xfeedface; - -function parseBufs(bufs) { - EnigAssert(bufs.length >= 2); - EnigAssert(bufs.get(0) === COMMANDS.IAC); - return parseCommand(bufs, 1, {}); -} - -function parseCommand(bufs, i, event) { - const command = bufs.get(i); // :TODO: fix deprecation... [i] is not the same - event.commandCode = command; - event.command = COMMAND_NAMES[command]; - - const handler = COMMAND_IMPLS[command]; - if(handler) { - return handler(bufs, i + 1, event); - } else { - if(2 !== bufs.length) { - Log.warn( { bufsLength : bufs.length }, 'Expected bufs length of 2'); // expected: IAC + COMMAND - } - - event.buf = bufs.splice(0, 2).toBuffer(); - return event; - } -} - -function parseOption(bufs, i, event) { - const option = bufs.get(i); // :TODO: fix deprecation... [i] is not the same - event.optionCode = option; - event.option = OPTION_NAMES[option]; - - const handler = OPTION_IMPLS[option]; - return handler ? handler(bufs, i + 1, event) : unknownOption(bufs, i + 1, event); -} - - -function TelnetClient(input, output) { - baseClient.Client.apply(this, arguments); - - const self = this; - - let bufs = buffers(); - this.bufs = bufs; - - this.sentDont = {}; // DON'T's we've already sent - - this.setInputOutput(input, output); - - this.negotiationsComplete = false; // are we in the 'negotiation' phase? - this.didReady = false; // have we emit the 'ready' event? - - this.subNegotiationState = { - newEnvironRequested : false, - }; - - this.dataHandler = function(b) { - if(!Buffer.isBuffer(b)) { - EnigAssert(false, `Cannot push non-buffer ${typeof b}`); - return; - } - - bufs.push(b); - - let i; - while((i = bufs.indexOf(IAC_BUF)) >= 0) { - - // - // Some clients will send even IAC separate from data - // - if(bufs.length <= (i + 1)) { - i = MORE_DATA_REQUIRED; - break; - } - - EnigAssert(bufs.length > (i + 1)); - - if(i > 0) { - self.emit('data', bufs.splice(0, i).toBuffer()); - } - - i = parseBufs(bufs); - - if(MORE_DATA_REQUIRED === i) { - break; - } else if(i) { - if(i.option) { - self.emit(i.option, i); // "transmit binary", "echo", ... - } - - self.handleTelnetEvent(i); - - if(i.data) { - self.emit('data', i.data); - } - } - } - - if(MORE_DATA_REQUIRED !== i && bufs.length > 0) { - // - // Standard data payload. This can still be "non-user" data - // such as ANSI control, but we don't handle that here. - // - self.emit('data', bufs.splice(0).toBuffer()); - } - }; - - this.input.on('data', this.dataHandler); - - this.input.on('end', () => { - self.emit('end'); - }); - - this.input.on('error', err => { - this.connectionDebug( { err : err }, 'Socket error' ); - return self.emit('end'); - }); - - this.connectionTrace = (info, msg) => { - if(Config().loginServers.telnet.traceConnections) { - const logger = self.log || Log; - return logger.trace(info, `Telnet: ${msg}`); - } - }; - - this.connectionDebug = (info, msg) => { - const logger = self.log || Log; - return logger.debug(info, `Telnet: ${msg}`); - }; - - this.connectionWarn = (info, msg) => { - const logger = self.log || Log; - return logger.warn(info, `Telnet: ${msg}`); - }; - - this.readyNow = () => { - if(!this.didReady) { - this.didReady = true; - this.emit('ready', { firstMenu : Config().loginServers.telnet.firstMenu } ); - } - }; - - this.disconnect = function() { - try { - return this.output.end.apply(this.output, arguments); - } - catch(e) { - // nothing - } - }; -} - -util.inherits(TelnetClient, baseClient.Client); - -/////////////////////////////////////////////////////////////////////////////// -// Telnet Command/Option handling -/////////////////////////////////////////////////////////////////////////////// -TelnetClient.prototype.handleTelnetEvent = function(evt) { - - if(!evt.command) { - return this.connectionWarn( { evt : evt }, 'No command for event'); - } - - // handler name e.g. 'handleWontCommand' - const handlerName = `handle${evt.command.charAt(0).toUpperCase()}${evt.command.substr(1)}Command`; - - if(this[handlerName]) { - // specialized - this[handlerName](evt); - } else { - // generic-ish - this.handleMiscCommand(evt); - } -}; - -TelnetClient.prototype.handleWillCommand = function(evt) { - if('terminal type' === evt.option) { - // - // See RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html - // - this.requestTerminalType(); - } else if('new environment' === evt.option) { - // - // See RFC 1572 @ http://www.faqs.org/rfcs/rfc1572.html - // - this.requestNewEnvironment(); - } else { - // :TODO: temporary: - this.connectionTrace(evt, 'WILL'); - } -}; - -TelnetClient.prototype.handleWontCommand = function(evt) { - if(this.sentDont[evt.option]) { - return this.connectionTrace(evt, 'WONT - DON\'T already sent'); - } - - this.sentDont[evt.option] = true; - - if('new environment' === evt.option) { - this.dont.new_environment(); - } else { - this.connectionTrace(evt, 'WONT'); - } -}; - -TelnetClient.prototype.handleDoCommand = function(evt) { - // :TODO: handle the rest, e.g. echo nd the like - - if('linemode' === evt.option) { - // - // Client wants to enable linemode editing. Denied. - // - this.wont.linemode(); - } else if('encrypt' === evt.option) { - // - // Client wants to enable encryption. Denied. - // - this.wont.encrypt(); - } else { - // :TODO: temporary: - this.connectionTrace(evt, 'DO'); - } -}; - -TelnetClient.prototype.handleDontCommand = function(evt) { - this.connectionTrace(evt, 'DONT'); -}; - -TelnetClient.prototype.handleSbCommand = function(evt) { - const self = this; - - if('terminal type' === evt.option) { - // - // See RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html - // - // :TODO: According to RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html - // We should keep asking until we see a repeat. From there, determine the best type/etc. - self.setTermType(evt.ttype); - - self.negotiationsComplete = true; // :TODO: throw in a array of what we've taken care. Complete = array satisified or timeout - - self.readyNow(); - } else if('new environment' === evt.option) { - // - // Handling is as follows: - // * Map 'TERM' -> 'termType' and only update if ours is 'unknown' - // * Map COLUMNS -> 'termWidth' and only update if ours is 0 - // * Map ROWS -> 'termHeight' and only update if ours is 0 - // * Add any new variables, ignore any existing - // - Object.keys(evt.envVars || {} ).forEach(function onEnv(name) { - if('TERM' === name && 'unknown' === self.term.termType) { - self.setTermType(evt.envVars[name]); - } else if('COLUMNS' === name && 0 === self.term.termWidth) { - self.term.termWidth = parseInt(evt.envVars[name]); - self.clearMciCache(); // term size changes = invalidate cache - self.connectionDebug({ termWidth : self.term.termWidth, source : 'NEW-ENVIRON'}, 'Window width updated'); - } else if('ROWS' === name && 0 === self.term.termHeight) { - self.term.termHeight = parseInt(evt.envVars[name]); - self.clearMciCache(); // term size changes = invalidate cache - self.connectionDebug({ termHeight : self.term.termHeight, source : 'NEW-ENVIRON'}, 'Window height updated'); - } else { - if(name in self.term.env) { - - EnigAssert( - SB_COMMANDS.INFO === evt.type || SB_COMMANDS.IS === evt.type, - 'Unexpected type: ' + evt.type - ); - - self.connectionWarn( - { varName : name, value : evt.envVars[name], existingValue : self.term.env[name] }, - 'Environment variable already exists' - ); - } else { - self.term.env[name] = evt.envVars[name]; - self.connectionDebug( { varName : name, value : evt.envVars[name] }, 'New environment variable' ); - } + default : + return this.socket.command(Commands.WONT, command.option); } }); - } else if('window size' === evt.option) { - // - // Update termWidth & termHeight. - // Set LINES and COLUMNS environment variables as well. - // - self.term.termWidth = evt.width; - self.term.termHeight = evt.height; + this.socket.on('DONT', command => { + this._logTrace(command, 'DONT'); + }); - if(evt.width > 0) { - self.term.env.COLUMNS = evt.height; + this.socket.on('WILL', command => { + switch (command.option) { + case Options.TTYPE : + return this.socket.sb.send.ttype(); + + case Options.NEW_ENVIRON : + return this.socket.sb.send.new_environ( + [ 'ROWS', 'COLUMNS', 'TERM', 'TERM_PROGRAM' ] + ); + + default : + break; + } + }); + + this.socket.on('WONT', command => { + return this._logTrace(command, 'WONT'); + }); + + this.socket.on('SB', command => { + switch (command.option) { + case Options.TTYPE : + this.setTermType(command.optionData.ttype); + return this._clientReady(); + + case Options.NEW_ENVIRON : + { + this._logDebug( + { vars : command.optionData.vars, userVars : command.optionData.userVars }, + 'New environment received' + ); + + // get a value from vars with fallback of user vars + const getValue = (name) => { + return command.optionData.vars.find(nv => nv.name === name) || + command.optionData.userVars.find(nv => nv.name === name); + }; + + if ('unknown' === this.term.termType) { + // allow from vars or user vars + const term = getValue('TERM') || getValue('TERM_PROGRAM'); + if (term) { + this.setTermType(term.value); + } + } + + if (0 === this.term.termHeight || 0 === this.term.termWidth) { + const updateTermSize = (what) => { + const value = parseInt(getValue(what)); + if (value) { + this.term[what === 'ROWS' ? 'termHeight' : 'termWidth'] = value; + this.clearMciCache(); + this._logDebug( + { [ what ] : value, source : 'NEW-ENVIRON' }, + 'Window size updated' + ); + } + }; + + updateTermSize('ROWS'); + updateTermSize('COLUMNS'); + } + } + break; + + case Options.NAWS : + { + const { width, height } = command.optionData; + + this.term.termWidth = width; + this.term.termHeight = height; + + if (width) { + this.term.env.COLUMNS = width; + } + + if (height) { + this.term.env.ROWS = height; + } + + this.clearMciCache(); + + this._logDebug( + { width, height, source : 'NAWS' }, + 'Windows size updated' + ); + } + break; + + default : + return this._logTrace(command, 'SB'); + } + }); + + this.socket.on('IP', command => { + this._logDebug(command, 'Interrupt Process (IP) - Ending session'); + return this.disconnect(); + }); + + this.socket.on('AYT', () => { + this.socket.write('\b'); + return this._logTrace(command, 'Are You There (AYT) - Replied'); + }); + } + + get dataPassthrough() { + return this.socket.passthrough; + } + + set dataPassthrough(passthrough) { + this.socket.passthrough = passthrough; + } + + disconnect() { + try { + return this.socket.rawSocket.end(); + } catch (e) { + // ignored + } + } + + banner() { + this.socket.dont.echo(); // don't echo characters + this.socket.will.echo(); // ...we'll echo them back + + this.socket.will.sga(); + this.socket.do.sga(); + + this.socket.do.transmit_binary(); + this.socket.will.transmit_binary(); + + this.socket.do.ttype(); + this.socket.do.naws(); + this.socket.do.new_environ(); + } + + _logTrace(info, msg) { + if (Config().loginServers.telnet.traceConnections) { + const log = this.log || Log; + return log.trace(info, `Telnet: ${msg}`); + } + } + + _logDebug(info, msg) { + const log = this.log || Log; + return log.debug(info, `Telnet: ${msg}`); + } + + _clientReady() { + if (this.clientReadyHandled) { + return; // already processed } - if(evt.height > 0) { - self.term.env.ROWS = evt.height; - } - - self.clearMciCache(); // term size changes = invalidate cache - - self.connectionDebug({ termWidth : evt.width , termHeight : evt.height, source : 'NAWS' }, 'Window size updated'); - } else { - self.connectionDebug(evt, 'SB'); + this.clientReadyHandled = true; + this.emit('ready', { firstMenu : Config().loginServers.telnet.firstMenu } ); } }; -const IGNORED_COMMANDS = [ - COMMANDS.EL, COMMANDS.GA, COMMANDS.NOP, COMMANDS.DM, COMMANDS.BRK -]; - - -TelnetClient.prototype.handleMiscCommand = function(evt) { - EnigAssert(evt.command !== 'undefined' && evt.command.length > 0); - - // - // See: - // * RFC 854 @ http://tools.ietf.org/html/rfc854 - // - if('ip' === evt.command) { - // Interrupt Process (IP) - this.log.debug('Interrupt Process (IP) - Ending'); - - this.input.end(); - } else if('ayt' === evt.command) { - this.output.write('\b'); - - this.log.debug('Are You There (AYT) - Replied "\\b"'); - } else if(IGNORED_COMMANDS.indexOf(evt.commandCode)) { - this.log.trace({ command : evt.command, commandCode : evt.commandCode }, 'Ignoring command'); - } else { - this.log.warn({ evt : evt }, 'Unknown command'); - } -}; - -TelnetClient.prototype.requestTerminalType = function() { - const buf = Buffer.from( [ - COMMANDS.IAC, - COMMANDS.SB, - OPTIONS.TERMINAL_TYPE, - SB_COMMANDS.SEND, - COMMANDS.IAC, - COMMANDS.SE ]); - this.output.write(buf); -}; - -const WANTED_ENVIRONMENT_VAR_BUFS = [ - Buffer.from( 'LINES' ), - Buffer.from( 'COLUMNS' ), - Buffer.from( 'TERM' ), - Buffer.from( 'TERM_PROGRAM' ) -]; - -TelnetClient.prototype.requestNewEnvironment = function() { - - if(this.subNegotiationState.newEnvironRequested) { - this.log.debug('New environment already requested'); - return; - } - - const self = this; - - const bufs = buffers(); - bufs.push(Buffer.from( [ - COMMANDS.IAC, - COMMANDS.SB, - OPTIONS.NEW_ENVIRONMENT, - SB_COMMANDS.SEND ] - )); - - for(let i = 0; i < WANTED_ENVIRONMENT_VAR_BUFS.length; ++i) { - bufs.push(Buffer.from( [ NEW_ENVIRONMENT_COMMANDS.VAR ] ), WANTED_ENVIRONMENT_VAR_BUFS[i] ); - } - - bufs.push(Buffer.from([ NEW_ENVIRONMENT_COMMANDS.USERVAR, COMMANDS.IAC, COMMANDS.SE ])); - - self.output.write(bufs.toBuffer()); - - this.subNegotiationState.newEnvironRequested = true; -}; - -TelnetClient.prototype.banner = function() { - this.will.echo(); - - this.will.suppress_go_ahead(); - this.do.suppress_go_ahead(); - - this.do.transmit_binary(); - this.will.transmit_binary(); - - this.do.terminal_type(); - - this.do.window_size(); - this.do.new_environment(); -}; - -function Command(command, client) { - this.command = COMMANDS[command.toUpperCase()]; - this.client = client; -} - -// Create Command objects with echo, transmit_binary, ... -Object.keys(OPTIONS).forEach(function(name) { - const code = OPTIONS[name]; - - Command.prototype[name.toLowerCase()] = function() { - const buf = Buffer.alloc(3); - buf[0] = COMMANDS.IAC; - buf[1] = this.command; - buf[2] = code; - return this.client.output.write(buf); - }; -}); - -// Create do, dont, etc. methods on Client -['do', 'dont', 'will', 'wont'].forEach(function(command) { - const get = function() { - return new Command(command, this); - }; - - Object.defineProperty(TelnetClient.prototype, command, { - get : get, - enumerable : true, - configurable : true - }); -}); +inherits(TelnetClient, Client); exports.getModule = class TelnetServerModule extends LoginServerModule { constructor() { @@ -840,24 +240,10 @@ exports.getModule = class TelnetServerModule extends LoginServerModule { } createServer(cb) { - this.server = net.createServer( sock => { - const client = new TelnetClient(sock, sock); - - client.banner(); - - this.handleNewClient(client, sock, ModuleInfo); - - // - // Set a timeout and attempt to proceed even if we don't know - // the term type yet, which is the preferred trigger - // for moving along - // - setTimeout( () => { - if(!client.didReady) { - Log.info('Proceeding after 3s without knowing term type'); - client.readyNow(); - } - }, 3000); + this.server = net.createServer( socket => { + const client = new TelnetClient(socket); + client.banner(); // start negotiations + this.handleNewClient(client, socket, ModuleInfo); }); this.server.on('error', err => { @@ -883,3 +269,5 @@ exports.getModule = class TelnetServerModule extends LoginServerModule { }); } }; + +exports.TelnetClient = TelnetClient; // WebSockets is a wrapper on top of this diff --git a/core/servers/login/websocket.js b/core/servers/login/websocket.js index dadcdbe6..42a22723 100644 --- a/core/servers/login/websocket.js +++ b/core/servers/login/websocket.js @@ -15,6 +15,7 @@ const http = require('http'); const https = require('https'); const fs = require('graceful-fs'); const Writable = require('stream'); +const { Duplex } = require('stream'); const forEachSeries = require('async/forEachSeries'); const ModuleInfo = exports.moduleInfo = { @@ -24,100 +25,91 @@ const ModuleInfo = exports.moduleInfo = { packageName : 'codes.l33t.enigma.websocket.server', }; -function WebSocketClient(ws, req, serverType) { +class WebSocketClient extends TelnetClient { + constructor(ws, req, serverType) { + // allow WebSocket to act like a Duplex (socket) + const wsDuplex = new class WebSocketDuplex extends Duplex { + constructor(ws) { + super(); + this.ws = ws; - Object.defineProperty(this, 'isSecure', { - get : () => ('secure' === serverType || true === this.proxied) ? true : false, - }); + this.ws.on('close', err => this.emit('close', err)); + this.ws.on('error', err => this.emit('error', err)); + this.ws.on('message', data => this._data(data)); + } - const self = this; + setClient(client, httpRequest) { + this.client = client; - this.dataHandler = function(data) { - if(self.pipedDest) { - self.pipedDest.write(data); + // Support X-Forwarded-For and X-Real-IP headers for proxied connections + this.resolvedRemoteAddress = + (this.client.proxied && (httpRequest.headers['x-forwarded-for'] || httpRequest.headers['x-real-ip'])) || + httpRequest.connection.remoteAddress; + } + + get remoteAddress() { + return this.resolvedRemoteAddress; + } + + _write(data, encoding, cb) { + cb = cb || ( () => { /* eat it up */} ); // handle data writes after close + return this.ws.send(data, { binary : true }, cb); + } + + _read() { + // dummy + } + + _data(data) { + this.push(data); + } + }(ws); + + super(wsDuplex); + wsDuplex.setClient(this, req); + + // fudge remoteAddress on socket, which is now TelnetSocket + this.socket.remoteAddress = wsDuplex.remoteAddress; + + wsDuplex.on('close', () => { + // we'll remove client connection which will in turn end() via our SocketBridge above + return this.emit('end'); + }); + + this.serverType = serverType; + + // + // Monitor connection status with ping/pong + // + ws.on('pong', () => { + Log.trace(`Pong from ${wsDuplex.remoteAddress}`); + ws.isConnectionAlive = true; + }); + + Log.trace( { headers : req.headers }, 'WebSocket connection headers' ); + + // + // If the config allows it, look for 'x-forwarded-proto' as "https" + // to override |isSecure| + // + if(true === _.get(Config(), 'loginServers.webSocket.proxied') && + 'https' === req.headers['x-forwarded-proto']) + { + Log.debug(`Assuming secure connection due to X-Forwarded-Proto of "${req.headers['x-forwarded-proto']}"`); + this.proxied = true; } else { - self.socketBridge.emit('data', data); - } - }; - - // - // This bridge makes accessible various calls that client sub classes - // want to access on I/O socket - // - this.socketBridge = new class SocketBridge extends Writable { - constructor(ws) { - super(); - this.ws = ws; + this.proxied = false; } - end() { - return ws.close(); - } - - write(data, cb) { - cb = cb || ( () => { /* eat it up */} ); // handle data writes after close - - return this.ws.send(data, { binary : true }, cb); - } - - pipe(dest) { - Log.trace('WebSocket SocketBridge pipe()'); - self.pipedDest = dest; - } - - unpipe() { - Log.trace('WebSocket SocketBridge unpipe()'); - self.pipedDest = null; - } - - resume() { - Log.trace('WebSocket SocketBridge resume()'); - } - - get remoteAddress() { - // Support X-Forwarded-For and X-Real-IP headers for proxied connections - return (self.proxied && (req.headers['x-forwarded-for'] || req.headers['x-real-ip'])) || req.connection.remoteAddress; - } - }(ws); - - ws.on('message', this.dataHandler); - - ws.on('close', () => { - // we'll remove client connection which will in turn end() via our SocketBridge above - return this.emit('end'); - }); - - // - // Monitor connection status with ping/pong - // - ws.on('pong', () => { - Log.trace(`Pong from ${this.socketBridge.remoteAddress}`); - ws.isConnectionAlive = true; - }); - - TelnetClient.call(this, this.socketBridge, this.socketBridge); - - Log.trace( { headers : req.headers }, 'WebSocket connection headers' ); - - // - // If the config allows it, look for 'x-forwarded-proto' as "https" - // to override |isSecure| - // - if(true === _.get(Config(), 'loginServers.webSocket.proxied') && - 'https' === req.headers['x-forwarded-proto']) - { - Log.debug(`Assuming secure connection due to X-Forwarded-Proto of "${req.headers['x-forwarded-proto']}"`); - this.proxied = true; - } else { - this.proxied = false; + // start handshake process + this.banner(); } - // start handshake process - this.banner(); + get isSecure() { + return ('secure' === this.serverType || true === this.proxied) ? true : false; + } } -require('util').inherits(WebSocketClient, TelnetClient); - const WSS_SERVER_TYPES = [ 'insecure', 'secure' ]; exports.getModule = class WebSocketLoginServer extends LoginServerModule { @@ -216,7 +208,7 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { server.wsServer.on('connection', (ws, req) => { const webSocketClient = new WebSocketClient(ws, req, serverType); - this.handleNewClient(webSocketClient, webSocketClient.socketBridge, ModuleInfo); + this.handleNewClient(webSocketClient, webSocketClient.socket, ModuleInfo); }); Log.info( { server : serverName, port : port }, 'Listening for connections' ); @@ -227,9 +219,4 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { cb(err); }); } - - webSocketConnection(conn) { - const webSocketClient = new WebSocketClient(conn); - this.handleNewClient(webSocketClient, webSocketClient.socketShim, ModuleInfo); - } }; diff --git a/core/string_util.js b/core/string_util.js index fa9a9097..2f6596c8 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -13,6 +13,7 @@ exports.pad = pad; exports.insert = insert; exports.replaceAt = replaceAt; exports.isPrintable = isPrintable; +exports.containsNonLatinCodepoints = containsNonLatinCodepoints; exports.stripAllLineFeeds = stripAllLineFeeds; exports.debugEscapedString = debugEscapedString; exports.stringFromNullTermBuffer = stringFromNullTermBuffer; @@ -28,6 +29,7 @@ exports.isAnsi = isAnsi; exports.isAnsiLine = isAnsiLine; exports.isFormattedLine = isFormattedLine; exports.splitTextAtTerms = splitTextAtTerms; +exports.wildcardMatch = wildcardMatch; // :TODO: create Unicode version of this const VOWELS = [ @@ -196,6 +198,20 @@ function isPrintable(s) { return !RE_NON_PRINTABLE.test(s); } +const NonLatinCodePointsRegExp = /[^\u0000-\u00ff]/; + +function containsNonLatinCodepoints(s) { + if (!s.length) { + return false; + } + + if (s.charCodeAt(0) > 255) { + return true; + } + + return NonLatinCodepointsRegEx.test(s); +} + function stripAllLineFeeds(s) { return s.replace(/\r?\n|[\r\u2028\u2029]/g, ''); } @@ -459,3 +475,8 @@ function isAnsi(input) { function splitTextAtTerms(s) { return s.split(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g); } + +function wildcardMatch(input, rule) { + const escapeRegex = (s) => s.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); + return new RegExp("^" + rule.split("*").map(escapeRegex).join(".*") + "$").test(input); +} \ No newline at end of file diff --git a/core/telnet_bridge.js b/core/telnet_bridge.js index a3a4d672..5c43ced6 100644 --- a/core/telnet_bridge.js +++ b/core/telnet_bridge.js @@ -11,7 +11,16 @@ const async = require('async'); const _ = require('lodash'); const net = require('net'); const EventEmitter = require('events'); -const buffers = require('buffers'); + +const { + TelnetSocket, + TelnetSpec : + { + Commands, + Options, + SubNegotiationCommands, + }, +} = require('telnet-socket'); /* Expected configuration block: @@ -33,7 +42,10 @@ exports.moduleInfo = { author : 'Andrew Pamment', }; -const IAC_DO_TERM_TYPE = Buffer.from( [ 255, 253, 24 ] ); +const IAC_DO_TERM_TYPE = TelnetSocket.commandBuffer( + Commands.DO, + Options.TTYPE, +); class TelnetClientConnection extends EventEmitter { constructor(client) { @@ -46,6 +58,7 @@ class TelnetClientConnection extends EventEmitter { restorePipe() { if(!this.pipeRestored) { this.pipeRestored = true; + this.client.dataPassthrough = false; // client may have bailed if(null !== _.get(this, 'client.term.output', null)) { @@ -62,6 +75,7 @@ class TelnetClientConnection extends EventEmitter { this.emit('connected'); this.pipeRestored = false; + this.client.dataPassthrough = true; this.client.term.output.pipe(this.bridgeConnection); }); @@ -69,7 +83,7 @@ class TelnetClientConnection extends EventEmitter { this.client.term.rawWrite(data); // - // Wait for a terminal type request, and send it eactly once. + // Wait for a terminal type request, and send it exactly once. // This is enough (in additional to other negotiations handled in telnet.js) // to get us in on most systems // @@ -110,25 +124,18 @@ class TelnetClientConnection extends EventEmitter { // Create a TERMINAL-TYPE sub negotiation buffer using the // actual/current terminal type. // - let bufs = buffers(); - - bufs.push(Buffer.from( + const sendTermType = TelnetSocket.commandBuffer( + Commands.SB, + Options.TTYPE, [ - 255, // IAC - 250, // SB - 24, // TERMINAL-TYPE - 0, // IS + SubNegotiationCommands.IS, + ...Buffer.from(this.client.term.termType), // e.g. "ansi" + Commands.IAC, + Commands.SE, ] - )); - - bufs.push( - Buffer.from(this.client.term.termType), // e.g. "ansi" - Buffer.from( [ 255, 240 ] ) // IAC, SE ); - - return bufs.toBuffer(); + return sendTermType; } - } exports.getModule = class TelnetBridgeModule extends MenuModule { diff --git a/core/user_login.js b/core/user_login.js index 99faa1a2..3db6b5cc 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -81,9 +81,9 @@ function userLogin(client, username, password, options, cb) { if(existingClientConnection) { client.log.info( { - existingClientId : existingClientConnection.session.id, - username : user.username, - userId : user.userId + existingNodeId : existingClientConnection.node, + username : user.username, + userId : user.userId }, 'Already logged in' ); @@ -97,11 +97,12 @@ function userLogin(client, username, password, options, cb) { // update client logger with addition of username client.log = logger.log.child( { - clientId : client.log.fields.clientId, + nodeId : client.log.fields.nodeId, sessionId : client.log.fields.sessionId, username : user.username, } ); + client.log.info('Successful login'); // User's unique session identifier is the same as the connection itself diff --git a/core/user_property.js b/core/user_property.js index cc68ef09..d55a0ebe 100644 --- a/core/user_property.js +++ b/core/user_property.js @@ -29,7 +29,7 @@ module.exports = { UserComment : 'user_comment', // NYI AutoSignature : 'auto_signature', - DownloadQueue : 'dl_queue', // download_queue.js + DownloadQueue : 'dl_queue', // see download_queue.js FailedLoginAttempts : 'failed_login_attempts', AccountLockedTs : 'account_locked_timestamp', @@ -64,5 +64,6 @@ module.exports = { AuthFactor2OTP : 'auth_factor2_otp', // If present, OTP type for 2FA. See OTPTypes AuthFactor2OTPSecret : 'auth_factor2_otp_secret', // Secret used in conjunction with OTP 2FA AuthFactor2OTPBackupCodes : 'auth_factor2_otp_backup', // JSON array of backup codes + }; diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index e51a8734..6b335617 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -41,6 +41,8 @@ - [Message networks]({{ site.baseurl }}{% link messageareas/message-networks.md %}) - [BSO Import & Export]({{ site.baseurl }}{% link messageareas/bso-import-export.md %}) - [Netmail]({{ site.baseurl }}{% link messageareas/netmail.md %}) + - [QWK]({{ site.baseurl }}{% link messageareas/qwk.md %}) + - [FTN]({{ site.baseurl }}{% link messageareas/ftn.md %}) - Art - [General]({{ site.baseurl }}{% link art/general.md %}) @@ -65,6 +67,7 @@ - BBSLink - Combatnet - Exodus + - [Telnet Bridge]({{ site.baseurl }}{% link modding/telnet-bridge.md %}) - [Existing Mods]({{ site.baseurl }}{% link modding/existing-mods.md %}) - [File Area List]({{ site.baseurl }}{% link modding/file-area-list.md %}) - [Last Callers]({{ site.baseurl }}{% link modding/last-callers.md %}) @@ -86,6 +89,7 @@ - [Auto Signature Editor]({{ site.baseurl }}{% link modding/autosig-edit.md %}) - Administration + - [Administration]({{ site.baseurl }}{% link admin/administration.md %}) - [oputil]({{ site.baseurl }}{% link admin/oputil.md %}) - [Updating]({{ site.baseurl }}{% link admin/updating.md %}) diff --git a/docs/admin/administration.md b/docs/admin/administration.md new file mode 100644 index 00000000..8d15fba5 --- /dev/null +++ b/docs/admin/administration.md @@ -0,0 +1,43 @@ +--- +layout: page +title: Administration +--- + +# Administration + +## Keeping Up to Date +See [Updating](updating.md). + +## Viewing Logs +See [Monitoring Logs](/docs/troubleshooting/monitoring-logs.md). + +## Managing Users +User management is currently handled via the [oputil CLI](oputil.md). + +## Backing Up Your System +It is *highly* recommended to perform **regular backups** of your system. Nothing is worse than spending a lot of time setting up a system only to have to go away unexpectedly! + +In general, simply creating a copy/archive of your system is enough for the default configuration. If you have changed default paths to point outside of your main ENiGMA½ installation take special care to ensure these are preserved as well. Database files may be in a state of flux when simply copying files. See **Database Backups** below for details on consistent backups. + +### Database Backups +[SQLite's CLI backup command](https://sqlite.org/cli.html#special_commands_to_sqlite3_dot_commands_) can be used for creating database backup files. This can be performed as an additional step to a full backup to ensure the database is backed up in a consistent state (whereas simply copying the files does not make any guarantees). + +As an example, consider the following Bash script that creates foo.sqlite3.backup files: + +```bash +for dbfile in /path/to/enigma-bbs/db/*.sqlite3; do + sqlite3 $dbfile ".backup '/path/to/db_backup/$(basename $dbfile).backup'" +done +``` + +### Backup Tools +There are many backup solutions available across all platforms. Configuration of such tools is outside the scope of this documentation. With that said, the author has had great success with [Borg](https://www.borgbackup.org/). + +## General Maintenance Tasks +### Vacuuming Database Files +SQLite database files become less performant over time and waste space. It is recommended to periodically vacuum your databases. Before proceeding, you should make a backup! + +Example: +```bash +sqlite3 ./db/message.sqlite3 "vacuum;" +``` \ No newline at end of file diff --git a/docs/admin/oputil.md b/docs/admin/oputil.md index 0b05f7aa..e591de9f 100644 --- a/docs/admin/oputil.md +++ b/docs/admin/oputil.md @@ -291,17 +291,32 @@ Actions: import-areas PATH Import areas using FidoNet *.NA or AREAS.BBS file + qwk-dump PATH Dumps a QWK packet to stdout. + qwk-export [AREA_TAGS] PATH Exports one or more configured message area to a QWK + packet in the directory specified by PATH. The QWK + BBS ID will be obtained by the final component of PATH. + import-areas arguments: --conf CONF_TAG Conference tag in which to import areas --network NETWORK Network name/key to associate FTN areas --uplinks UL1,UL2,... One or more uplinks (comma separated) --type TYPE Area import type - Valid types are "bbs" and "na" + Valid types are "bbs" and "na". + +qwk-export arguments: + --user USER User in which to export for. Defaults to the SysOp. + --after TIMESTAMP Export only messages with a timestamp later than + TIMESTAMP. + --no-qwke Disable QWKE extensions. + --no-synchronet Disable Synchronet style extensions. ``` | Action | Description | Examples | |-----------|-------------------|---------------------------------------| | `import-areas` | Imports areas using a FidoNet style *.NA or AREAS.BBS formatted file. Optionally maps areas to FTN networks. | `./oputil.js config import-areas /some/path/l33tnet.na` | +| `areafix` | Utility for sending AreaFix mails without logging into the system | | +| `qwk-dump` | Dump a QWK packet to stdout | `./oputil.js mb qwk-dump /path/to/XIBALBA.QWK` | +| `qwk-export` | Export messages to a QWK packet | `./oputil.js mb qwk-export /path/to/XIBALBA.QWK` | When using the `import-areas` action, you will be prompted for any missing additional arguments described in "import-areas args". diff --git a/docs/admin/updating.md b/docs/admin/updating.md index 235737b7..3cf93768 100644 --- a/docs/admin/updating.md +++ b/docs/admin/updating.md @@ -2,19 +2,30 @@ layout: page title: Updating --- -## Updating your Installation -Updating ENiGMA½ can be a bit of a learning curve compared to other systems. Especially when running off of a development branch (such as `0.0.9-alpha` being the recommended branch as of this writing), you'll want frequent updates. +# Updating +Keeping your system up to date ensures you have the latest fixes, features, and general improvements. Updating ENiGMA½ can be a bit of a learning curve compared to traditional binary-release systems you may be used to, especially when running from Git cloned source. -## Steps -In general the steps are as follows: -1. `cd /path/to/enigma-bbs` -2. `git pull` -3. `npm update` or `yarn` to refresh any new or updated modules. -4. Merge updates to `config/menu_template.hjson` to your `config/yourbbsname-menu.hjson` file. +## Updating From Source +If you have installed using Git source (if you used the `install.sh` script) follow these general steps to update your system: + +1. **Back up your system**! +2. Pull down the latest source: +```bash +cd /path/to/enigma-bbs +git pull +``` +3. :bulb: Review `WHATSNEW.md` and `UPDATE.md` for any specific instructions or changes to be aware of. +4. Update your dependencies: +```bash +npm install # or 'yarn' +``` +4. Merge updates from `config/menu_template.hjson` to your `config/yourbbsname-menu.hjson` file (or simply use the template as a reference to spot any newly added default menus that you may wish to have on your system as well!). 5. If there are updates to the `art/themes/luciano_blocktronics/theme.hjson` file and you have a custom theme, you may want to look at them as well. +6. Finally, restart your running ENiGMA½ instance. -Visual diff tools such as [DiffMerge](https://www.sourcegear.com/diffmerge/downloads.php) (free, works on all major platforms) can be very helpful here. +:information_source: Visual diff tools such as [DiffMerge](https://www.sourcegear.com/diffmerge/downloads.php) (free, works on all major platforms) can be very helpful for the tasks outlined above! + +:information_source: It is recommended to tail the logs and poke around a bit after an update. -Remember to also keep an eye on [WHATSNEW](/WHATSNEW.md) and [UPGRADE](/UPGRADE.md)! diff --git a/docs/art/general.md b/docs/art/general.md index a0620de3..18c438ec 100644 --- a/docs/art/general.md +++ b/docs/art/general.md @@ -7,13 +7,36 @@ One of the most basic elements of BBS customization is through it's artwork. ENi As a general rule, art files live in one of two places: -1. The `art/general` directory. This is where you place command non-themed art files. -2. Within a theme such as `art/themes/super_fancy_theme`. +1. The `art/general` directory. This is where you place common/non-themed art files. +2. Within a _theme_ such as `art/themes/super_fancy_theme`. -### Menu Entries -While art can be displayed programmatically such as from a custom module, the most basic and common form is via `menu.hjson` entries. This usually falls into one of two forms: a "standard" entry where a single `art` spec is utilized or a entry for a custom module where multiple pieces are declared and used. The second style usually takes the form of a `config.art` block with two or more entries. +### Art in Menus +While art can be displayed programmatically such as from a custom module, the most basic and common form is via `menu.hjson` entries. This usually falls into one of two forms. -A menu entry has a few elements that control how art is choosen and displayed. First, the `art` *spec* tells teh system how to look for the art asset. Second, the `config` block can further control aspecs of lookup and display: +**Form 1**: A "standard" entry where a single `art` spec is utilized: +```hjson +{ + mainMenu: { + art: main_menu.ans + } +} +``` + +**Form 2**: An entry for a custom module where multiple pieces are declared and used. The second style usually takes the form of a `config.art` block with two or more entries: +```hjson +{ + nodeMessage: { + config: { + art: { + header: node_msg_header + footer: node_msg_footer + } + } + } +} +``` + +A menu entry has a few elements that control how art is selected and displayed. First, the `art` *spec* tells the system how to look for the art asset. Second, the `config` block can further control aspects of lookup and display. The following table describes such entries: | Item | Description| |------|------------| @@ -23,13 +46,13 @@ A menu entry has a few elements that control how art is choosen and displayed. F | `cls` | Clear the screen before display if set to `true`. | | `random` | Set to `false` to explicitly disable random lookup. | | `types` | An optional array of types (aka file extensions) to consider for lookup. For example : `[ '.ans', '.asc' ]` | -| `readSauce` | May be set to `false` if you need to explictly disable SAUCE support. | +| `readSauce` | May be set to `false` if you need to explicitly disable SAUCE support. | #### Art Spec -It was mentioned that the `art` member is a *spec*. The value of a `art` member controls how the system looks for an asset. The following forms are supported: +In the section above it is mentioned that the `art` member is a *spec*. The value of a `art` spec controls how the system looks for an asset. The following forms are supported: * `FOO`: The system will look for `FOO.ANS`, `FOO.ASC`, `FOO.TXT`, etc. using the default search path. Unless otherwise specified if `FOO1.ANS`, `FOO2.ANS`, and so on exist, a random selection will be made. -* `FOO.ANS`: By specifying an extension, only that type will be searched for. +* `FOO.ANS`: By specifying an extension, only the exact match will be searched for. * `rel/path/to/BAR.ANS`: Only match a path (relative to the system's `art` directory). * `/path/to/BAZ.ANS`: Exact path only. @@ -40,8 +63,27 @@ ENiGMA½ uses a fallback system for art selection. When a menu entry calls for a 3. In the system default theme directory. 4. In the `art/general` directory. +#### ACS-Driven Conditionals +The [ACS](/docs/configuration/acs.md) system can be used to make conditional art selection choices. To do this, provide an array of possible values in your art spec. As an example: +```hjson +{ + fancyMenu: { + art: [ + { + acs: GM[l33t] + art: leet_art.ans + } + { + // default + art: newb.asc + } + ] + } +} +``` + #### SyncTERM Style Fonts -ENiGMA½ can set a [SyncTERM](http://syncterm.bbsdev.net/) style font for art display. This is supported by many popular BBS terminals besides just SyncTERM and is common for displaying Amiga style fonts for example. The system will use the `font` specifier or look for a font declared in an artworks SAUCE record (unless `readSauce` is `false`). +ENiGMA½ can set a [SyncTERM](http://syncterm.bbsdev.net/) style font for art display. This is supported by many other popular BBS terminals as well. A common usage is for displaying Amiga style fonts for example. The system will use the `font` specifier or look for a font declared in an artworks SAUCE record (unless `readSauce` is `false`). The most common fonts are probably as follows: @@ -96,7 +138,7 @@ See [this specification](https://github.com/protomouse/synchronet/blob/master/sr #### SyncTERM Style Baud Rates The `baudRate` member can set a [SyncTERM](http://syncterm.bbsdev.net/) style emulated baud rate. May be `300`, `600`, `1200`, `2400`, `4800`, `9600`, `19200`, `38400`, `57600`, `76800`, or `115200`. A value of `ulimited`, `off`, or `0` resets (disables) the rate. See [this specification](https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt) for more information. -## Common Example +### Common Example ```hjson fullLogoffSequenceRandomBoardAd: { art: OTHRBBS @@ -108,4 +150,7 @@ fullLogoffSequenceRandomBoardAd: { cls: true } } -``` \ No newline at end of file +``` + +### See Also +See also the [Show Art Module](/docs/modding/show-art.md) for more advanced art display! \ No newline at end of file diff --git a/docs/art/mci.md b/docs/art/mci.md index 60bb8ae6..42091d98 100644 --- a/docs/art/mci.md +++ b/docs/art/mci.md @@ -2,22 +2,22 @@ layout: page title: MCI Codes --- -ENiGMA½ supports a variety of MCI codes. Some **predefined** codes produce information about the current user, system, -or other statistics while others are used to instantiate a **View**. MCI codes are two characters in length and are -prefixed with a percent (%) symbol. Some MCI codes have additional options that may be set directly from the code itself -while others -- and more advanced options -- are controlled via the current theme. Standard (non-focus) and focus colors +ENiGMA½ supports a variety of MCI codes. Some **predefined** codes produce information about the current user, system, +or other statistics while others are used to instantiate a **View**. MCI codes are two characters in length and are +prefixed with a percent (%) symbol. Some MCI codes have additional options that may be set directly from the code itself +while others -- and more advanced options -- are controlled via the current theme. Standard (non-focus) and focus colors are set by placing duplicate codes back to back in art files. ## Predefined MCI Codes -There are many predefined MCI codes that can be used anywhere on the system (placed in any art file). More are added all -the time so also check out [core/predefined_mci.js](https://github.com/NuSkooler/enigma-bbs/blob/master/core/mci_view_factory.js) +There are many predefined MCI codes that can be used anywhere on the system (placed in any art file). More are added all +the time so also check out [core/predefined_mci.js](https://github.com/NuSkooler/enigma-bbs/blob/master/core/mci_view_factory.js) for a full listing. Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, etc. | Code | Description | |------|--------------| | `BN` | Board Name | -| `VL` | Version *label*, e.g. "ENiGMA½ v0.0.10-alpha" | -| `VN` | Version *number*, eg.. "0.0.10-alpha" | +| `VL` | Version *label*, e.g. "ENiGMA½ v0.0.11-beta" | +| `VN` | Version *number*, eg.. "0.0.11-beta" | | `SN` | SysOp username | | `SR` | SysOp real name | | `SL` | SysOp location | @@ -75,7 +75,7 @@ for a full listing. Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, et | `SD` | Total downloads, system wide | | `SO` | Total downloaded amount, system wide (formatted to appropriate bytes/megs/etc.) | | `SU` | Total uploads, system wide | -| `SP` | Total uploaded amount, system wide (formatted to appropriate bytes/megs/etc.) | +| `SP` | Total uploaded amount, system wide (formatted to appropriate bytes/megs/etc.) | | `TF` | Total number of files on the system | | `TB` | Total amount of files on the system (formatted to appropriate bytes/megs/gigs/etc.) | | `TP` | Total messages posted/imported to the system *currently* | @@ -93,7 +93,7 @@ Some additional special case codes also exist: ## Views -A **View** is a control placed on a **form** that can display variable data or collect input. One example of a View is +A **View** is a control placed on a **form** that can display variable data or collect input. One example of a View is a Vertical Menu (`%VM`): Old-school BBSers may recognize this as a lightbar menu. | Code | Name | Description | @@ -103,14 +103,14 @@ a Vertical Menu (`%VM`): Old-school BBSers may recognize this as a lightbar menu | `ME` | Masked Edit Text | Collect user input using a *mask* | | `MT` | Multi Line Text Edit | Multi line edit control | | `BT` | Button | A button | -| `VM` | Vertical Menu | A vertical menu aka a vertical lightbar | -| `HM` | Horizontal Menu | A horizontal menu aka a horizontal lightbar | +| `VM` | Vertical Menu | A vertical menu aka a vertical lightbar | +| `HM` | Horizontal Menu | A horizontal menu aka a horizontal lightbar | | `SM` | Spinner Menu | A spinner input control | -| `TM` | Toggle Menu | A toggle menu commonly used for Yes/No style input | +| `TM` | Toggle Menu | A toggle menu commonly used for Yes/No style input | | `KE` | Key Entry | A *single* key input control | -Peek at [/core/mci_view_factory.js](https://github.com/NuSkooler/enigma-bbs/blob/master/core/mci_view_factory.js) to +Peek at [/core/mci_view_factory.js](https://github.com/NuSkooler/enigma-bbs/blob/master/core/mci_view_factory.js) to see additional information. @@ -132,7 +132,7 @@ Predefined MCI codes and other Views can have properties set via `menu.hjson` an | `itemFormat` | Sets the format for a list entry. See **Entry Formatting** below | | `focusItemFormat` | Sets the format for a focused list entry. See **Entry Formatting** below | -These are just a few of the properties set on various views. *Use the source Luke*, as well as taking a look at the default +These are just a few of the properties set on various views. *Use the source Luke*, as well as taking a look at the default `menu.hjson` and `theme.hjson` files! ### Custom Properties @@ -144,7 +144,7 @@ Standard style types available for `textStyle` and `focusTextStyle`: | Style | Description | |----------|--------------| -| `normal` | Leaves text as-is. This is the default. | +| `normal` | Leaves text as-is. This is the default. | | `upper` | ENIGMA BULLETIN BOARD SOFTWARE | | `lower` | enigma bulletin board software | | `title` | Enigma Bulletin Board Software | diff --git a/docs/filebase/first-file-area.md b/docs/filebase/first-file-area.md index 46d8d649..e61e5a25 100644 --- a/docs/filebase/first-file-area.md +++ b/docs/filebase/first-file-area.md @@ -3,15 +3,15 @@ layout: page title: Configuring a File Base --- ## Configuring a File Base -ENiGMA½ offers a powerful and flexible file base. Configuration of file the file base and areas is handled via the `fileBase` section of `config.hjson`. +ENiGMA½ offers a powerful and flexible file base. Configuration of file the file base and areas is handled via the `fileBase` section of `config.hjson`. ## ENiGMA½ File Base Key Concepts First, there are some core concepts you should understand: * Storage Tags -* Areas (and Area Tags) +* Area Tags ### Storage Tags -*Storage Tags* define paths to physical (file) storage locations that are referenced in a file *Area* entry. Each entry may be either a fully qualified path or a relative path. Relative paths are relative to the value set by the `areaStoragePrefix` key (defaults to `/path/to/enigma-bbs/file_base`). +*Storage Tags* define paths to physical (filesystem) storage locations that are referenced in a file *Area* entry. Each entry may be either a fully qualified path or a relative path. Relative paths are relative to the value set by the `fileBase.areaStoragePrefix` key (defaults to `/path/to/enigma-bbs/file_base`). Below is an example defining some storage tags using the relative and fully qualified forms: @@ -28,7 +28,7 @@ storageTags: { :information_source: Remember that paths are case sensitive on most non-Windows systems! ### Areas -File base *Areas* are configured using the `fileBase::areas` configuration block in `config.hjson`. Valid members for an area are as follows: +File base *Areas* are configured using the `fileBase.areas` configuration block in `config.hjson`. Each entry's block starts with an *area tag*. Valid members for an area are as follows: | Item | Required | Description | |--------|---------------|------------------| @@ -41,7 +41,7 @@ Example areas section: ```hjson areas: { - retro_pc: { + retro_pc: { // an area tag! name: Retro PC desc: Oldschool PC/DOS storageTags: [ "retro_pc_dos", "retro_pc_bbs" ] @@ -55,6 +55,7 @@ This combines the two concepts described above. When viewing the file areas from ```hjson fileBase: { + // override the default relative location areaStoragePrefix: /enigma-bbs/file_base storageTags: { diff --git a/docs/filebase/index.md b/docs/filebase/index.md index d5dac134..0e14e732 100644 --- a/docs/filebase/index.md +++ b/docs/filebase/index.md @@ -20,3 +20,9 @@ ENiGMA½ has strayed away from the old familiar setup here and instead takes a m * Duplicates are checked for by cryptographically secure [SHA-256](https://en.wikipedia.org/wiki/SHA-2) hashes. * Support for many archive and file formats. External utilities can easily be added to the configuration to extend for additional formats. * Much, much more! + +### Modding +The default ENiGMA½ approach for file areas may not be for everyone. Remember that you can mod everything your setup! Some inspirational examples: +* A more traditional set of areas and scrolling file listings. +* An S/X style integration of message areas and file areas. +* Something completely different! Some tweaks are possible without any code while others may require creating new JavaScript modules to use instead of the defaults. diff --git a/docs/filebase/tic-support.md b/docs/filebase/tic-support.md index 9d092fb1..4c128bab 100644 --- a/docs/filebase/tic-support.md +++ b/docs/filebase/tic-support.md @@ -2,9 +2,10 @@ layout: page title: TIC Support --- -ENiGMA½ supports TIC files. This is handled by mapping TIC areas to local file areas. +## TIC Support +ENiGMA½ supports FidoNet-Style TIC file attachments by mapping TIC areas to local file areas. -Under a given node defined in the `ftn_bso` config section in `config.hjson` (see +Under a given node defined in the `ftn_bso` config section in `config.hjson` (see [BSO Import/Export](../messageareas/bso-import-export)), TIC configuration may be supplied: ```hjson @@ -28,11 +29,9 @@ Under a given node defined in the `ftn_bso` config section in `config.hjson` (se } ``` -You then need to configure the mapping between TIC areas you want to carry, and the file -base area and storage tag for them to be tossed to. Optionally you can also add hashtags to the tossed -files to assist users in searching for files: +You then need to configure the mapping between TIC areas you want to carry, and the file base area and storage tag for them to be tossed to. Optionally you can also add hashtags to the tossed files to assist users in searching for files: -````hjson +```hjson ticAreas: { agn_node: { areaTag: msgNetworks @@ -41,21 +40,20 @@ ticAreas: { } } -```` -Multiple TIC areas can be mapped to a single file base area. +``` +Multiple TIC areas can be mapped to a single file base area. -## Example Configuration +### Example Configuration +An example configuration linking file base areas, FTN BSO node configuration and TIC area configuration. -An example configuration linking filebase areas, FTN BSO node configuration and TIC area configuration. - -````hjson +```hjson fileBase: { areaStoragePrefix: /home/bbs/file_areas/ - + storageTags: { msg_network: "msg_network" } - + areas: { msgNetworks: { name: Message Networks @@ -97,4 +95,7 @@ ticAreas: { hashTags: agoranet,infopack } } -```` \ No newline at end of file +``` + +## See Also +[Message Networks](/docs/messageareas/message-networks.md) diff --git a/docs/filebase/uploads.md b/docs/filebase/uploads.md index 8e1e2530..795c0781 100644 --- a/docs/filebase/uploads.md +++ b/docs/filebase/uploads.md @@ -3,9 +3,9 @@ layout: page title: Uploads --- ## Uploads -The default ACS for file areas areas in ENiGMA½ is to allow read (viewing of the area), and downloads for users while only permitting SysOps to write (upload). See [File Base ACS](acs.md) for more information. +The default ACS for file areas in ENiGMA½ is to allow regular users 'read' and sysops 'read/write'. Read ACS includes listing and downloading while write allows for uploading. See [File Base ACS](acs.md) for more information. -To allow uploads to a particular area, change the ACS level for `write`. For example: +Let's allow regular users (in the "users" group) to upload to an area: ```hjson uploads: { name: Uploads diff --git a/docs/installation/install-script.md b/docs/installation/install-script.md index 2f92db79..564a6973 100644 --- a/docs/installation/install-script.md +++ b/docs/installation/install-script.md @@ -6,10 +6,10 @@ title: Install Script Under most Linux/UNIX like environments (Linux, BSD, OS X, ...) new users can simply execute the `install.sh` script to get everything up and running. Cut + paste the following into your terminal: ``` -curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/master/misc/install.sh | bash +curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/0.0.11-beta/misc/install.sh | bash ``` -You may review the [installation script](https://raw.githubusercontent.com/NuSkooler/enigma-bbs/master/misc/install.sh) +You may review the [installation script](https://raw.githubusercontent.com/NuSkooler/enigma-bbs/0.0.11-beta/misc/install.sh) on GitHub before running it. The script will install nvm, Node.js 6 and grab the latest ENiGMA BBS from GitHub. It will also guide you through creating a basic configuration file, and recommend some packages to install. diff --git a/docs/installation/testing.md b/docs/installation/testing.md index bfd2de26..576b28fd 100644 --- a/docs/installation/testing.md +++ b/docs/installation/testing.md @@ -26,7 +26,9 @@ _____________________ _____ ____________________ __________\_ / System started! ``` -Grab your favourite telnet client, connect to localhost:8888 and test out your installation. +Grab your favourite telnet client, connect to localhost:8888 and test out your installation. + +To shut down the server, press Ctrl-C. ## Points of Interest @@ -44,4 +46,4 @@ If you don't have any telnet software, these are compatible with ENiGMA½: * [NetRunner](http://mysticbbs.com/downloads.html) * [MagiTerm](https://magickabbs.com/index.php/magiterm/) * [VTX](https://github.com/codewar65/VTX_ClientServer) (Browser based) -* [fTelnet](https://www.ftelnet.ca/) (Browser based) \ No newline at end of file +* [fTelnet](https://www.ftelnet.ca/) (Browser based) diff --git a/docs/messageareas/bso-import-export.md b/docs/messageareas/bso-import-export.md index fac7bc6d..6fc6298e 100644 --- a/docs/messageareas/bso-import-export.md +++ b/docs/messageareas/bso-import-export.md @@ -5,58 +5,23 @@ title: BSO Import / Export ## BSO Import / Export The scanner/tosser module `ftn_bso` provides **B**inkley **S**tyle **O**utbound (BSO) import/toss and scan/export of messages EchoMail and NetMail messages. Configuration is supplied in `config.hjson` under `scannerTossers.ftn_bso`. -:information_source: ENiGMA½'s `ftn_bso` module is not a mailer and **makes no attempts** to perfrom packet transport! An external [mailer](http://www.filegate.net/bbsmailers.htm) such as [Binkd](https://github.com/pgul/binkd) is required for this! +:information_source: ENiGMA½'s `ftn_bso` module is not a mailer and **makes no attempts to perform packet transport**! An external [mailer](http://www.filegate.net/bbsmailers.htm) such as [Binkd](https://github.com/pgul/binkd) is required for this task. +### Configuration Let's look at some of the basic configuration: | Config Item | Required | Description | |-------------|----------|----------------------------------------------------------| -| `schedule` | :+1: | Sets `import` and `export` schedules. [Later style text parsing](https://bunkat.github.io/later/parsers.html#text) supported. `import` also can utilize a `@watch:` syntax while `export` additionally supports `@immediate`. | +| `schedule` | :+1: | Sets `import` and `export` schedules. [Later style text parsing](https://bunkat.github.io/later/parsers.html#text) supported. `import` also can utilize a `@watch:` syntax while `export` additionally supports `@immediate`. | | `packetMsgEncoding` | :-1: | Override default `utf8` encoding. -| `defaultNetwork` | :-1: | Explicitly set default network (by tag in `messageNetworks.ftn.networks`). If not set, the first found is used. | -| `nodes` | :+1: | Per-node settings. Entries (keys) here support wildcards for a portion of the FTN-style address (e.g.: `21:1/*`). `archiveType` may be set to a FTN supported archive extention that the system supports (TODO); if unset, only .PKT files are produced. `encoding` may be set to override `packetMsgEncoding` on a per-node basis. If the node requires a packet password, set `packetPassword` | -| `paths` | :-1: | An optional configuration block that can set a additional paths or override defaults. See "Paths" below. | +| `defaultNetwork` | :-1: | Explicitly set default network (by tag found within `messageNetworks.ftn.networks`). If not set, the first found is used. | +| `nodes` | :+1: | Per-node settings. Entries (keys) here support wildcards for a portion of the FTN-style address (e.g.: `21:1/*`). See **Nodes** below. +| `paths` | :-1: | An optional configuration block that can set a additional paths or override defaults. See **Paths** below. | | `packetTargetByteSize` | :-1: | Overrides the system *target* packet (.pkt) size of 512000 bytes (512k) | | `bundleTargetByteSize` | :-1: | Overrides the system *target* ArcMail bundle size of 2048000 bytes (2M) | -### Paths -Paths for packet files work out of the box and are relative to your install directory. If you want to configure `reject` or `retain` to keep rejected/imported packet files respectively, set those values. You may override defaults as well. - -| Key | Description | Default | -|-----|-------------|---------| -| `outbound` | *Base* path to write outbound (exported) packet files and bundles. | `enigma-bbs/mail/ftn_out/` | -| `inbound` | *Base* path to write inbound (ie: those written by an external mailer) packet files an bundles. | `enigma-bbs/mail/ftn_in/` | -| `secInbound` | *Base* path to write **secure** inbound packet files and bundles. | `enigma-bbs/mail/ftn_secin/` | -| `reject` | Path in which to write rejected packet files. | No default | -| `retain` | Path in which to write imported packet files. Useful for debugging or if you wish to archive the raw .pkt files. | No default | - - -## Scheduling -Schedules can be defined for importing and exporting via `import` and `export` under `schedule`. Each entry is allowed a "free form" text and/or special indicators for immediate export or watch file triggers. - - * `@immediate`: A message will be immediately exported if this trigger is defined in a schedule. Only used for `export`. - * `@watch:/path/to/file`: This trigger watches the path specified for changes and will trigger an import or export when such events occur. Only used for `import`. - * Free form [Later style](https://bunkat.github.io/later/parsers.html#text) text — can be things like `at 5:00 pm` or `every 2 hours`. - -See [Later text parsing documentation](http://bunkat.github.io/later/parsers.html#text) for more information. - -### Example Schedule Configuration - -```hjson -{ - scannerTossers: { - ftn_bso: { - schedule: { - import: every 1 hours or @watch:/path/to/watchfile.ext - export: every 1 hours or @immediate - } - } - } -} -``` - -## Nodes -The `nodes` section defines how to export messages for one or more uplinks. +#### Nodes +The `nodes` section defines how to export messages for one or more uplinks. A node entry starts with a [FTN address](http://ftsc.org/docs/old/fsp-1028.001) (up to 5D) **as a key** in `config.hjson`. This key may contain wildcard(s) for net/zone/node/point/domain. @@ -65,7 +30,7 @@ A node entry starts with a [FTN address](http://ftsc.org/docs/old/fsp-1028.001) | `packetType` | :-1: | `2`, `2.2`, or `2+`. Defaults to `2+` for modern mailer compatiability. | | `packetPassword` | :-1: | Optional password for the packet | | `encoding` | :-1: | Encoding to use for message bodies; Defaults to `utf-8`. | -| `archiveType` | :-1: | Specifies the archive type (by extension) for ArcMail bundles. This should be `zip` for most setups. Other valid examples include `arc`, `arj`, `lhz`, `pak`, `sqz`, or `zoo`. See [Archivers](docs/configuration/archivers.md) for more information. | +| `archiveType` | :-1: | Specifies the archive type (by extension or MIME type) for ArcMail bundles. This should be `zip` (or `application/zip`) for most setups. Other valid examples include `arc`, `arj`, `lhz`, `pak`, `sqz`, or `zoo`. See [Archivers](docs/configuration/archivers.md) for more information. | **Example**: ```hjson @@ -85,7 +50,42 @@ A node entry starts with a [FTN address](http://ftsc.org/docs/old/fsp-1028.001) } ``` -## A More Complete Example +#### Paths +Paths for packet files work out of the box and are relative to your install directory. If you want to configure `reject` or `retain` to keep rejected/imported packet files respectively, set those values. You may override defaults as well. + +| Key | Description | Default | +|-----|-------------|---------| +| `outbound` | *Base* path to write outbound (exported) packet files and bundles. | `enigma-bbs/mail/ftn_out/` | +| `inbound` | *Base* path to write inbound (ie: those written by an external mailer) packet files an bundles. | `enigma-bbs/mail/ftn_in/` | +| `secInbound` | *Base* path to write **secure** inbound packet files and bundles. | `enigma-bbs/mail/ftn_secin/` | +| `reject` | Path in which to write rejected packet files. | No default | +| `retain` | Path in which to write imported packet files. Useful for debugging or if you wish to archive the raw .pkt files. | No default | + +### Scheduling +Schedules can be defined for importing and exporting via `import` and `export` under `schedule`. Each entry is allowed a "free form" text and/or special indicators for immediate export or watch file triggers. + +* `@immediate`: A message will be immediately exported if this trigger is defined in a schedule. Only used for `export`. +* `@watch:/path/to/file`: This trigger watches the path specified for changes and will trigger an import or export when such events occur. Only used for `import`. +* Free form [Later style](https://bunkat.github.io/later/parsers.html#text) text — can be things like `at 5:00 pm` or `every 2 hours`. + +See [Later text parsing documentation](http://bunkat.github.io/later/parsers.html#text) for more information. + +#### Example Schedule Configuration + +```hjson +{ + scannerTossers: { + ftn_bso: { + schedule: { + import: every 1 hours or @watch:/path/to/watchfile.ext + export: every 1 hours or @immediate + } + } + } +} +``` + +### A More Complete Example Below is a more complete example showing the sections described above. ```hjson @@ -149,7 +149,7 @@ do done ``` -Now, create an Event Scheuler entry in your `config.hjson`. As an example: +Now, create an Event Scheduler entry in your `config.hjson`. As an example: ```hjson eventScheduler: { events: { @@ -163,4 +163,4 @@ eventScheduler: { ``` ## Additional Resources -* [Blog entry on setting up ENiGMA + Binkd on CentOS7](https://l33t.codes/enigma-12-binkd-on-centos-7/). Note that this references an **older version**, so be wary of the `config.hjson` refernces! +[Blog entry on setting up ENiGMA + Binkd on CentOS7](https://l33t.codes/enigma-12-binkd-on-centos-7/). Note that this references an **older version**, so be wary of the `config.hjson` references! diff --git a/docs/messageareas/ftn.md b/docs/messageareas/ftn.md new file mode 100644 index 00000000..34f90862 --- /dev/null +++ b/docs/messageareas/ftn.md @@ -0,0 +1,105 @@ +--- +layout: page +title: FidoNet-Style Networks (FTN) +--- + +## FidoNet-Style Networks (FTN) +[FidoNet](https://en.wikipedia.org/wiki/FidoNet) proper and other FidoNet-Style networks are supported by ENiGMA½. A bit of configuration and you'll be up and running in no time! + +### Configuration +Getting a fully running FTN enabled system requires a few configuration points: + +1. `messageNetworks.ftn.networks`: Declares available networks. That is, networks you wish to sync up with. +2. `messageNetworks.ftn.areas`: Establishes local area mappings (ENiGMA½ to/from FTN area tags) and per-area specific configurations. +3. `scannerTossers.ftn_bso`: General configuration for the scanner/tosser (import/export) process. This is also where we configure per-node (uplink) settings. + +:information_source: ENiGMA½'s `ftn_bso` module is **not a mailer** and makes **no attempts** to perform packet transport! An external utility such as Binkd is required for this task. + +#### Networks +The `networks` block is a per-network configuration where each entry's ID (or "key") may be referenced elsewhere in `config.hjson`. For example, consider two networks: ArakNet (`araknet`) and fsxNet (`fsxnet`): + +```hjson +{ + messageNetworks: { + ftn: { + networks: { + // it is recommended to use lowercase network tags + fsxnet: { + defaultZone: 21 + localAddress: "21:1/121" + } + + araknet: { + defaultZone: 10 + localAddress: "10:101/9" + } + } + } + } +} +``` + +#### Areas +The `areas` section describes a mapping of local **area tags** configured in your `messageConferences` (see [Configuring a Message Area](configuring-a-message-area.md)) to a message network (described above), a FTN specific area tag, and remote uplink address(s). This section can be thought of similar to the *AREAS.BBS* file used by other BBS packages. + +When ENiGMA½ imports messages, they will be placed in the local area that matches key under `areas` while exported messages will be sent to the relevant `network`. + +| Config Item | Required | Description | +|-------------|----------|----------------------------------------------------------| +| `network` | :+1: | Associated network from the `networks` section above | +| `tag` | :+1: | FTN area tag (ie: `FSX_GEN`) | +| `uplinks` | :+1: | An array of FTN address uplink(s) for this network | + +Example: +```hjson +{ + messageNetworks: { + ftn: { + areas: { + // it is recommended to use lowercase area tags + fsx_general: // *local* tag found within messageConferences + network: fsxnet // that we are mapping to this network + tag: FSX_GEN // ...and this remote FTN-specific tag + uplinks: [ "21:1/100" ] // a single string also allowed here + } + } + } + } +} +``` + +:information_source: You can import `AREAS.BBS` or FTN style `.NA` files using [oputil](/docs/admin/oputil.md)! + +#### A More Complete Example +Below is a more complete *example* illustrating some of the concepts above: + +```hjson +{ + messageNetworks: { + ftn: { + networks: { + fsxnet: { + defaultZone: 21 + localAddress: "21:1/121" + } + } + + areas: { + fsx_general: { + network: fsxnet + + // ie as found in your info packs .NA file + tag: FSX_GEN + + uplinks: [ "21:1/100" ] + } + } + } + } +} +``` + +:information_source: Remember for a complete FTN experience, you'll probably also want to configure [FTN/BSO scanner/tosser](bso-import-export.md) settings. + +#### FTN/BSO Scanner Tosser +Please see the [FTN/BSO Scanner/Tosser](bso-import-export.md) documentation for information on this area. \ No newline at end of file diff --git a/docs/messageareas/message-networks.md b/docs/messageareas/message-networks.md index 5838da53..52a4227e 100644 --- a/docs/messageareas/message-networks.md +++ b/docs/messageareas/message-networks.md @@ -3,103 +3,22 @@ layout: page title: Message Networks --- ## Message Networks -ENiGMA½ considers all non-ENiGMA½, non-local messages (and their networks, such as FTN "external". That is, messages are only imported and exported from/to such a networks. Configuring such external message networks in ENiGMA½ requires three sections in your `config.hjson`. +ENiGMA½ supports external networks such as FidoNet-Style (FTN) and QWK by the way of importing and exporting to/from it's own internal format. This allows for a very flexible system that can easily be extended by creating new network modules. -1. `messageNetworks..networks`: declares available networks. -2. `messageNetworks..areas`: establishes local area mappings and per-area specifics. -3. `scannerTossers.`: general configuration for the scanner/tosser (import/export). This is also where we configure per-node settings. +All message network configuration occurs under the `messageNetworks.` block in `config.hjson` (where name is something such as `ftn` or `qwk`). The most basic of external message network configurations generally comprises of two sections: -### FTN Networks +1. `messageNetworks..networks`: Global/general configuration for a particular network where `` is for example `ftn` or `qwk`. +2. `messageNetworks..areas`: Provides mapping of ENiGMA½ **area tags** to their external counterparts. + +:information_source: A related section under `scannerTossers.` may provide configuration for scanning (importing) and tossing (exporting) messages for a particular network type. As an example, FidoNet-Style networks often work with BinkleyTerm Style Outbound (BSO) and thus the [FTN/BSO scanner/tosser](bso-import-export.md) (`ftn_bso`) module. + +### Currently Supported Networks +The following networks are supported out of the box. Remember that you can create modules to add others if desired! + +#### FidoNet-Style (FTN) FidoNet and FidoNet style (FTN) networks as well as a [FTN/BSO scanner/tosser](bso-import-export.md) (`ftn_bso` module) are configured via the `messageNetworks.ftn` and `scannerTossers.ftn_bso` blocks in `config.hjson`. -:information_source: ENiGMA½'s `ftn_bso` module is **not a mailer** and makes **no attempts** to perform packet transport! An external utility such as Binkd is required for this! +See [FidoNet-Style Networks](ftn.md) for more information. -#### Networks -The `networks` block a per-network configuration where each entry's key may be referenced elsewhere in `config.hjson`. - -Example: the following example declares two networks: `araknet` and `fsxnet`: -```hjson -{ - messageNetworks: { - ftn: { - networks: { - // it is recommended to use lowercase network tags - fsxnet: { - defaultZone: 21 - localAddress: "21:1/121" - } - - araknet: { - defaultZone: 10 - localAddress: "10:101/9" - } - } - } - } -} -``` - -#### Areas -The `areas` section describes a mapping of local **area tags** configured in your `messageConferences` (see [Configuring a Message Area](configuring-a-message-area.md)) to a message network (described above), a FTN specific area tag, and remote uplink address(s). This section can be thought of similar to the *AREAS.BBS* file used by other BBS packages. - -When ENiGMA½ imports messages, they will be placed in the local area that matches key under `areas` while exported messages will be sent to the relevant `network`. - -| Config Item | Required | Description | -|-------------|----------|----------------------------------------------------------| -| `network` | :+1: | Associated network from the `networks` section above | -| `tag` | :+1: | FTN area tag (ie: `FSX_GEN`) | -| `uplinks` | :+1: | An array of FTN address uplink(s) for this network | - -Example: -```hjson -{ - messageNetworks: { - ftn: { - areas: { - // it is recommended to use lowercase area tags - fsx_general: // *local* tag found within messageConferences - network: fsxnet // that we are mapping to this network - tag: FSX_GEN // ...and this remote FTN-specific tag - uplinks: [ "21:1/100" ] // a single string also allowed here - } - } - } - } -} -``` - -:information_source: You can import `AREAS.BBS` or FTN style `.NA` files using [oputil](/docs/admin/oputil.md)! - -### A More Complete Example -Below is a more complete *example* illustrating some of the concepts above: - -```hjson -{ - messageNetworks: { - ftn: { - networks: { - fsxnet: { - defaultZone: 21 - localAddress: "21:1/121" - } - } - - areas: { - fsx_general: { - network: fsxnet - - // ie as found in your info packs .NA file - tag: FSX_GEN - - uplinks: [ "21:1/100" ] - } - } - } - } -} -``` - -:information_source: Remember for a complete FTN experience, you'll probably also want to configure [FTN/BSO scanner/tosser](bso-import-export.md) settings. - -### FTN/BSO Scanner Tosser -Please see the [FTN/BSO Scanner/Tosser](bso-import-export.md) documentation for information on this area. +#### QWK +See [QWK and QWK-Net Style Networks](qwk.md) for more information. diff --git a/docs/messageareas/qwk.md b/docs/messageareas/qwk.md new file mode 100644 index 00000000..964551a3 --- /dev/null +++ b/docs/messageareas/qwk.md @@ -0,0 +1,47 @@ +--- +layout: page +title: QWK Support +--- + +## QWK and QWK-Net Style Networks +As like all other networks such as FidoNet-Style (FTN) networks, ENiGMA½ considers QWK external to the system but can import and export the format. + +### Supported Standards +QWK must be considered a semi-standard as there are many implementations. What follows is a short & incomplete list of such standards ENiGMA½ supports: +* The basic [QWK packet format](http://fileformats.archiveteam.org/wiki/QWK). +* [QWKE extensions](https://github.com/wwivbbs/wwiv/blob/master/specs/qwk/qwke.txt). +* [Synchronet BBS style extensions](http://wiki.synchro.net/ref:qwk) such as `HEADERS.DAT`, `@` kludges, and UTF-8 handling. + + +### Configuration +QWK configuration occurs in the `messageNetworks.qwk` config block of `config.hjson`. As QWK wants to deal with conference numbers and ENiGMA½ uses area tags (conferences and conference tags are only used for logical grouping), a mapping can be made. + +:information_source: During a regular, non QWK-Net exports, conference numbers can be auto-generated. Note that for QWK-Net style networks, you will need to create mappings however. + +Example: +```hjson +{ + messageNetworks: { + qwk: { + areas: { + general: { // local ENiGMA½ area tag + conference: 1 // conference number to map to + } + } + } + } +} +``` + +### oputil +The `oputil.js` utility can export packet files, dump the messages of a packet to stdout, etc. See [the oputil documentation](/docs/admin/oputil.md) for more information. + +### Offline Readers +A few of the offline readers that have been tested with QWK packet files produced by ENiGMA½: + +| Software | Status | Notes | +|----------|--------|-------| +| MultiMail/Win v0.52 | Supported | Private mail seems to break even with bundles from other systems | +| SkyReader/W32 v1.00 | Supported | Works well. No QWKE or HEADERS.DAT support. Gets confused with low conference numbers. | + +There are also [many other readers](https://www.softwolves.pp.se/old/2000/faq/bwprod) for various systems. \ No newline at end of file diff --git a/docs/modding/local-doors.md b/docs/modding/local-doors.md index 4ab8037b..2a4a1338 100644 --- a/docs/modding/local-doors.md +++ b/docs/modding/local-doors.md @@ -3,7 +3,7 @@ layout: page title: Local Doors --- ## Local Doors -ENiGMA½ has many ways to add doors to your system. In addition to the many built in door server modules, local doors are of course also supported using the ! The `abracadabra` module! +ENiGMA½ has many ways to add doors to your system. In addition to the [many built in door server modules](door-servers.md), local doors are of course also supported using the ! The `abracadabra` module! ## The abracadabra Module The `abracadabra` module provides a generic and flexible solution for many door types. Through this module you can execute native processes & scripts directly, and perform I/O through standard I/O (stdio) or a temporary TCP server. @@ -17,7 +17,7 @@ The `abracadabra` `config` block can contain the following members: | `dropFileType` | :+1: | Specifies the type of dropfile to generate (See **Dropfile Types** below). | | `cmd` | :+1: | Path to executable to launch. | | `args` | :-1: | Array of argument(s) to pass to `cmd`. See **Argument Variables** below for information on variables that can be used here. -| `cwd` | :-1: | Sets the Current Working Directory (CWD) for `cmd`. Defaults to the directory of `cmd`. | +| `cwd` | :-1: | Sets the Current Working Directory (CWD) for `cmd`. Defaults to the directory of `cmd`. | | `nodeMax` | :-1: | Max number of nodes that can access this door at once. Uses `name` as a tracking key. | | `tooManyArt` | :-1: | Art spec to display if too many instances are already in use. | | `io` | :-1: | How to process input/output (I/O). Can be `stdio` or `socket`. When using `stdio`, I/O is handled via standard stdin/stdout. When using `socket` a temporary socket server is spawned that can be connected back to. The server listens on localhost on `{srvPort}` (See **Argument Variables** below for more information). Default value is `stdio`. | @@ -99,8 +99,8 @@ doorPimpWars: { cmd: /usr/bin/dosemu args: [ "-quiet", - "-f", - "/path/to/dosemu.conf", + "-f", + "/path/to/dosemu.conf", "X:\\PW\\START.BAT {dropFile} {node}" ], nodeMax: 1 @@ -149,7 +149,7 @@ Please see the [bivrost!](https://github.com/NuSkooler/bivrost) documentation fo Pre-built binaries of bivrost! have been released under [Phenom Productions](https://www.phenomprod.com/) and can be found on various boards. #### Alternative Workarounds -Alternative workarounds include Telnet Bridge (`telnet_bridge` module) to hook up Telnet-accessible (including local) door servers -- It may also be possible bridge via [NET2BBS](http://pcmicro.com/netfoss/guide/net2bbs.html). +Alternative workarounds include [Telnet Bridge module](telnet-bridge.md) to hook up Telnet-accessible (including local) door servers -- It may also be possible bridge via [NET2BBS](http://pcmicro.com/netfoss/guide/net2bbs.html). ### QEMU with abracadabra [QEMU](http://wiki.qemu.org/Main_Page) provides a robust, cross platform solution for launching doors under many platforms (likely anywhere Node.js is supported and ENiGMA½ can run). Note however that there is an important and major caveat: **Multiple instances of a particular door/OS image should not be run at once!** Being more flexible means being a bit more complex. Let's look at an example for running L.O.R.D. under a UNIX like system such as Linux or FreeBSD. @@ -223,8 +223,13 @@ doorLORD: { } ``` +## See Also +* [Telnet Bridge](telnet-bridge.md) +* [Door Servers](door-servers.md) + ## Additional Resources -### DOSBox +### DOS Emulation +* [DOSEMU](http://www.dosemu.org/) * [DOSBox-X](https://github.com/joncampbell123/dosbox-x) ### Door Downloads & Support Sites @@ -233,4 +238,4 @@ doorLORD: { * http://bbstorrents.bbses.info/ #### L.O.R.D. -* http://lord.lordlegacy.com/ \ No newline at end of file +* http://lord.lordlegacy.com/ diff --git a/docs/modding/show-art.md b/docs/modding/show-art.md index c00d7009..5ffce10c 100644 --- a/docs/modding/show-art.md +++ b/docs/modding/show-art.md @@ -29,8 +29,8 @@ showWithExtraArgs: { If the `showWithExtraArgs` menu was entered and passed `extraArgs` as the following: ```json { - fizzBang : true, - fooBaz : "LOLART" + "fizzBang" : true, + "fooBaz" : "LOLART" } ``` diff --git a/docs/modding/telnet-bridge.md b/docs/modding/telnet-bridge.md new file mode 100644 index 00000000..36e1b3e6 --- /dev/null +++ b/docs/modding/telnet-bridge.md @@ -0,0 +1,96 @@ +--- +layout: page +title: Telnet Bridge +--- +## Telnet Bridge +The `telnet_bridge` module allows "bridged" Telnet connections from your board to other Telnet services (such as other BBSes!). + +## Configuration +### Config Block +Available `config` entries: +* `host`: Hostname or IP address to connect to. +* `port`: Port to connect to. Defaults to the standard Telnet port of `23`. +* `font`: A SyncTERM style font. Useful for example if you would like to connect form a "DOS" style BBS to an Amiga. See [the general art documentation on SyncTERM Style Fonts](/docs/art/general.md). + +### Example +Below is an example `menu.hjson` entry that would connect to [Xibalba](https://xibalba.l33t.codes): + +```hjson +{ + telnetBridgeXibalba: { + desc: Xibalba BBS + module: telnet_bridge + config: { + host: xibalba.l33t.codes + port: 45510 + } + } +} +``` + +### Using Extra Args +The `telnet_bridge` module can also accept standard `extraArgs` of the same configuration arguments described above. This can be illustrated with an example: + +```hjson +telnetBridgeMenu: { + desc: Telnet Bridge + art: telnet_bridge + config: { + font: cp437 + } + form: { + 0: { + mci: { + VM1: { + argName: selection + + items: [ + { + board: BLACK Flag + soft: Mystic + data: bf + } + { + board: Xibalba + soft: ENiGMA½ + data: xib + } + ] + + // sort by 'board' fields above + sort: board + submit: true + } + } + + submit: { + *: [ + { + value: { "selection" : "bf" } + action: @menu:telnetBridgeFromExtraFlags + extraArgs: { + host: blackflag.acid.org + } + } + { + value: { "selection" : "xib" } + action: @menu:telnetBridgeFromExtraFlags + extraArgs: { + host: xibalba.l33t.codes + port: 44510 + } + } + ] + } + } + } +} + +telnetBridgeFromExtraFlags: { + desc: Telnet Bridge + module: telnet_bridge +} +``` + +Here we've created a lightbar menu with custom items in which we'd use `itemFormat`'s with in a theme. When the user selects an item, the `telnetBridgeFromExtraFlags` menu is instantiated using the supplied `extraArgs`. + diff --git a/docs/servers/ssh.md b/docs/servers/ssh.md index c576f38e..2f8b7769 100644 --- a/docs/servers/ssh.md +++ b/docs/servers/ssh.md @@ -15,7 +15,8 @@ Entries available under `config.loginServers.ssh`: | `firstMenu` | :-1: | First menu an SSH connected user is presented with. Defaults to `sshConnected`. | | `firstMenuNewUser` | :-1: | Menu presented to user when logging in with one of the usernames found within `users.newUserNames` in your `config.hjson`. Examples include `new` and `apply`. | | `enabled` | :+1: | Set to `true` to enable the SSH server. | -| `port` | :-1: | Override the default port of `8443`. | +| `port` | :-1: | Override the default port of `8443`. | +| `address` | :-1: | Sets an explicit bind address. | | `algorithms` | :-1: | Configuration block for SSH algorithms. Includes keys of `kex`, `cipher`, `hmac`, and `compress`. See the algorithms section in the [ssh2-streams](https://github.com/mscdex/ssh2-streams#ssh2stream-methods) documentation for details. For defaults set by ENiGMA½, see `core/config.js`. | `traceConnections` | :-1: | Set to `true` to enable full trace-level information on SSH connections. @@ -29,7 +30,7 @@ Entries available under `config.loginServers.ssh`: port: 8889 privateKeyPem: /path/to/ssh_private_key.pem privateKeyPass: sup3rs3kr3tpa55 - } + } } } ``` diff --git a/docs/servers/telnet.md b/docs/servers/telnet.md index ccefc966..fb4baddb 100644 --- a/docs/servers/telnet.md +++ b/docs/servers/telnet.md @@ -8,10 +8,11 @@ The Telnet *login server* provides a standard **non-secure** Telnet login experi ## Configuration The following configuration can be made in `config.hjson` under the `loginServers.telnet` block: -| Item | Required | Description | +| Key | Required | Description | |------|----------|-------------| | `enabled` | :-1: Defaults to `true`. Set to `false` to disable Telnet | | `port` | :-1: | Override the default port of `8888`. | +| `address` | :-1: | Sets an explicit bind address. | | `firstMenu` | :-1: | First menu a telnet connected user is presented with. Defaults to `telnetConnected`. | ### Example Configuration @@ -21,7 +22,7 @@ The following configuration can be made in `config.hjson` under the `loginServer telnet: { enabled: true port: 8888 - } + } } } ``` diff --git a/docs/servers/web-server.md b/docs/servers/web-server.md index 816c28a2..5675c4c6 100644 --- a/docs/servers/web-server.md +++ b/docs/servers/web-server.md @@ -2,13 +2,10 @@ layout: page title: Web Server --- -ENiGMA½ comes with a built in *content server* for supporting both HTTP and HTTPS. Currently the -[File Bases](file_base.md) registers routes for file downloads, and static files can also be served -for your BBS. Other features will likely come in the future or you can easily write your own! +ENiGMA½ comes with a built in *content server* for supporting both HTTP and HTTPS. Currently the [File Bases](file_base.md) registers routes for file downloads, password reset email links are handled via the server, and static files can also be served for your BBS. Other features will likely come in the future or you can easily write your own! -## Configuration -By default the web server is not enabled. To enable it, you will need to at a minimum configure two keys in -the `contentServers::web` section of `config.hjson`: +# Configuration +By default the web server is not enabled. To enable it, you will need to at a minimum configure two keys in the `contentServers.web` section of `config.hjson`: ```hjson contentServers: { @@ -17,39 +14,44 @@ contentServers: { http: { enabled: true + port: 8080 } } } ``` -This will configure HTTP for port 8080 (override with `port`). To additionally enable HTTPS, you will need a -PEM encoded SSL certificate and private key. [LetsEncrypt](https://letsencrypt.org/) supply free trusted -certificates that work perfectly with ENiGMA½. +The following is a table of all configuration keys available under `contentServers.web`: +| Key | Required | Description | +|------|----------|-------------| +| `domain` | :+1: | Sets the domain, e.g. `bbs.yourdomain.com`. | +| `http` | :-1: | Sub configuration for HTTP (non-secure) connections. See **HTTP Configuration** below. | +| `overrideUrlPrefix` | :-1: | Instructs the system to be explicit when handing out URLs. Useful if your server is behind a transparent proxy. | -Once obtained, simply enable the HTTPS server: +### HTTP Configuration +Entries available under `contentServers.web.http`: -```hjson -contentServers: { - web: { - domain: bbs.yourdomain.com - // set 'overrideUrlPrefix' if for example, you use a transparent proxy in front of ENiGMA and need to be explicit about URLs the system hands out - overrideUrlPrefix: https://bbs.yourdomain.com - https: { - enabled: true - port: 8443 - certPem: /path/to/your/cert.pem - keyPem: /path/to/your/cert_private_key.pem - } - } -} -``` +| Key | Required | Description | +|------|----------|-------------| +| `enable` | :+1: | Set to `true` to enable this server. +| `port` | :-1: | Override the default port of `8080`. | +| `address` | :-1: | Sets an explicit bind address. | -If no certificate paths are supplied, ENiGMA½ will assume the defaults of `/config/https_cert.pem` and -`/config/https_cert_key.pem` accordingly. +### HTTPS Configuration +Entries available under `contentServers.web.https`: -### Static Routes -Static files live relative to the `contentServers::web::staticRoot` path which defaults to `enigma-bbs/www`. +| Key | Required | Description | +|------|----------|-------------| +| `enable` | :+1: | Set to `true` to enable this server. +| `port` | :-1: | Override the default port of `8080`. | +| `address` | :-1: | Sets an explicit bind address. | +| `certPem` | :+1: | Overrides the default certificate path of `/config/https_cert.pem`. Certificate must be in PEM format. See **Certificates** below. | +| `keyPem` | :+1: | Overrides the default certificate key path of `/config/https_cert_key.pem`. Key must be in PEM format. See **Certificates** below. | -### Custom Error Pages -Customized error pages can be created for [HTTP error codes](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_Client_Error) -by providing a `.html` file in the *static routes* area. For example: `404.html`. +#### Certificates +If you don't have a TLS certificate for your domain, a good source for a certificate can be [LetsEncrypt](https://letsencrypt.org/) who supplies free and trusted TLS certificates. + +## Static Routes +Static files live relative to the `contentServers.web.staticRoot` path which defaults to `enigma-bbs/www`. + +## Custom Error Pages +Customized error pages can be created for [HTTP error codes](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_Client_Error) by providing a `.html` file in the *static routes* area. For example: `404.html`. diff --git a/docs/troubleshooting/monitoring-logs.md b/docs/troubleshooting/monitoring-logs.md index 28a3773a..4fa27a5c 100644 --- a/docs/troubleshooting/monitoring-logs.md +++ b/docs/troubleshooting/monitoring-logs.md @@ -3,7 +3,7 @@ layout: page title: Monitoring Logs --- ## Monitoring Logs -ENiGMA½ does not produce much to stdout. Logs are produced by Bunyan which outputs each entry as a JSON object. +ENiGMA½ does not produce much to stdout. Logs are produced by [Bunyan](https://github.com/trentm/node-bunyan) which outputs each entry as a JSON object. Start by installing bunyan and making it available on your path: @@ -11,11 +11,11 @@ Start by installing bunyan and making it available on your path: npm install bunyan -g ``` -or with Yarn: +or via Yarn: ```bash yarn global add bunyan ``` - + To tail logs in a colorized and pretty format, issue the following command: ```bash tail -F /path/to/enigma-bbs/logs/enigma-bbs.log | bunyan diff --git a/misc/install.sh b/misc/install.sh index 5da8e0e4..ed81d205 100755 --- a/misc/install.sh +++ b/misc/install.sh @@ -2,7 +2,7 @@ { # this ensures the entire script is downloaded before execution -ENIGMA_NODE_VERSION=${ENIGMA_NODE_VERSION:=10} +ENIGMA_NODE_VERSION=${ENIGMA_NODE_VERSION:=12} ENIGMA_BRANCH=${ENIGMA_BRANCH:=master} ENIGMA_INSTALL_DIR=${ENIGMA_INSTALL_DIR:=$HOME/enigma-bbs} ENIGMA_SOURCE=${ENIGMA_SOURCE:=https://github.com/NuSkooler/enigma-bbs.git} diff --git a/package.json b/package.json index 0d9b23f0..aee75136 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "enigma-bbs", - "version": "0.0.10-alpha", + "version": "0.0.11-beta", "description": "ENiGMA½ Bulletin Board System", "author": "Bryan Ashby ", "license": "BSD-2-Clause", @@ -22,44 +22,46 @@ "retro" ], "dependencies": { - "async": "3.1.0", - "binary-parser": "^1.5.0", + "async": "3.2.0", + "binary-parser": "^1.6.2", "buffers": "github:NuSkooler/node-buffers", "bunyan": "^1.8.12", "exiftool": "^0.0.3", - "fs-extra": "8.1.0", + "fs-extra": "9.0.0", "glob": "7.1.6", - "graceful-fs": "^4.2.3", - "hashids": "2.1.0", + "graceful-fs": "^4.2.4", + "hashids": "2.2.1", "hjson": "^3.2.1", - "iconv-lite": "0.5.0", - "inquirer": "^7.0.0", + "iconv-lite": "0.5.1", + "ini-config-parser": "^1.0.4", + "inquirer": "^7.1.0", "later": "1.2.0", "lodash": "^4.17.15", "lru-cache": "^5.1.1", - "mime-types": "2.1.25", - "minimist": "1.2.0", - "moment": "^2.24.0", + "mime-types": "2.1.27", + "minimist": "1.2.5", + "moment": "^2.25.3", "nntp-server": "^1.0.3", "node-pty": "^0.9.0", - "nodemailer": "^6.3.1", + "nodemailer": "^6.4.6", "otplib": "11.0.1", "qrcode-generator": "^1.4.4", "rlogin": "^1.0.0", "sane": "4.1.0", "sanitize-filename": "^1.6.3", - "sqlite3": "^4.1.0", - "sqlite3-trans": "^1.2.1", - "ssh2": "0.8.6", + "sqlite3": "^4.2.0", + "sqlite3-trans": "^1.2.2", + "ssh2": "0.8.9", "temptmp": "^1.1.0", - "uuid": "^3.3.3", + "uuid": "^8.0.0", "uuid-parse": "1.1.0", - "ws": "^7.2.0", + "ws": "^7.3.0", "xxhash": "^0.3.0", - "yazl": "^2.5.1" + "yazl": "^2.5.1", + "telnet-socket" : "^0.2.3" }, "devDependencies": {}, "engines": { - "node": ">=8" + "node": ">=12" } } diff --git a/yarn.lock b/yarn.lock index b13f2369..0954c769 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,21 +10,16 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@types/color-name@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -ajv@^5.3.0: - version "5.5.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" - integrity sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU= - dependencies: - co "^4.6.0" - fast-deep-equal "^1.0.0" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.3.0" - ansi-escapes@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.2.1.tgz#4dccdb846c3eee10f6d64dea66273eab90c37228" @@ -47,12 +42,18 @@ ansi-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + +ansi-styles@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" + integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== dependencies: - color-convert "^1.9.0" + "@types/color-name" "^1.1.1" + color-convert "^2.0.1" anymatch@^2.0.0: version "2.0.0" @@ -107,53 +108,33 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= -asn1@~0.2.0, asn1@~0.2.3: +asn1@~0.2.0: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== dependencies: safer-buffer "~2.1.0" -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= - assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= -async-limiter@^1.0.0: +async@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" + integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== + +at-least-node@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" - integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg== - -async@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/async/-/async-3.1.0.tgz#42b3b12ae1b74927b5217d8c0016baaf62463772" - integrity sha512-4vx/aaY6j/j3Lw3fbCHNWP0pPaTCew3F6F3hYyl/tHs/ndmV1q7NW9T5yuJ2XAGwdQrP+6Wu20x06U4APo/iQQ== - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== atob@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= - -aws4@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" - integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== - balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -172,17 +153,17 @@ base@^0.11.1: mixin-deep "^1.2.0" pascalcase "^0.1.1" -bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2: +bcrypt-pbkdf@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= dependencies: tweetnacl "^0.14.3" -binary-parser@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/binary-parser/-/binary-parser-1.5.0.tgz#3e50de3a5076badbacd760e833e7d94892b9e9fa" - integrity sha512-z+hqNSnO7trFDPLihjUGTwlSTbcIzLYSCwnbiasFkRvCIY9F3ZTex7Mlm9UAP3w5mfHD3KxejnWFPJjtsVVMuw== +binary-parser@1.6.2, binary-parser@^1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/binary-parser/-/binary-parser-1.6.2.tgz#8410a82ffd9403271ec182bd91e63a09cee88cbe" + integrity sha512-cYAhKB51A9T/uylDvMK7uAYaPLWLwlferNOpnQ0E0fuO73yPi7kWaWiOm22BvuKxCbggmkiFN0VkuLg6gc+KQQ== brace-expansion@^1.1.7: version "1.1.11" @@ -256,19 +237,13 @@ capture-exit@^2.0.0: dependencies: rsvp "^4.8.4" -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= - -chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" + ansi-styles "^4.1.0" + supports-color "^7.1.0" chardet@^0.7.0: version "0.7.0" @@ -302,16 +277,16 @@ cli-width@^2.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= - code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= +coffee-script@^1.12.4: + version "1.12.7" + resolved "https://registry.yarnpkg.com/coffee-script/-/coffee-script-1.12.7.tgz#c05dae0cb79591d05b3070a8433a98c9a89ccc53" + integrity sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw== + collection-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" @@ -320,31 +295,17 @@ collection-visit@^1.0.0: map-visit "^1.0.0" object-visit "^1.0.0" -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== dependencies: - color-name "1.1.3" + color-name "~1.1.4" -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -combined-stream@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" - integrity sha1-cj599ugBrFYTETp+RFqbactjKBg= - dependencies: - delayed-stream "~1.0.0" - -combined-stream@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" - integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w== - dependencies: - delayed-stream "~1.0.0" +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== component-emitter@^1.2.1: version "1.2.1" @@ -366,7 +327,7 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= -core-util-is@1.0.2, core-util-is@~1.0.0: +core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= @@ -382,13 +343,6 @@ cross-spawn@^6.0.0: shebang-command "^1.2.0" which "^1.2.9" -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= - dependencies: - assert-plus "^1.0.0" - debug@^2.1.2, debug@^2.2.0, debug@^2.3.3: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -408,6 +362,11 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= +deep-extend@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.5.1.tgz#b894a9dd90d3023fbf1c55a394fb858eb2066f1f" + integrity sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w== + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -447,11 +406,6 @@ del@^3.0.0: pify "^3.0.0" rimraf "^2.2.8" -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= - delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" @@ -479,14 +433,6 @@ dtrace-provider@~0.8: dependencies: nan "^2.10.0" -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -555,11 +501,6 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - external-editor@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.3.tgz#5866db29a97826dbe4bf3afd24070ead9ea43a27" @@ -583,26 +524,6 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= - -extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= - -fast-deep-equal@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" - integrity sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ= - -fast-json-stable-stringify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" - integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= - fb-watchman@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" @@ -632,20 +553,6 @@ for-in@^1.0.2: resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= - -form-data@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" - integrity sha1-SXBJi+YEwgwAXU9cI67NIda0kJk= - dependencies: - asynckit "^0.4.0" - combined-stream "1.0.6" - mime-types "^2.1.12" - fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -661,14 +568,15 @@ from2@^2.3.0: inherits "^2.0.1" readable-stream "^2.0.0" -fs-extra@8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" - integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== +fs-extra@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.0.tgz#b6afc31036e247b2466dc99c29ae797d5d4580a3" + integrity sha512-pmEYSk3vYsG/bF651KPUXZ+hvjpgWYw/Gc7W9NFUe3ZVLczKKWIij3IKpOrQcdw4TILtibFslZ0UmR8Vvzig4g== dependencies: + at-least-node "^1.0.0" graceful-fs "^4.2.0" - jsonfile "^4.0.0" - universalify "^0.1.0" + jsonfile "^6.0.1" + universalify "^1.0.0" fs-minipass@^1.2.5: version "1.2.5" @@ -708,13 +616,6 @@ get-value@^2.0.3, get-value@^2.0.6: resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= - dependencies: - assert-plus "^1.0.0" - glob@7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -771,28 +672,15 @@ graceful-fs@^4.2.0: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.0.tgz#8d8fdc73977cb04104721cb53666c1ca64cd328b" integrity sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg== -graceful-fs@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" - integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== +graceful-fs@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" + integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= - -har-validator@~5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.0.tgz#44657f5688a22cfd4b72486e81b3a3fb11742c29" - integrity sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA== - dependencies: - ajv "^5.3.0" - har-schema "^2.0.0" - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== has-unicode@^2.0.0: version "2.0.1" @@ -830,29 +718,20 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" -hashids@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/hashids/-/hashids-2.1.0.tgz#96eec6de3081a76dcd839650a6a26e6081e729d3" - integrity sha512-N53K2p7TrwKLNHKHcEDH+qpiAgO9JfyPEg8Tfy4fB9AcVhwxlTanJ55HVV9BQJQ6ajM1Wfmtl2wgKuEbcucolw== +hashids@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/hashids/-/hashids-2.2.1.tgz#ad0c600f0083aa0df7451dfd184e53db34f71289" + integrity sha512-+hQeKWwpSDiWFeu/3jKUvwboE4Z035gR6FnpscbHPOEEjCbgv2px9/Mlb3O0nOTRyZOw4MMFRYfVL3zctOV6OQ== hjson@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/hjson/-/hjson-3.2.1.tgz#20de41dc87fc9a10d1557d0230b0e02afb1b09ac" integrity sha512-OhhrFMeC7dVuA1xvxuXGTv/yTdhTvbe8hz+3LgVNsfi9+vgz0sF/RrkuX8eegpKaMc9cwYwydImBH6iePoJtdQ== -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -iconv-lite@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.5.0.tgz#59cdde0a2a297cc2aeb0c6445a195ee89f127550" - integrity sha512-NnEhI9hIEKHOzJ4f697DMz9IQEXr/MMJ5w64vN2/4Ai+wRnvV7SBrL0KLoRlwaKVghOc7LQ5YkPLuX146b6Ydw== +iconv-lite@0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.5.1.tgz#b2425d3c7b18f7219f2ca663d103bddb91718d64" + integrity sha512-ONHr16SQvKZNSqjQT9gy5z24Jw+uqfO02/ngBSBoqChZ+W8qXX7GPRa1RoUnzGADw8K63R1BXUMzarCVQBpY8Q== dependencies: safer-buffer ">= 2.1.2 < 3" @@ -883,28 +762,37 @@ inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +ini-config-parser@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/ini-config-parser/-/ini-config-parser-1.0.4.tgz#0abc75cb68c506204712d2b4861400b6adbfda78" + integrity sha512-5hLh5Cqai67pTrLQ9q/K/3EtSP2Tzu41AZzwPLSegkkMkc42dGweLgkbiocCBiBBEg2fPhs6pKmdFhwj5Ul3Bg== + dependencies: + coffee-script "^1.12.4" + deep-extend "^0.5.1" + rimraf "^2.6.1" + ini@~1.3.0: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== -inquirer@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.0.tgz#9e2b032dde77da1db5db804758b8fea3a970519a" - integrity sha512-rSdC7zelHdRQFkWnhsMu2+2SO41mpv2oF2zy4tMhmiLWkcKbOAs87fWAJhVXttKVwhdZvymvnuM95EyEXg2/tQ== +inquirer@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.1.0.tgz#1298a01859883e17c7264b82870ae1034f92dd29" + integrity sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg== dependencies: ansi-escapes "^4.2.1" - chalk "^2.4.2" + chalk "^3.0.0" cli-cursor "^3.1.0" cli-width "^2.0.0" external-editor "^3.0.3" figures "^3.0.0" lodash "^4.17.15" mute-stream "0.0.8" - run-async "^2.2.0" - rxjs "^6.4.0" + run-async "^2.4.0" + rxjs "^6.5.3" string-width "^4.1.0" - strip-ansi "^5.1.0" + strip-ansi "^6.0.0" through "^2.3.6" is-accessor-descriptor@^0.1.6: @@ -1020,21 +908,11 @@ is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-promise@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" - integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= - is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= - is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -1062,48 +940,15 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= - -json-schema-traverse@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" - integrity sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A= - -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= - -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= - -jsonfile@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" - integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= +jsonfile@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.0.1.tgz#98966cba214378c8c84b82e085907b40bf614179" + integrity sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg== + dependencies: + universalify "^1.0.0" optionalDependencies: graceful-fs "^4.1.6" -jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.2.3" - verror "1.10.0" - kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -1138,11 +983,6 @@ lodash@^4.17.15: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== -lodash@^4.17.4: - version "4.17.11" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" - integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== - lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -1188,29 +1028,17 @@ micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" -mime-db@1.42.0: - version "1.42.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.42.0.tgz#3e252907b4c7adb906597b4b65636272cf9e7bac" - integrity sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ== +mime-db@1.44.0: + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== -mime-db@~1.36.0: - version "1.36.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397" - integrity sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw== - -mime-types@2.1.25: - version "2.1.25" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.25.tgz#39772d46621f93e2a80a856c53b86a62156a6437" - integrity sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg== +mime-types@2.1.27: + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== dependencies: - mime-db "1.42.0" - -mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.20" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19" - integrity sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A== - dependencies: - mime-db "~1.36.0" + mime-db "1.44.0" mimic-fn@^2.1.0: version "2.1.0" @@ -1229,7 +1057,12 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= -minimist@1.2.0, minimist@^1.1.1, minimist@^1.2.0: +minimist@1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +minimist@^1.1.1, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= @@ -1269,10 +1102,10 @@ moment@^2.10.6: resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" integrity sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y= -moment@^2.24.0: - version "2.24.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" - integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== +moment@^2.25.3: + version "2.25.3" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.25.3.tgz#252ff41319cf41e47761a1a88cab30edfe9808c0" + integrity sha512-PuYv0PHxZvzc15Sp8ybUCoQ+xpyPWvjOuK72a5ovzp2LI32rJXOiIfyoFoYvG3s6EwwrdkMyWuRiEHSZRLJNdg== ms@2.0.0: version "2.0.0" @@ -1388,10 +1221,10 @@ node-pty@^0.9.0: dependencies: nan "^2.14.0" -nodemailer@^6.3.1: - version "6.3.1" - resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.3.1.tgz#2784beebac6b9f014c424c54dbdcc5c4d1221346" - integrity sha512-j0BsSyaMlyadEDEypK/F+xlne2K5m6wzPYMXS/yxKI0s7jmT1kBx6GEKRVbZmyYfKOsjkeC/TiMVDJBI/w5gMQ== +nodemailer@^6.4.6: + version "6.4.6" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.4.6.tgz#d37f504f6560b36616f646a606894fe18819107f" + integrity sha512-/kJ+FYVEm2HuUlw87hjSqTss+GU35D4giOpdSfGp7DO+5h6RlJj7R94YaYHOkoxu1CSaM0d3WRBtCzwXrY6MKA== nopt@^4.0.1: version "4.0.1" @@ -1443,11 +1276,6 @@ number-is-nan@^1.0.0: resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - object-assign@^4.0.1, object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -1545,11 +1373,6 @@ path-key@^2.0.0, path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= - pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -1582,11 +1405,6 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== -psl@^1.1.24: - version "1.1.29" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67" - integrity sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ== - pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -1595,21 +1413,11 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - qrcode-generator@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/qrcode-generator/-/qrcode-generator-1.4.4.tgz#63f771224854759329a99048806a53ed278740e7" integrity sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw== -qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== - rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -1665,32 +1473,6 @@ repeat-string@^1.6.1: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= -request@^2.87.0: - version "2.88.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" - integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.0" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.4.3" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -1733,21 +1515,19 @@ rsvp@^4.8.4: resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== -run-async@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" - integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA= - dependencies: - is-promise "^2.1.0" +run-async@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== -rxjs@^6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.4.0.tgz#f3bb0fe7bda7fb69deac0c16f17b50b0b8790504" - integrity sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw== +rxjs@^6.5.3: + version "6.5.5" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec" + integrity sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ== dependencies: tslib "^1.9.0" -safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== @@ -1764,7 +1544,7 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3", safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -1918,53 +1698,36 @@ split2@^3.0.0: dependencies: readable-stream "^3.0.0" -sqlite3-trans@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/sqlite3-trans/-/sqlite3-trans-1.2.1.tgz#642dff9f6da53d533ccd264b49e68c8818542255" - integrity sha512-KLtR+PBZN/moxDTKWTwWypkunDCJ0oi5vknjht8omjUXswwUEf+MX2DKtgQB1V5Tsjgc4mL4mHjv9zp7+FHs5g== +sqlite3-trans@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/sqlite3-trans/-/sqlite3-trans-1.2.2.tgz#faf268cc8d04dfd1a4854d64a70a229bdb50609f" + integrity sha512-+c2je0JMgPeNYHM7vMwEv/nHqOMYa5NNgQDcUyFkVMJ5QHATOQ+GywJptlVbkRCjgSTctmighfWLwUHPlkXbSQ== dependencies: - lodash "^4.17.4" + lodash "^4.17.15" -sqlite3@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-4.1.0.tgz#e051fb9c133be15726322a69e2e37ec560368380" - integrity sha512-RvqoKxq+8pDHsJo7aXxsFR18i+dU2Wp5o12qAJOV5LNcDt+fgJsc2QKKg3sIRfXrN9ZjzY1T7SNe/DFVqAXjaw== +sqlite3@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-4.2.0.tgz#49026d665e9fc4f922e56fb9711ba5b4c85c4901" + integrity sha512-roEOz41hxui2Q7uYnWsjMOTry6TcNUNmp8audCx18gF10P2NknwdpF+E+HKvz/F2NvPKGGBF4NGc+ZPQ+AABwg== dependencies: nan "^2.12.1" node-pre-gyp "^0.11.0" - request "^2.87.0" -ssh2-streams@~0.4.7: - version "0.4.7" - resolved "https://registry.yarnpkg.com/ssh2-streams/-/ssh2-streams-0.4.7.tgz#093b89069de9cf5f06feff0601a5301471b01611" - integrity sha512-JhF8BNfeguOqVHOLhXjzLlRKlUP8roAEhiT/y+NcBQCqpRUupLNrRf2M+549OPNVGx21KgKktug4P3MY/IvTig== +ssh2-streams@~0.4.10: + version "0.4.10" + resolved "https://registry.yarnpkg.com/ssh2-streams/-/ssh2-streams-0.4.10.tgz#48ef7e8a0e39d8f2921c30521d56dacb31d23a34" + integrity sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ== dependencies: asn1 "~0.2.0" bcrypt-pbkdf "^1.0.2" streamsearch "~0.1.2" -ssh2@0.8.6: - version "0.8.6" - resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-0.8.6.tgz#dcc62e1d3b9e58a21f711f5186f043e4e792e6da" - integrity sha512-T0cPmEtmtC8WxSupicFDjx3vVUdNXO8xu2a/D5bjt8ixOUCe387AgvxU3mJgEHpu7+Sq1ZYx4d3P2pl/yxMH+w== +ssh2@0.8.9: + version "0.8.9" + resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-0.8.9.tgz#54da3a6c4ba3daf0d8477a538a481326091815f3" + integrity sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw== dependencies: - ssh2-streams "~0.4.7" - -sshpk@^1.7.0: - version "1.14.2" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.2.tgz#c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98" - integrity sha1-xvxhZIo9nE52T9P8306hBeSSupg= - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - dashdash "^1.12.0" - getpass "^0.1.1" - safer-buffer "^2.0.2" - optionalDependencies: - bcrypt-pbkdf "^1.0.0" - ecc-jsbn "~0.1.1" - jsbn "~0.1.0" - tweetnacl "~0.14.0" + ssh2-streams "~0.4.10" static-extend@^0.1.1: version "0.1.2" @@ -2033,13 +1796,20 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@^5.1.0, strip-ansi@^5.2.0: +strip-ansi@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" @@ -2050,12 +1820,12 @@ strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== dependencies: - has-flag "^3.0.0" + has-flag "^4.0.0" tar@^4: version "4.4.6" @@ -2070,6 +1840,14 @@ tar@^4: safe-buffer "^5.1.2" yallist "^3.0.2" +telnet-socket@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/telnet-socket/-/telnet-socket-0.2.3.tgz#0ffdc64ea957cb64f8ac5287d45a857f1c05a16e" + integrity sha512-PbZycTkGq6VcVUa35FYFySx4pCzmJo4xoMX6cimls1/kv/lrgMfddKfgjBKt6HQuokkkDfieDhGLq/L/P2Unaw== + dependencies: + binary-parser "1.6.2" + buffers "github:NuSkooler/node-buffers" + temptmp@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/temptmp/-/temptmp-1.1.0.tgz#bfbbff858d7f7d59c563fbf069758a7775ecd431" @@ -2132,14 +1910,6 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" -tough-cookie@~2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" - integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== - dependencies: - psl "^1.1.24" - punycode "^1.4.1" - truncate-utf8-bytes@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b" @@ -2152,14 +1922,7 @@ tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: +tweetnacl@^0.14.3: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= @@ -2179,10 +1942,10 @@ union-value@^1.0.0: is-extendable "^0.1.1" set-value "^0.4.3" -universalify@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" - integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +universalify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" + integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== unset-value@^1.0.0: version "1.0.0" @@ -2217,24 +1980,10 @@ uuid-parse@1.1.0: resolved "https://registry.yarnpkg.com/uuid-parse/-/uuid-parse-1.1.0.tgz#7061c5a1384ae0e1f943c538094597e1b5f3a65b" integrity sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A== -uuid@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" - integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== - -uuid@^3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" - integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== - -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" +uuid@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c" + integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== walker@~1.0.5: version "1.0.7" @@ -2262,12 +2011,10 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -ws@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.0.tgz#422eda8c02a4b5dba7744ba66eebbd84bcef0ec7" - integrity sha512-+SqNqFbwTm/0DC18KYzIsMTnEWpLwJsiasW/O17la4iDRRIO9uaHbvKiAS3AHgTiuuWerK/brj4O6MYZkei9xg== - dependencies: - async-limiter "^1.0.0" +ws@^7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd" + integrity sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w== xtend@~4.0.1: version "4.0.1"