From 901628ccc9595dcbba8b55784bfa070b22aecc79 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 15 Feb 2019 15:22:21 -0700 Subject: [PATCH 001/140] Bump to 0.0.10-alpha to prepare for work --- README.md | 2 +- UPGRADE.md | 3 +++ WHATSNEW.md | 2 ++ docs/art/mci.md | 4 ++-- docs/installation/install-script.md | 4 ++-- package.json | 2 +- 6 files changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f297d060..167f5288 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,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.10-alpha/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 038c5c0a..215089ad 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -40,6 +40,9 @@ npm install Report your issue on Xibalba BBS, hop in #enigma-bbs on Freenet and chat, or [file a issue on GitHub](https://github.com/NuSkooler/enigma-bbs/issues). +# 0.0.9-alpha to 0.0.10-alpha + + # 0.0.8-alpha to 0.0.9-alpha * Development is now against Node.js 10.x LTS. Follow your standard upgrade path to update to Node 10.x before using 0.0.9-alpha! * The property `justify` found on various views previously had `left` and `right` values swapped (oops!); you will need to adjust any custom `theme.hjson` that use one or the other and swap them as well. diff --git a/WHATSNEW.md b/WHATSNEW.md index 39dfca49..31fe24c5 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -1,6 +1,8 @@ # Whats New This document attempts to track **major** changes and additions in ENiGMA½. For details, see GitHub. +## 0.0.10-alpha + ## 0.0.9-alpha * Development is now against Node.js 10.x LTS. While other Node.js series may continue to work, you're own your own and YMMV! * Fixed `justify` properties: `left` and `right` values were formerly swapped (oops!) diff --git a/docs/art/mci.md b/docs/art/mci.md index 3e5e18c5..629ed8ab 100644 --- a/docs/art/mci.md +++ b/docs/art/mci.md @@ -16,8 +16,8 @@ for a full listing. Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, et | Code | Description | |------|--------------| | `BN` | Board Name | -| `VL` | Version *label*, e.g. "ENiGMA½ v0.0.9-alpha" | -| `VN` | Version *number*, eg.. "0.0.9-alpha" | +| `VL` | Version *label*, e.g. "ENiGMA½ v0.0.10-alpha" | +| `VN` | Version *number*, eg.. "0.0.10-alpha" | | `SN` | SysOp username | | `SR` | SysOp real name | | `SL` | SysOp location | diff --git a/docs/installation/install-script.md b/docs/installation/install-script.md index 584492a2..c24dede9 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.10-alpha/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.10-alpha/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/package.json b/package.json index 15add31e..73aee3cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "enigma-bbs", - "version": "0.0.9-alpha", + "version": "0.0.10-alpha", "description": "ENiGMA½ Bulletin Board System", "author": "Bryan Ashby ", "license": "BSD-2-Clause", From aee84006beceaa8f2206887569eb614428e2b49f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 15 Feb 2019 18:56:10 -0700 Subject: [PATCH 002/140] Docs pakcage updates to silence vunerability alerts --- docs/Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index f49b797b..7a170268 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -20,11 +20,11 @@ GEM hacker (0.0.1) html-pipeline (2.7.1) activesupport (>= 2) - nokogiri (>= 1.4) + nokogiri (>= 1.8.5) http_parser.rb (0.6.0) i18n (0.9.1) concurrent-ruby (~> 1.0) - jekyll (3.7.0) + jekyll (3.7.4) addressable (~> 2.4) colorator (~> 1.0) em-websocket (~> 0.5) @@ -71,7 +71,7 @@ GEM public_suffix (3.0.1) rb-fsevent (0.10.2) rb-inotify (0.9.10) - ffi (>= 0.5.0, < 2) + ffi (>= 1.9.24, < 2) rouge (3.1.0) ruby_dep (1.5.0) safe_yaml (1.0.4) From 08186109a14b497da70b67c8ccc5f2cd69c42374 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 15 Feb 2019 19:06:10 -0700 Subject: [PATCH 003/140] Update dep. packages --- package.json | 6 ++--- yarn.lock | 74 ++++++++++++++++++++++++++-------------------------- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/package.json b/package.json index 73aee3cc..299074d6 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "retro" ], "dependencies": { - "async": "^2.6.1", + "async": "^2.6.2", "binary-parser": "^1.3.2", "buffers": "github:NuSkooler/node-buffers", "bunyan": "^1.8.12", @@ -33,11 +33,11 @@ "hashids": "^1.1.1", "hjson": "^3.1.2", "iconv-lite": "^0.4.23", - "inquirer": "^6.2.1", + "inquirer": "^6.2.2", "later": "1.2.0", "lodash": "^4.17.10", "lru-cache": "^5.1.1", - "mime-types": "^2.1.21", + "mime-types": "^2.1.22", "minimist": "1.2.x", "moment": "^2.24.0", "nntp-server": "^1.0.3", diff --git a/yarn.lock b/yarn.lock index 8a18bc57..6a922b8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17,10 +17,10 @@ ajv@^5.3.0: fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.3.0" -ansi-escapes@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" - integrity sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw== +ansi-escapes@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" + integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== ansi-regex@^2.0.0: version "2.1.1" @@ -119,12 +119,12 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg== -async@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" - integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ== +async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.2.tgz#18330ea7e6e313887f5d2f2a904bac6fe4dd5381" + integrity sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg== dependencies: - lodash "^4.17.10" + lodash "^4.17.11" asynckit@^0.4.0: version "0.4.0" @@ -253,10 +253,10 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -chalk@^2.0.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" - integrity sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ== +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== dependencies: ansi-styles "^3.2.1" escape-string-regexp "^1.0.5" @@ -554,7 +554,7 @@ extend@~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.0: +external-editor@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.3.tgz#5866db29a97826dbe4bf3afd24070ead9ea43a27" integrity sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA== @@ -858,21 +858,21 @@ ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== -inquirer@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.1.tgz#9943fc4882161bdb0b0c9276769c75b32dbfcd52" - integrity sha512-088kl3DRT2dLU5riVMKKr1DlImd6X7smDhpXUCkJDCKvTEJeRiXh0G132HG9u5a+6Ylw9plFRY7RuTnwohYSpg== +inquirer@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.2.tgz#46941176f65c9eb20804627149b743a218f25406" + integrity sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA== dependencies: - ansi-escapes "^3.0.0" - chalk "^2.0.0" + ansi-escapes "^3.2.0" + chalk "^2.4.2" cli-cursor "^2.1.0" cli-width "^2.0.0" - external-editor "^3.0.0" + external-editor "^3.0.3" figures "^2.0.0" - lodash "^4.17.10" + lodash "^4.17.11" mute-stream "0.0.7" run-async "^2.2.0" - rxjs "^6.1.0" + rxjs "^6.4.0" string-width "^2.1.0" strip-ansi "^5.0.0" through "^2.3.6" @@ -1098,7 +1098,7 @@ later@1.2.0: resolved "https://registry.yarnpkg.com/later/-/later-1.2.0.tgz#f2cf6c4dd7956dd2f520adf0329836e9876bad0f" integrity sha1-8s9sTdeVbdL1IK3wMpg26YdrrQ8= -lodash@^4.17.10, lodash@^4.17.4: +lodash@^4.17.10, lodash@^4.17.11, 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== @@ -1158,10 +1158,10 @@ mime-db@~1.36.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397" integrity sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw== -mime-db@~1.37.0: - version "1.37.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8" - integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg== +mime-db@~1.38.0: + version "1.38.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad" + integrity sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg== mime-types@^2.1.12, mime-types@~2.1.19: version "2.1.20" @@ -1170,12 +1170,12 @@ mime-types@^2.1.12, mime-types@~2.1.19: dependencies: mime-db "~1.36.0" -mime-types@^2.1.21: - version "2.1.21" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96" - integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg== +mime-types@^2.1.22: + version "2.1.22" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.22.tgz#fe6b355a190926ab7698c9a0556a11199b2199bd" + integrity sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog== dependencies: - mime-db "~1.37.0" + mime-db "~1.38.0" mimic-fn@^1.0.0: version "1.2.0" @@ -1698,10 +1698,10 @@ run-async@^2.2.0: dependencies: is-promise "^2.1.0" -rxjs@^6.1.0: - version "6.3.3" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.3.tgz#3c6a7fa420e844a81390fb1158a9ec614f4bad55" - integrity sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw== +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== dependencies: tslib "^1.9.0" From c695c1f4c5069502d365606c33bc847e8f574e2f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 15 Feb 2019 21:53:18 -0700 Subject: [PATCH 004/140] New findMessages() filters --- core/message.js | 44 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/core/message.js b/core/message.js index 7a30fb02..26a3bf21 100644 --- a/core/message.js +++ b/core/message.js @@ -236,16 +236,18 @@ module.exports = class Message { filter.uuids - use with resultType='id' filter.ids - use with resultType='uuid' - filter.toUserName - filter.fromUserName + filter.toUserName - string|Array(string) + filter.fromUserName - string|Array(string) filter.replyToMessageId + filter.operator = (AND)|OR + filter.newerThanTimestamp - may not be used with |date| filter.date - moment object - may not be used with |newerThanTimestamp| filter.newerThanMessageId filter.areaTag - note if you want by conf, send in all areas for a conf - *filter.metaTuples - {category, name, value} + filter.metaTuples - [ {category, name, value} ] filter.terms - FTS search @@ -267,6 +269,7 @@ module.exports = class Message { filter.resultType = filter.resultType || 'id'; filter.extraFields = filter.extraFields || []; + filter.operator = filter.operator || 'AND'; if('messageList' === filter.resultType) { filter.extraFields = _.uniq(filter.extraFields.concat( @@ -296,9 +299,9 @@ module.exports = class Message { let sqlOrderBy; let sqlWhere = ''; - function appendWhereClause(clause) { + function appendWhereClause(clause, op) { if(sqlWhere) { - sqlWhere += ' AND '; + sqlWhere += ` ${op || filter.operator} `; } else { sqlWhere += ' WHERE '; } @@ -345,7 +348,7 @@ module.exports = class Message { } // explicit exclude of Private - appendWhereClause(`m.area_tag != "${Message.WellKnownAreaTags.Private}"`); + appendWhereClause(`m.area_tag != "${Message.WellKnownAreaTags.Private}"`, 'AND'); } if(_.isNumber(filter.replyToMessageId)) { @@ -353,8 +356,18 @@ module.exports = class Message { } [ 'toUserName', 'fromUserName' ].forEach(field => { - if(_.isString(filter[field]) && filter[field].length > 0) { - appendWhereClause(`m.${_.snakeCase(field)} LIKE "${sanitizeString(filter[field])}"`); + let val = filter[field]; + if(!val) { + return; // next item + } + if(_.isString(val)) { + val = [ val ]; + } + if(Array.isArray(val)) { + val = '(' + val.map(v => { + return `m.${_.snakeCase(field)} LIKE "${sanitizeString(v)}"`; + }).join(' OR ') + ')'; + appendWhereClause(val); } }); @@ -380,6 +393,21 @@ module.exports = class Message { ); } + if(Array.isArray(filter.metaTuples)) { + let sub = []; + filter.metaTuples.forEach(mt => { + sub.push(`(meta_category = "${mt.category}" AND meta_name = "${mt.name}" AND meta_value = "${sanitizeString(mt.value)}")`); + }); + sub = sub.join(` ${filter.operator} `); + appendWhereClause( + `m.message_id IN ( + SELECT message_id + FROM message_meta + WHERE ${sub} + )` + ); + } + sql += `${sqlWhere} ${sqlOrderBy}`; if(_.isNumber(filter.limit)) { From 83d0daf4b797fa578ac54cc2633dafde84c42ef4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 15 Feb 2019 21:55:18 -0700 Subject: [PATCH 005/140] Add 'My Messages' module --- art/themes/luciano_blocktronics/MSGMNU.ANS | Bin 3660 -> 3685 bytes art/themes/luciano_blocktronics/MYMSGLST.ANS | Bin 0 -> 2122 bytes art/themes/luciano_blocktronics/theme.hjson | 15 +++++ core/my_messages.js | 59 +++++++++++++++++++ misc/menu_template.in.hjson | 37 ++++++++++++ 5 files changed, 111 insertions(+) create mode 100644 art/themes/luciano_blocktronics/MYMSGLST.ANS create mode 100644 core/my_messages.js diff --git a/art/themes/luciano_blocktronics/MSGMNU.ANS b/art/themes/luciano_blocktronics/MSGMNU.ANS index 3585799b98f1e143184bf8efa54e5da16bfccba8..9c373944c901ce9fba4302f0fee7c0ef272e5d00 100644 GIT binary patch delta 54 zcmX>j^HgR7C#$lBvvjm!Zmx8+fwi$&u0n2VadBdLYOz8|K9H>-9c^rsyIGi3n3M6> IWOY7O0M3sPA^-pY delta 29 lcmaDVb4F$ZCo7kcv9olvu~F_uh0Qvw!kml`Cp++|0sw}G2wMOE diff --git a/art/themes/luciano_blocktronics/MYMSGLST.ANS b/art/themes/luciano_blocktronics/MYMSGLST.ANS new file mode 100644 index 0000000000000000000000000000000000000000..a56197bcc6fce15564c05499a6a4ac1d26e5aacc GIT binary patch literal 2122 zcmb_d%Zd|06wR!JY~45pv^MVhk#wV3jbRWM1LELpvQr`|)(kpEe`}a;n7`8v_ z?!8rAi9rOdMR(VIpL1_j&(_s)UM<_oyV<(-VHj5oG0fJLhrC|lK_kdU1AIjqImf5I zow+QMhiJqnOx-YOz7U|h@P=hA#fR3O2`^5 zK)TCt#MxA-sidKBEwUk3kD{Key^knW#(`C!bEoGR90+7v`Aox??MIskO_paO@xZ@_ zo@zV71uU{4N%+zEo14U4VFQf}z4f&SRJ@5a2O?YrKWB;84upH6fUA<0dxq@B=*}#3H>0O1 zss34`{IcWp;rYp%)oE2R-hWz6RWItQu72K|OpcCTO^zqB?!SC~<}O~G=q^8h0Z?wp AJ^%m! literal 0 HcmV?d00001 diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 4a35eb15..998d4c34 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -450,6 +450,21 @@ } } + messageAreaMyMessagesList: { + config: { + // Fri Sep 25th + dateTimeFormat: ddd MMM Do + } + mci: { + VM1: { + height: 16 + width: 71 + itemFormat: "|00|15 {msgNum:<4.4} |03{subject:<34.33} {fromUserName:<19.18} |03{ts:<12.12}" + focusItemFormat: "|00|19> |15{msgNum:<4.4} {subject:<34.33} {fromUserName:<19.18} {ts:<12.12}" + } + } + } + messageAreaViewPost: { 0: { mci: { diff --git a/core/my_messages.js b/core/my_messages.js new file mode 100644 index 00000000..721e1f9d --- /dev/null +++ b/core/my_messages.js @@ -0,0 +1,59 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; +const Message = require('./message.js'); +const UserProps = require('./user_property.js'); + +exports.moduleInfo = { + name : 'My Messages', + desc : 'Finds messages addressed to the current user.', + author : 'NuSkooler', +}; + +exports.getModule = class MyMessagesModule extends MenuModule { + constructor(options) { + super(options); + } + + initSequence() { + const filter = { + toUserName : [ this.client.user.username, this.client.user.getProperty(UserProps.RealName) ], + sort : 'modTimestamp', + resultType : 'messageList', + limit : 1024 * 16, // we want some sort of limit... + }; + + Message.findMessages(filter, (err, messageList) => { + if(err) { + this.client.log.warn( { error : err.message }, 'Error finding messages addressed to current user'); + return this.prevMenu(); + } + this.messageList = messageList; + this.finishedLoading(); + }); + } + + finishedLoading() { + if(!this.messageList || 0 === this.messageList.length) { + return this.gotoMenu( + this.menuConfig.config.noResultsMenu || 'messageSearchNoResults', + { menuFlags : [ 'popParent' ] } + ); + } + + const menuOpts = { + extraArgs : { + messageList : this.messageList, + noUpdateLastReadId : true + }, + menuFlags : [ 'popParent' ], + }; + + return this.gotoMenu( + this.menuConfig.config.messageListMenu || 'messageAreaMessageList', + menuOpts + ); + } +}; diff --git a/misc/menu_template.in.hjson b/misc/menu_template.in.hjson index bdcf97cd..ac51397f 100644 --- a/misc/menu_template.in.hjson +++ b/misc/menu_template.in.hjson @@ -1970,6 +1970,43 @@ } } + messageAreaMyMessagesList: { + desc: Personal Messages + module: msg_list + art: MYMSGLST + config: { + menuViewPost: messageAreaViewPost + } + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: message + } + TL6: { + // theme me! + } + } + submit: { + *: [ + { + value: { message: null } + action: @method:selectMessage + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + messageSearchNoResults: { desc: Message Search art: MSRCNORES From f41d12c6885c68c0d4b7b5a74c9b6d4d8442fdee Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 16 Feb 2019 11:03:46 -0700 Subject: [PATCH 006/140] + oputil user rename --- core/oputil/oputil_help.js | 2 ++ core/oputil/oputil_user.js | 48 ++++++++++++++++++++++++++++++++++--- docs/admin/oputil.md | 3 +++ misc/menu_template.in.hjson | 3 --- 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index f8a0d42c..6f485f0a 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -30,6 +30,8 @@ actions: aliases: password, passwd rm USERNAME permanently removes user from system aliases: remove, delete, del + rename USERNAME NEWNAME rename a user + aliases: mv activate USERNAME set status to active deactivate USERNAME set status to inactive disable USERNAME set status to disabled diff --git a/core/oputil/oputil_user.js b/core/oputil/oputil_user.js index a52facfb..33327d1d 100644 --- a/core/oputil/oputil_user.js +++ b/core/oputil/oputil_user.js @@ -227,6 +227,41 @@ function removeUser(user) { ); } +function renameUser(user) { + if(argv._.length < 3) { + return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); + } + + const newUserName = argv._[argv._.length - 1]; + + async.series( + [ + (callback) => { + const { validateUserNameAvail } = require('../../core/system_view_validate.js'); + return validateUserNameAvail(newUserName, callback); + }, + (callback) => { + const userDb = require('../../core/database.js').dbs.user; + userDb.run( + `UPDATE user + SET user_name = ? + WHERE id = ?;`, + [ newUserName, user.userId, ], + err => { + return callback(err); + } + ); + } + ], + err => { + if(err) { + return console.error(err.reason ? err.reason : err.message); + } + return console.info(`User "${user.username}" renamed to "${newUserName}"`); + } + ); +} + function modUserGroups(user) { if(argv._.length < 3) { return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); @@ -317,9 +352,13 @@ function handleUserCommand() { return errUsage(); } - const action = argv._[1]; - const usernameIdx = [ 'pw', 'pass', 'passwd', 'password', 'group' ].includes(action) ? argv._.length - 2 : argv._.length - 1; - const userName = argv._[usernameIdx]; + const action = argv._[1]; + const usernameIdx = [ + 'pw', 'pass', 'passwd', 'password', + 'group', + 'mv', 'rename' + ].includes(action) ? argv._.length - 2 : argv._.length - 1; + const userName = argv._[usernameIdx]; if(!userName) { return errUsage(); @@ -341,6 +380,9 @@ function handleUserCommand() { del : removeUser, delete : removeUser, + mv : renameUser, + rename : renameUser, + activate : setAccountStatus, deactivate : setAccountStatus, disable : setAccountStatus, diff --git a/docs/admin/oputil.md b/docs/admin/oputil.md index bb6dcacf..e1164540 100644 --- a/docs/admin/oputil.md +++ b/docs/admin/oputil.md @@ -49,6 +49,8 @@ actions: aliases: password, passwd rm USERNAME permanently removes user from system aliases: remove, delete, del + rename USERNAME NEWNAME rename a user + aliases: mv activate USERNAME set status to active deactivate USERNAME set status to inactive disable USERNAME set status to disabled @@ -61,6 +63,7 @@ actions: | `info` | Display user information| `./oputil.js user info joeuser` | N/A | | `pw` | Set password | `./oputil.js user pw joeuser s3cr37` | `passwd`, `password` | | `rm` | Removes user | `./oputil.js user del joeuser` | `remove`, `del`, `delete` | +| `rename` | Renames a user | `./oputil.js user rename joeuser joe` | `mv` | | `activate` | Activates user | `./oputil.js user activate joeuser` | N/A | | `deactivate` | Deactivates user | `./oputil.js user deactivate joeuser` | N/A | | `disable` | Disables user (user will not be able to login) | `./oputil.js user disable joeuser` | N/A | diff --git a/misc/menu_template.in.hjson b/misc/menu_template.in.hjson index ac51397f..9aac86f3 100644 --- a/misc/menu_template.in.hjson +++ b/misc/menu_template.in.hjson @@ -1985,9 +1985,6 @@ submit: true argName: message } - TL6: { - // theme me! - } } submit: { *: [ From f628168a68ba66bb49fc8c83cde0332d7271ddb7 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 18 Feb 2019 14:49:01 -0700 Subject: [PATCH 007/140] Add note to WHATSNEW --- WHATSNEW.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/WHATSNEW.md b/WHATSNEW.md index 31fe24c5..7137038a 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -2,6 +2,9 @@ This document attempts to track **major** changes and additions in ENiGMA½. For details, see GitHub. ## 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. + ## 0.0.9-alpha * Development is now against Node.js 10.x LTS. While other Node.js series may continue to work, you're own your own and YMMV! From 65ef1feb6c9cb5e200191977c81f290f5dda6e46 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 20 Feb 2019 21:12:41 -0700 Subject: [PATCH 008/140] Use crypto.timingSafeEqual() vs hand rolled method for constant time password comparison --- core/user.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/core/user.js b/core/user.js index 3b261dc6..43779bca 100644 --- a/core/user.js +++ b/core/user.js @@ -60,18 +60,6 @@ module.exports = class User { }; } - static isSamePasswordSlowCompare(passBuf1, passBuf2) { - if(passBuf1.length !== passBuf2.length) { - return false; - } - - let c = 0; - for(let i = 0; i < passBuf1.length; i++) { - c |= passBuf1[i] ^ passBuf2[i]; - } - return 0 === c; - } - isAuthenticated() { return true === this.authenticated; } @@ -220,7 +208,7 @@ module.exports = class User { const passDkBuf = Buffer.from(passDk, 'hex'); const propsDkBuf = Buffer.from(propsDk, 'hex'); - return callback(User.isSamePasswordSlowCompare(passDkBuf, propsDkBuf) ? + return callback(crypto.timingSafeEqual(passDkBuf, propsDkBuf) ? null : Errors.AccessDenied('Invalid password') ); From 57938e761eebaf21a5b136760c4e280acf10261c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 20 Feb 2019 23:55:09 -0700 Subject: [PATCH 009/140] + Implement SSH PubKey authentication * Security related items to config/security dir --- UPGRADE.md | 2 +- core/config.js | 11 ++--- core/servers/login/ssh.js | 82 +++++++++++++++++++++++------------ core/user.js | 71 +++++++++++++++++++++--------- core/user_login.js | 9 +++- core/user_property.js | 81 +++++++++++++++++----------------- misc/config_template.in.hjson | 4 +- 7 files changed, 164 insertions(+), 96 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index 215089ad..ed5e687d 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -41,7 +41,7 @@ Report your issue on Xibalba BBS, hop in #enigma-bbs on Freenet and chat, or [file a issue on GitHub](https://github.com/NuSkooler/enigma-bbs/issues). # 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. # 0.0.8-alpha to 0.0.9-alpha * Development is now against Node.js 10.x LTS. Follow your standard upgrade path to update to Node 10.x before using 0.0.9-alpha! diff --git a/core/config.js b/core/config.js index 5c1f9c42..d6cdadba 100644 --- a/core/config.js +++ b/core/config.js @@ -250,6 +250,7 @@ function getDefaultConfig() { paths : { config : paths.join(__dirname, './../config/'), + security : paths.join(__dirname, './../config/security'), // certs, keys, etc. mods : paths.join(__dirname, './../mods/'), loginServers : paths.join(__dirname, './servers/login/'), contentServers : paths.join(__dirname, './servers/content/'), @@ -259,7 +260,7 @@ function getDefaultConfig() { art : paths.join(__dirname, './../art/general/'), themes : paths.join(__dirname, './../art/themes/'), - logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such + logs : paths.join(__dirname, './../logs/'), db : paths.join(__dirname, './../db/'), modsDb : paths.join(__dirname, './../db/mods/'), dropFiles : paths.join(__dirname, './../drop/'), // + "/node/ @@ -284,10 +285,10 @@ function getDefaultConfig() { // // > openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \ // -pkeyopt rsa_keygen_pubexp:65537 | openssl rsa \ - // -out ./config/ssh_private_key.pem -aes128 + // -out ./config/security/ssh_private_key.pem -aes128 // - // (The above is a more modern equivelant of the following): - // > openssl genrsa -aes128 -out ./config/ssh_private_key.pem 2048 + // (The above is a more modern equivalent of the following): + // > openssl genrsa -aes128 -out ./config/security/ssh_private_key.pem 2048 // // 2 - Set 'privateKeyPass' to the password you used in step #1 // @@ -297,7 +298,7 @@ function getDefaultConfig() { // - https://blog.sleeplessbeastie.eu/2017/12/28/how-to-generate-private-key/ // - https://gist.github.com/briansmith/2ee42439923d8e65a266994d0f70180b // - privateKeyPem : paths.join(__dirname, './../config/ssh_private_key.pem'), + privateKeyPem : paths.join(__dirname, './../config/security/ssh_private_key.pem'), firstMenu : 'sshConnected', firstMenuNewUser : 'sshConnectedNewUser', diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index ee63ac78..3b0e852f 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -14,6 +14,8 @@ const { Errors, ErrorReasons } = require('../../enig_error.js'); +const User = require('../../user.js'); +const UserProps = require('../../user_property.js'); // deps const ssh2 = require('ssh2'); @@ -21,6 +23,7 @@ const fs = require('graceful-fs'); const util = require('util'); const _ = require('lodash'); const assert = require('assert'); +const crypto = require('crypto'); const ModuleInfo = exports.moduleInfo = { name : 'SSH', @@ -42,8 +45,6 @@ function SSHClient(clientConn) { clientConn.on('authentication', function authAttempt(ctx) { const username = ctx.username || ''; - const password = ctx.password || ''; - const config = Config(); self.isNewUser = (config.users.newUserNames || []).indexOf(username) > -1; @@ -106,37 +107,36 @@ function SSHClient(clientConn) { } }; - // - // If the system is open and |isNewUser| is true, the login - // sequence is hijacked in order to start the application process. - // - if(false === config.general.closedSystem && self.isNewUser) { - return ctx.accept(); - } + const authWithPasswordOrPubKey = (authType) => { + if('pubKey' !== authType || !self.user.isAuthenticated() || !ctx.signature) { + // step 1: login/auth using PubKey + userLogin(self, ctx.username, ctx.password, { authType, ctx }, (err) => { + if(err) { + if(isSpecialHandleError(err)) { + return handleSpecialError(err, username); + } - if(username.length > 0 && password.length > 0) { - userLogin(self, ctx.username, ctx.password, function authResult(err) { - if(err) { - if(isSpecialHandleError(err)) { - return handleSpecialError(err, username); + if(Errors.BadLogin().code === err.code) { + return slowTerminateConnection(); + } + + return safeContextReject(SSHClient.ValidAuthMethods); } - if(Errors.BadLogin().code === err.code) { - return slowTerminateConnection(); - } - - return safeContextReject(SSHClient.ValidAuthMethods); + ctx.accept(); + }); + } else { + // step 2: verify signature + const pubKeyActual = ssh2.utils.parseKey(self.user.getProperty(UserProps.LoginPubKey)); + if(!pubKeyActual || !pubKeyActual.verify(ctx.blob, ctx.signature)) { + return slowTerminateConnection(); } - - ctx.accept(); - }); - } else { - if(-1 === SSHClient.ValidAuthMethods.indexOf(ctx.method)) { - return safeContextReject(SSHClient.ValidAuthMethods); + return ctx.accept(); } + }; + const authKeyboardInteractive = () => { if(0 === username.length) { - // :TODO: can we display something here? return safeContextReject(); } @@ -176,6 +176,30 @@ function SSHClient(clientConn) { } }); }); + }; + + // + // If the system is open and |isNewUser| is true, the login + // sequence is hijacked in order to start the application process. + // + if(false === config.general.closedSystem && self.isNewUser) { + return ctx.accept(); + } + + switch(ctx.method) { + case 'password' : + return authWithPasswordOrPubKey('password'); + //return authWithPassword(); + + case 'publickey' : + return authWithPasswordOrPubKey('pubKey'); + //return authWithPubKey(); + + case 'keyboard-interactive' : + return authKeyboardInteractive(); + + default : + return safeContextReject(SSHClient.ValidAuthMethods); } }); @@ -293,7 +317,11 @@ function SSHClient(clientConn) { util.inherits(SSHClient, baseClient.Client); -SSHClient.ValidAuthMethods = [ 'password', 'keyboard-interactive' ]; +SSHClient.ValidAuthMethods = [ + 'password', + 'keyboard-interactive', + 'publickey', +]; exports.getModule = class SSHServerModule extends LoginServerModule { constructor() { diff --git a/core/user.js b/core/user.js index 43779bca..36a2f72f 100644 --- a/core/user.js +++ b/core/user.js @@ -21,6 +21,7 @@ const async = require('async'); const _ = require('lodash'); const moment = require('moment'); const sanatizeFilename = require('sanitize-filename'); +const ssh2 = require('ssh2'); exports.isRootUserId = function(id) { return 1 === id; }; @@ -47,7 +48,10 @@ module.exports = class User { static get StandardPropertyGroups() { return { - password : [ UserProps.PassPbkdf2Salt, UserProps.PassPbkdf2Dk ], + auth : [ + UserProps.PassPbkdf2Salt, UserProps.PassPbkdf2Dk, + UserProps.LoginPubKey, UserProps.LoginPubKeyFingerprintSHA256, + ], }; } @@ -174,10 +178,49 @@ module.exports = class User { }); } - authenticate(username, password, cb) { + authenticate(username, password, options, cb) { + if(!cb && _.isFunction(options)) { + cb = options; + options = {}; + } + const self = this; const tempAuthInfo = {}; + const validatePassword = (props, callback) => { + User.generatePasswordDerivedKey(password, props[UserProps.PassPbkdf2Salt], (err, dk) => { + if(err) { + return callback(err); + } + + // + // Use constant time comparison here for security feel-goods + // + const passDkBuf = Buffer.from(dk, 'hex'); + const propsDkBuf = Buffer.from(props[UserProps.PassPbkdf2Dk], 'hex'); + + return callback(crypto.timingSafeEqual(passDkBuf, propsDkBuf) ? + null : + Errors.AccessDenied('Invalid password') + ); + }); + }; + + const validatePubKey = (props, callback) => { + const pubKeyActual = ssh2.utils.parseKey(props[UserProps.LoginPubKey]); + if(!pubKeyActual) { + return callback(Errors.AccessDenied('Invalid public key')); + } + + if(options.ctx.key.algo != pubKeyActual.type || + !crypto.timingSafeEqual(options.ctx.key.data, pubKeyActual.getPublicSSH())) + { + return callback(Errors.AccessDenied('Invalid public key')); + } + + return callback(null); + }; + async.waterfall( [ function fetchUserId(callback) { @@ -191,27 +234,15 @@ module.exports = class User { }, function getRequiredAuthProperties(callback) { // fetch properties required for authentication - User.loadProperties(tempAuthInfo.userId, { names : User.StandardPropertyGroups.password }, (err, props) => { + User.loadProperties( tempAuthInfo.userId, { names : User.StandardPropertyGroups.auth }, (err, props) => { return callback(err, props); }); }, - function getDkWithSalt(props, callback) { - // get DK from stored salt and password provided - User.generatePasswordDerivedKey(password, props[UserProps.PassPbkdf2Salt], (err, dk) => { - return callback(err, dk, props[UserProps.PassPbkdf2Dk]); - }); - }, - function validateAuth(passDk, propsDk, callback) { - // - // Use constant time comparison here for security feel-goods - // - const passDkBuf = Buffer.from(passDk, 'hex'); - const propsDkBuf = Buffer.from(propsDk, 'hex'); - - return callback(crypto.timingSafeEqual(passDkBuf, propsDkBuf) ? - null : - Errors.AccessDenied('Invalid password') - ); + function validatePassOrPubKey(props, callback) { + if('pubKey' === options.authType) { + return validatePubKey(props, callback); + } + return validatePassword(props, callback); }, function initProps(callback) { User.loadProperties(tempAuthInfo.userId, (err, allProps) => { diff --git a/core/user_login.js b/core/user_login.js index 3e3f5f04..1f1180da 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -22,7 +22,12 @@ const _ = require('lodash'); exports.userLogin = userLogin; -function userLogin(client, username, password, cb) { +function userLogin(client, username, password, options, cb) { + if(!cb && _.isFunction(options)) { + cb = options; + options = {}; + } + const config = Config(); if(config.users.badUserNames.includes(username.toLowerCase())) { @@ -34,7 +39,7 @@ function userLogin(client, username, password, cb) { }, 2000); } - client.user.authenticate(username, password, err => { + client.user.authenticate(username, password, options, err => { if(err) { client.user.sessionFailedLoginAttempts = _.get(client.user, 'sessionFailedLoginAttempts', 0) + 1; const disconnect = config.users.failedLogin.disconnect; diff --git a/core/user_property.js b/core/user_property.js index 56e47e66..c3aabcd0 100644 --- a/core/user_property.js +++ b/core/user_property.js @@ -8,54 +8,57 @@ // can utilize their own properties as well! // module.exports = { - PassPbkdf2Salt : 'pw_pbkdf2_salt', - PassPbkdf2Dk : 'pw_pbkdf2_dk', + PassPbkdf2Salt : 'pw_pbkdf2_salt', + PassPbkdf2Dk : 'pw_pbkdf2_dk', - AccountStatus : 'account_status', // See User.AccountStatus enum + AccountStatus : 'account_status', // See User.AccountStatus enum - RealName : 'real_name', - Sex : 'sex', - Birthdate : 'birthdate', - Location : 'location', - Affiliations : 'affiliation', - EmailAddress : 'email_address', - WebAddress : 'web_address', - TermHeight : 'term_height', - TermWidth : 'term_width', - ThemeId : 'theme_id', - AccountCreated : 'account_created', - LastLoginTs : 'last_login_timestamp', - LoginCount : 'login_count', - UserComment : 'user_comment', // NYI + RealName : 'real_name', + Sex : 'sex', + Birthdate : 'birthdate', + Location : 'location', + Affiliations : 'affiliation', + EmailAddress : 'email_address', + WebAddress : 'web_address', + TermHeight : 'term_height', + TermWidth : 'term_width', + ThemeId : 'theme_id', + AccountCreated : 'account_created', + LastLoginTs : 'last_login_timestamp', + LoginCount : 'login_count', + UserComment : 'user_comment', // NYI - DownloadQueue : 'dl_queue', // download_queue.js + DownloadQueue : 'dl_queue', // download_queue.js - FailedLoginAttempts : 'failed_login_attempts', - AccountLockedTs : 'account_locked_timestamp', - AccountLockedPrevStatus : 'account_locked_prev_status', // previous account status before lock out + FailedLoginAttempts : 'failed_login_attempts', + AccountLockedTs : 'account_locked_timestamp', + AccountLockedPrevStatus : 'account_locked_prev_status', // previous account status before lock out - EmailPwResetToken : 'email_password_reset_token', - EmailPwResetTokenTs : 'email_password_reset_token_ts', + EmailPwResetToken : 'email_password_reset_token', + EmailPwResetTokenTs : 'email_password_reset_token_ts', - FileAreaTag : 'file_area_tag', - FileBaseFilters : 'file_base_filters', - FileBaseFilterActiveUuid : 'file_base_filter_active_uuid', - FileBaseLastViewedId : 'user_file_base_last_viewed', - FileDlTotalCount : 'dl_total_count', - FileUlTotalCount : 'ul_total_count', - FileDlTotalBytes : 'dl_total_bytes', - FileUlTotalBytes : 'ul_total_bytes', + FileAreaTag : 'file_area_tag', + FileBaseFilters : 'file_base_filters', + FileBaseFilterActiveUuid : 'file_base_filter_active_uuid', + FileBaseLastViewedId : 'user_file_base_last_viewed', + FileDlTotalCount : 'dl_total_count', + FileUlTotalCount : 'ul_total_count', + FileDlTotalBytes : 'dl_total_bytes', + FileUlTotalBytes : 'ul_total_bytes', - MessageConfTag : 'message_conf_tag', - MessageAreaTag : 'message_area_tag', - MessagePostCount : 'post_count', + MessageConfTag : 'message_conf_tag', + MessageAreaTag : 'message_area_tag', + MessagePostCount : 'post_count', - DoorRunTotalCount : 'door_run_total_count', - DoorRunTotalMinutes : 'door_run_total_minutes', + DoorRunTotalCount : 'door_run_total_count', + DoorRunTotalMinutes : 'door_run_total_minutes', - AchievementTotalCount : 'achievement_total_count', - AchievementTotalPoints : 'achievement_total_points', + AchievementTotalCount : 'achievement_total_count', + AchievementTotalPoints : 'achievement_total_points', - MinutesOnlineTotalCount : 'minutes_online_total_count', + MinutesOnlineTotalCount : 'minutes_online_total_count', + + LoginPubKey : 'login_public_key', // OpenSSL format + //LoginPubKeyFingerprintSHA256 : 'login_public_key_fp_sha256', // hint: ssh-kegen -lf id_rsa.pub }; diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index 5e523e72..38482a6a 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -118,10 +118,10 @@ // // > openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \ // -pkeyopt rsa_keygen_pubexp:65537 | openssl rsa \ - // -out ./config/ssh_private_key.pem -aes128 + // -out ./config/security/ssh_private_key.pem -aes128 // // (The above is a more modern equivelant of the following): - // > openssl genrsa -aes128 -out ./config/ssh_private_key.pem 2048 + // > openssl genrsa -aes128 -out ./config/security/ssh_private_key.pem 2048 // // 2 - Set 'privateKeyPass' to the password you used in step #1 // From 23779c3abee6eb91e68afbdc4b126bfe571b4222 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 22 Feb 2019 22:51:12 -0700 Subject: [PATCH 010/140] Use authInfo obj vs weird params. auth factor 1: factor 2 for 2FA, etc. --- core/servers/content/nntp.js | 2 +- core/servers/login/ssh.js | 7 +++---- core/user.js | 23 +++++++++++++---------- core/user_login.js | 11 ++++++++++- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/core/servers/content/nntp.js b/core/servers/content/nntp.js index 4959dc64..9f7c5a13 100644 --- a/core/servers/content/nntp.js +++ b/core/servers/content/nntp.js @@ -141,7 +141,7 @@ class NNTPServer extends NNTPServerBase { return new Promise( resolve => { const user = new User(); - user.authenticate(username, password, err => { + user.authenticateFactor1({ type : User.AuthFactor1Types.Password, username, password }, err => { if(err) { // :TODO: Log IP address this.log.debug( { username, reason : err.message }, 'Authentication failure'); diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index 3b0e852f..3e486bbd 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -23,7 +23,6 @@ const fs = require('graceful-fs'); const util = require('util'); const _ = require('lodash'); const assert = require('assert'); -const crypto = require('crypto'); const ModuleInfo = exports.moduleInfo = { name : 'SSH', @@ -108,7 +107,7 @@ function SSHClient(clientConn) { }; const authWithPasswordOrPubKey = (authType) => { - if('pubKey' !== authType || !self.user.isAuthenticated() || !ctx.signature) { + if(User.AuthFactor1Types.PubKey !== authType || !self.user.isAuthenticated() || !ctx.signature) { // step 1: login/auth using PubKey userLogin(self, ctx.username, ctx.password, { authType, ctx }, (err) => { if(err) { @@ -188,11 +187,11 @@ function SSHClient(clientConn) { switch(ctx.method) { case 'password' : - return authWithPasswordOrPubKey('password'); + return authWithPasswordOrPubKey(User.AuthFactor1Types.Password); //return authWithPassword(); case 'publickey' : - return authWithPasswordOrPubKey('pubKey'); + return authWithPasswordOrPubKey(User.AuthFactor1Types.PubKey); //return authWithPubKey(); case 'keyboard-interactive' : diff --git a/core/user.js b/core/user.js index 36a2f72f..3b20aa76 100644 --- a/core/user.js +++ b/core/user.js @@ -178,17 +178,20 @@ module.exports = class User { }); } - authenticate(username, password, options, cb) { - if(!cb && _.isFunction(options)) { - cb = options; - options = {}; - } + static get AuthFactor1Types() { + return { + PubKey : 'pubKey', + Password : 'password', + }; + } + authenticateFactor1(authInfo, cb) { + const username = authInfo.username; const self = this; const tempAuthInfo = {}; const validatePassword = (props, callback) => { - User.generatePasswordDerivedKey(password, props[UserProps.PassPbkdf2Salt], (err, dk) => { + User.generatePasswordDerivedKey(authInfo.password, props[UserProps.PassPbkdf2Salt], (err, dk) => { if(err) { return callback(err); } @@ -212,8 +215,8 @@ module.exports = class User { return callback(Errors.AccessDenied('Invalid public key')); } - if(options.ctx.key.algo != pubKeyActual.type || - !crypto.timingSafeEqual(options.ctx.key.data, pubKeyActual.getPublicSSH())) + if(authInfo.pubKey.key.algo != pubKeyActual.type || + !crypto.timingSafeEqual(authInfo.pubKey.key.data, pubKeyActual.getPublicSSH())) { return callback(Errors.AccessDenied('Invalid public key')); } @@ -234,12 +237,12 @@ module.exports = class User { }, function getRequiredAuthProperties(callback) { // fetch properties required for authentication - User.loadProperties( tempAuthInfo.userId, { names : User.StandardPropertyGroups.auth }, (err, props) => { + User.loadProperties(tempAuthInfo.userId, { names : User.StandardPropertyGroups.auth }, (err, props) => { return callback(err, props); }); }, function validatePassOrPubKey(props, callback) { - if('pubKey' === options.authType) { + if(User.AuthFactor1Types.PubKey === authInfo.type) { return validatePubKey(props, callback); } return validatePassword(props, callback); diff --git a/core/user_login.js b/core/user_login.js index 1f1180da..98bdeefb 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -15,6 +15,7 @@ const { const UserProps = require('./user_property.js'); const SysProps = require('./system_property.js'); const SystemLogKeys = require('./system_log.js'); +const User = require('./user.js'); // deps const async = require('async'); @@ -39,7 +40,15 @@ function userLogin(client, username, password, options, cb) { }, 2000); } - client.user.authenticate(username, password, options, err => { + const authInfo = { + username, + password, + }; + + authInfo.type = options.authType || User.AuthFactor1Types.Password; + authInfo.pubKey = options.ctx; + + client.user.authenticateFactor1(authInfo, err => { if(err) { client.user.sessionFailedLoginAttempts = _.get(client.user, 'sessionFailedLoginAttempts', 0) + 1; const disconnect = config.users.failedLogin.disconnect; From 64de10cbf7481989c8408b71d0fa614e113255f3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 5 Apr 2019 21:53:30 -0600 Subject: [PATCH 011/140] Fix crash if client disconnects/is logged out while door is running --- core/abracadabra.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/abracadabra.js b/core/abracadabra.js index 34374049..36a58544 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -170,6 +170,12 @@ exports.getModule = class AbracadabraModule extends MenuModule { this.doorInstance.run(exeInfo, () => { trackDoorRunEnd(doorTracking); + // client may have disconnected while process was active - + // we're done here if so. + if(!this.client.term.output) { + return; + } + // // Try to clean up various settings such as scroll regions that may // have been set within the door From 19e70d1c781129a27b05c4472516e96e286e849d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 8 Apr 2019 20:14:00 -0600 Subject: [PATCH 012/140] Fix log message & hopefully handle client socket errors better --- core/login_server_module.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/login_server_module.js b/core/login_server_module.js index 041f317c..a08abfe9 100644 --- a/core/login_server_module.js +++ b/core/login_server_module.js @@ -40,6 +40,10 @@ module.exports = class LoginServerModule extends ServerModule { } handleNewClient(client, clientSock, modInfo) { + clientSock.on('error', err => { + logger.log.warn({ modInfo, error : err.message }, 'Client socket error'); + }); + // // Start tracking the client. A session ID aka client ID // will be established in addNewClient() below. @@ -68,7 +72,7 @@ module.exports = class LoginServerModule extends ServerModule { }); client.on('error', err => { - logger.log.info({ clientId : client.session.id }, 'Connection error: %s' % err.message); + logger.log.info({ clientId : client.session.id, error : err.message }, 'Connection error'); }); client.on('close', err => { From 0ed507cd7bfd67b72f0b5c53753633abece240ba Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 9 Apr 2019 20:07:19 -0600 Subject: [PATCH 013/140] Initial real 2FA/OTP work --- .eslintrc.json | 4 ++- WHATSNEW.md | 2 ++ core/acs_parser.js | 17 ++++++++++ core/config.js | 4 +++ core/servers/login/ssh.js | 6 ++-- core/system_menu_method.js | 57 +++++++++++++++++++++----------- core/user.js | 38 +++++++++++++++------ core/user_property.js | 7 ++-- docs/configuration/acs.md | 4 +-- docs/configuration/menu-hjson.md | 1 + misc/acs_parser.pegjs | 17 ++++++++++ package.json | 3 +- yarn.lock | 12 +++++++ 13 files changed, 133 insertions(+), 39 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 612da123..53bd1287 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,7 +3,9 @@ "es6": true, "node": true }, - "extends": "eslint:recommended", + "extends": [ + "eslint:recommended" + ], "rules": { "indent": [ "error", diff --git a/WHATSNEW.md b/WHATSNEW.md index 7137038a..d4a5bc39 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -4,6 +4,8 @@ This document attempts to track **major** changes and additions in ENiGMA½. For ## 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. ++ SSH Public Key Authentication has been added. The system uses a OpenSSH style public key set on the `ssh_public_key` user property. ++ ## 0.0.9-alpha diff --git a/core/acs_parser.js b/core/acs_parser.js index d4084b95..3763c8e7 100644 --- a/core/acs_parser.js +++ b/core/acs_parser.js @@ -846,6 +846,7 @@ function peg$parse(input, options) { const UserProps = require('./user_property.js'); const Log = require('./logger.js').log; + const User = require('./user.js'); const _ = require('lodash'); const moment = require('moment'); @@ -982,6 +983,22 @@ function peg$parse(input, options) { SC : function isSecureConnection() { return _.get(client, 'session.isSecure', false); }, + AF : function currentAuthFactor() { + if(!user) { + return false; + } + return !isNaN(value) && user.authFactor >= value; + }, + AR : function authFactorRequired() { + if(!user) { + return false; + } + switch(value) { + case 1 : return user.authFactor >= User.AuthFactors.Factor1; + case 2 : return user.authFactor >= User.AuthFActors.Factor2; + default : return false; + } + }, ML : function minutesLeft() { // :TODO: implement me! return false; diff --git a/core/config.js b/core/config.js index d6cdadba..14f258d8 100644 --- a/core/config.js +++ b/core/config.js @@ -224,6 +224,10 @@ function getDefaultConfig() { autoUnlockMinutes : 60 * 6, // 0=disabled; Auto unlock after N minutes. }, unlockAtEmailPwReset : true, // if true, password reset via email will unlock locked accounts + + twoFactorAuth : { + method : 'googleAuth', + } }, theme : { diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index 3e486bbd..6156fa3a 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -107,7 +107,7 @@ function SSHClient(clientConn) { }; const authWithPasswordOrPubKey = (authType) => { - if(User.AuthFactor1Types.PubKey !== authType || !self.user.isAuthenticated() || !ctx.signature) { + if(User.AuthFactor1Types.SSHPubKey !== authType || !self.user.isAuthenticated() || !ctx.signature) { // step 1: login/auth using PubKey userLogin(self, ctx.username, ctx.password, { authType, ctx }, (err) => { if(err) { @@ -126,7 +126,7 @@ function SSHClient(clientConn) { }); } else { // step 2: verify signature - const pubKeyActual = ssh2.utils.parseKey(self.user.getProperty(UserProps.LoginPubKey)); + const pubKeyActual = ssh2.utils.parseKey(self.user.getProperty(UserProps.AuthPubKey)); if(!pubKeyActual || !pubKeyActual.verify(ctx.blob, ctx.signature)) { return slowTerminateConnection(); } @@ -191,7 +191,7 @@ function SSHClient(clientConn) { //return authWithPassword(); case 'publickey' : - return authWithPasswordOrPubKey(User.AuthFactor1Types.PubKey); + return authWithPasswordOrPubKey(User.AuthFactor1Types.SSHPubKey); //return authWithPubKey(); case 'keyboard-interactive' : diff --git a/core/system_menu_method.js b/core/system_menu_method.js index ea2cbc09..b83bea80 100644 --- a/core/system_menu_method.js +++ b/core/system_menu_method.js @@ -8,12 +8,14 @@ const { userLogin } = require('./user_login.js'); const messageArea = require('./message_area.js'); const { ErrorReasons } = require('./enig_error.js'); const UserProps = require('./user_property.js'); +const { user2FA_OTP } = require('./user_2fa_otp.js'); // deps const _ = require('lodash'); const iconv = require('iconv-lite'); exports.login = login; +exports.login2FA_OTP = login2FA_OTP; exports.logoff = logoff; exports.prevMenu = prevMenu; exports.nextMenu = nextMenu; @@ -23,32 +25,47 @@ exports.prevArea = prevArea; exports.nextArea = nextArea; exports.sendForgotPasswordEmail = sendForgotPasswordEmail; +const handleAuthFailures = (callingMenu, err, cb) => { + // already logged in with this user? + if(ErrorReasons.AlreadyLoggedIn === err.reasonCode && + _.has(callingMenu, 'menuConfig.config.tooNodeMenu')) + { + return callingMenu.gotoMenu(callingMenu.menuConfig.config.tooNodeMenu, cb); + } + + // banned username results in disconnect + if(ErrorReasons.NotAllowed === err.reasonCode) { + return logoff(callingMenu, {}, {}, cb); + } + + const ReasonsMenus = [ + ErrorReasons.TooMany, ErrorReasons.Disabled, ErrorReasons.Inactive, ErrorReasons.Locked + ]; + if(ReasonsMenus.includes(err.reasonCode)) { + const menu = _.get(callingMenu, [ 'menuConfig', 'config', err.reasonCode.toLowerCase() ]); + return menu ? callingMenu.gotoMenu(menu, cb) : logoff(callingMenu, {}, {}, cb); + } + + // Other error + return callingMenu.prevMenu(cb); +}; + function login(callingMenu, formData, extraArgs, cb) { userLogin(callingMenu.client, formData.value.username, formData.value.password, err => { if(err) { - // already logged in with this user? - if(ErrorReasons.AlreadyLoggedIn === err.reasonCode && - _.has(callingMenu, 'menuConfig.config.tooNodeMenu')) - { - return callingMenu.gotoMenu(callingMenu.menuConfig.config.tooNodeMenu, cb); - } + return handleAuthFailures(callingMenu, err, cb); + } - // banned username results in disconnect - if(ErrorReasons.NotAllowed === err.reasonCode) { - return logoff(callingMenu, {}, {}, cb); - } + // success! + return callingMenu.nextMenu(cb); + }); +} - const ReasonsMenus = [ - ErrorReasons.TooMany, ErrorReasons.Disabled, ErrorReasons.Inactive, ErrorReasons.Locked - ]; - if(ReasonsMenus.includes(err.reasonCode)) { - const menu = _.get(callingMenu, [ 'menuConfig', 'config', err.reasonCode.toLowerCase() ]); - return menu ? callingMenu.gotoMenu(menu, cb) : logoff(callingMenu, {}, {}, cb); - } - - // Other error - return callingMenu.prevMenu(cb); +function login2FA_OTP(callingMenu, formData, extraArgs, cb) { + user2FA_OTP(callingMenu.client, formData.value.token, err => { + if(err) { + return handleAuthFailures(callingMenu, err, cb); } // success! diff --git a/core/user.js b/core/user.js index 3b20aa76..2d4883d7 100644 --- a/core/user.js +++ b/core/user.js @@ -27,10 +27,11 @@ exports.isRootUserId = function(id) { return 1 === id; }; module.exports = class User { constructor() { - this.userId = 0; - this.username = ''; - this.properties = {}; // name:value - this.groups = []; // group membership(s) + this.userId = 0; + this.username = ''; + this.properties = {}; // name:value + this.groups = []; // group membership(s) + this.authFactor = User.AuthFactors.None; } // static property accessors @@ -38,6 +39,14 @@ module.exports = class User { return 1; } + static get AuthFactors() { + return { + None : 0, // Not yet authenticated in any way + Factor1 : 1, // username + password/pubkey/etc. checked out + Factor2 : 2, // validated with 2FA of some sort such as OTP + }; + } + static get PBKDF2() { return { iterations : 1000, @@ -50,7 +59,7 @@ module.exports = class User { return { auth : [ UserProps.PassPbkdf2Salt, UserProps.PassPbkdf2Dk, - UserProps.LoginPubKey, UserProps.LoginPubKeyFingerprintSHA256, + UserProps.AuthPubKey, ], }; } @@ -180,8 +189,9 @@ module.exports = class User { static get AuthFactor1Types() { return { - PubKey : 'pubKey', + SSHPubKey : 'sshPubKey', Password : 'password', + TLSClient : 'tlsClientAuth', }; } @@ -210,7 +220,7 @@ module.exports = class User { }; const validatePubKey = (props, callback) => { - const pubKeyActual = ssh2.utils.parseKey(props[UserProps.LoginPubKey]); + const pubKeyActual = ssh2.utils.parseKey(props[UserProps.AuthPubKey]); if(!pubKeyActual) { return callback(Errors.AccessDenied('Invalid public key')); } @@ -242,7 +252,7 @@ module.exports = class User { }); }, function validatePassOrPubKey(props, callback) { - if(User.AuthFactor1Types.PubKey === authInfo.type) { + if(User.AuthFactor1Types.SSHPubKey === authInfo.type) { return validatePubKey(props, callback); } return validatePassword(props, callback); @@ -323,7 +333,12 @@ module.exports = class User { self.username = tempAuthInfo.username; self.properties = tempAuthInfo.properties; self.groups = tempAuthInfo.groups; - self.authenticated = true; + self.authFactor = User.AuthFactors.Factor1; + + // + // If 2FA/OTP is required, this user is not quite authenticated yet. + // + self.authenticated = !(self.getProperty(UserProps.AuthFactor2OTP) ? true : false); self.removeProperty(UserProps.FailedLoginAttempts); @@ -604,7 +619,10 @@ module.exports = class User { user.username = userName; user.properties = properties; user.groups = groups; - user.authenticated = false; // this is NOT an authenticated user! + + // explicitly NOT an authenticated user! + user.authenticated = false; + user.authFactor = User.AuthFactors.None; return cb(err, user); } diff --git a/core/user_property.js b/core/user_property.js index c3aabcd0..f3f3b652 100644 --- a/core/user_property.js +++ b/core/user_property.js @@ -58,7 +58,10 @@ module.exports = { MinutesOnlineTotalCount : 'minutes_online_total_count', - LoginPubKey : 'login_public_key', // OpenSSL format - //LoginPubKeyFingerprintSHA256 : 'login_public_key_fp_sha256', // hint: ssh-kegen -lf id_rsa.pub + SSHPubKey : 'ssh_public_key', // OpenSSH format (ssh-keygen, etc.) + AuthFactor1Types : 'auth_factor1_types', // List of User.AuthFactor1Types value(s) + AuthFactor2OTP : 'auth_factor2_otp', // If present, OTP type for 2FA + AuthFactor2OTPSecret : 'auth_factor2_otp_secret', // Secret used in conjunction with OTP 2FA + AuthFactor2OTPScratchCodes : 'auth_factor2_otp_scratch', // JSON array style codes ["code1", "code2", ...] }; diff --git a/docs/configuration/acs.md b/docs/configuration/acs.md index d0a45d06..64d67648 100644 --- a/docs/configuration/acs.md +++ b/docs/configuration/acs.md @@ -37,8 +37,8 @@ The following are ACS codes available as of this writing: | MMminutes | It is currently >= _minutes_ past midnight (system time) | | ACachievementCount | User has >= _achievementCount_ achievements | | APachievementPoints | User has >= _achievementPoints_ achievement points | - -\* Many more ACS codes are planned for the near future. +| AFauthFactor | User's current *Authentication Factor* is >= _authFactor_. Authentication factor 1 refers to username + password (or PubKey) while factor 2 refers to 2FA such as One-Time-Password authentication. | +| ARauthFactorReq | Current users **requires** an Authentication Factor >= _authFactorReq_ | ## ACS Strings ACS strings are one or more ACS codes in addition to some basic language semantics. diff --git a/docs/configuration/menu-hjson.md b/docs/configuration/menu-hjson.md index a59e5b24..9294c08c 100644 --- a/docs/configuration/menu-hjson.md +++ b/docs/configuration/menu-hjson.md @@ -86,6 +86,7 @@ Many built in global/system methods exist. Below are a few. See [system_menu_met | Method | Description | |--------|-------------| | `login` | Performs a standard login. | +| `login2FA_OTP` | Performs a 2-Factor Authentication (2FA) One-Time Password (OTP) check, if configured for the user. | | `logoff` | Performs a standard system logoff. | | `prevMenu` | Goes to the previous menu. | | `nextMenu` | Goes to the next menu (as set by `next`) | diff --git a/misc/acs_parser.pegjs b/misc/acs_parser.pegjs index 8a39deea..060344a8 100644 --- a/misc/acs_parser.pegjs +++ b/misc/acs_parser.pegjs @@ -2,6 +2,7 @@ { const UserProps = require('./user_property.js'); const Log = require('./logger.js').log; + const User = require('./user.js'); const _ = require('lodash'); const moment = require('moment'); @@ -138,6 +139,22 @@ SC : function isSecureConnection() { return _.get(client, 'session.isSecure', false); }, + AF : function currentAuthFactor() { + if(!user) { + return false; + } + return !isNaN(value) && user.authFactor >= value; + }, + AR : function authFactorRequired() { + if(!user) { + return false; + } + switch(value) { + case 1 : return user.authFactor >= User.AuthFactors.Factor1; + case 2 : return user.authFactor >= User.AuthFActors.Factor2; + default : return false; + } + }, ML : function minutesLeft() { // :TODO: implement me! return false; diff --git a/package.json b/package.json index 299074d6..4a700f1d 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,8 @@ "uuid-parse": "^1.0.0", "ws": "^6.1.3", "xxhash": "^0.2.4", - "yazl": "^2.5.1" + "yazl": "^2.5.1", + "otplib": "^10.0.1" }, "devDependencies": {}, "engines": { diff --git a/yarn.lock b/yarn.lock index 6a922b8f..09bf3e98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1478,6 +1478,13 @@ osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" +otplib@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/otplib/-/otplib-10.0.1.tgz#d37fcd13203298c0b94937d55c5a3527ed877875" + integrity sha512-FtbKelYtio2af5LDBWz3bWS6T03taHJAIv3evMrXuvoM50z5jbWoEMabPCk0A0JqiLGBzAIDJWfR9gSsvRYZHA== + dependencies: + thirty-two "1.0.2" + p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" @@ -2026,6 +2033,11 @@ temptmp@^1.1.0: dependencies: del "^3.0.0" +thirty-two@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/thirty-two/-/thirty-two-1.0.2.tgz#4ca2fffc02a51290d2744b9e3f557693ca6b627a" + integrity sha1-TKL//AKlEpDSdEueP1V2k8prYno= + through2@^2.0.3: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" From d54338c46e8df77b7d8b0c996849b8eba73dc07b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 9 Apr 2019 20:24:52 -0600 Subject: [PATCH 014/140] Listen 'address' for Gopher --- core/servers/content/gopher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/servers/content/gopher.js b/core/servers/content/gopher.js index ee889b8c..4e51c889 100644 --- a/core/servers/content/gopher.js +++ b/core/servers/content/gopher.js @@ -113,7 +113,7 @@ exports.getModule = class GopherModule extends ServerModule { return cb(Errors.Invalid(`Invalid port: ${config.contentServers.gopher.port}`)); } - return this.server.listen(port, cb); + return this.server.listen(port, config.contentServers.gopher.address, cb); } get enabled() { From 3460b98bf5bff2818d1a13fbef924eae1dc40599 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 9 Apr 2019 20:25:03 -0600 Subject: [PATCH 015/140] Listen 'address' for Web --- core/servers/content/web.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/servers/content/web.js b/core/servers/content/web.js index 1a09aace..04b9ccdd 100644 --- a/core/servers/content/web.js +++ b/core/servers/content/web.js @@ -138,7 +138,7 @@ exports.getModule = class WebServerModule extends ServerModule { return nextService(Errors.Invalid(`Invalid port: ${config.contentServers.web[service].port}`)); } - this[name].listen(port, err => { + this[name].listen(port, config.contentServers.web[service].address, err => { return nextService(err); }); } else { From 8114a1e3f21f6b8e721ec272d479d664a79ebff1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 9 Apr 2019 20:25:14 -0600 Subject: [PATCH 016/140] Listen 'address' for Telnet --- core/servers/login/telnet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index fb6ca745..1b892e71 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -875,7 +875,7 @@ exports.getModule = class TelnetServerModule extends LoginServerModule { return cb(Errors.Invalid(`Invalid port: ${config.loginServers.telnet.port}`)); } - this.server.listen(port, err => { + this.server.listen(port, config.loginServers.telnet.address, err => { if(!err) { Log.info( { server : ModuleInfo.name, port : port }, 'Listening for connections' ); } From 37ea1e3a3032087cefdeffcad1068370326dd0d2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 9 Apr 2019 20:25:27 -0600 Subject: [PATCH 017/140] Listen 'address' for SSH --- core/servers/login/ssh.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index ee63ac78..acdabf26 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -353,7 +353,7 @@ exports.getModule = class SSHServerModule extends LoginServerModule { return cb(Errors.Invalid(`Invalid port: ${config.loginServers.ssh.port}`)); } - this.server.listen(port, err => { + this.server.listen(port, config.loginServers.ssh.address, err => { if(!err) { Log.info( { server : ModuleInfo.name, port : port }, 'Listening for connections' ); } From 50426d0e606c5bf455ecf456d2bb52bc864af876 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 9 Apr 2019 20:25:37 -0600 Subject: [PATCH 018/140] Listen 'address' for WebSockets --- core/servers/login/websocket.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/servers/login/websocket.js b/core/servers/login/websocket.js index 35bc0757..6a6b01dc 100644 --- a/core/servers/login/websocket.js +++ b/core/servers/login/websocket.js @@ -200,7 +200,8 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { } const serverName = `${ModuleInfo.name} (${serverType})`; - const confPort = _.get(Config(), [ 'loginServers', 'webSocket', 'secure' === serverType ? 'wss' : 'ws', 'port' ] ); + const conf = _.get(Config(), [ 'loginServers', 'webSocket', 'secure' === serverType ? 'wss' : 'ws' ] ); + const confPort = conf.port; const port = parseInt(confPort); if(isNaN(port)) { @@ -208,7 +209,7 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { return nextServerType(Errors.Invalid(`Invalid port: ${confPort}`)); } - server.httpServer.listen(port, err => { + server.httpServer.listen(port, conf.address, err => { if(err) { return nextServerType(err); } From 509831cc0c36d6836861ff890b65bfceef543c9b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 23 Apr 2019 18:16:55 -0600 Subject: [PATCH 019/140] Better handling of active door instances --- core/abracadabra.js | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/core/abracadabra.js b/core/abracadabra.js index 36a58544..9ac4dec7 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -84,6 +84,22 @@ exports.getModule = class AbracadabraModule extends MenuModule { * Font support ala all other menus... or does this just work? */ + incrementActiveDoorNodeInstances() { + if(activeDoorNodeInstances[this.config.name]) { + activeDoorNodeInstances[this.config.name] += 1; + } else { + activeDoorNodeInstances[this.config.name] = 1; + } + this.activeDoorInstancesIncremented = true; + } + + decrementActiveDoorNodeInstances() { + if(true === this.activeDoorInstancesIncremented) { + activeDoorNodeInstances[this.config.name] -= 1; + this.activeDoorInstancesIncremented = false; + } + } + initSequence() { const self = this; @@ -116,14 +132,8 @@ exports.getModule = class AbracadabraModule extends MenuModule { }); } } else { - // :TODO: JS elegant way to do this? - if(activeDoorNodeInstances[self.config.name]) { - activeDoorNodeInstances[self.config.name] += 1; - } else { - activeDoorNodeInstances[self.config.name] = 1; - } - - callback(null); + self.incrementActiveDoorNodeInstances(); + return callback(null); } }, function prepareDoor(callback) { @@ -169,6 +179,7 @@ exports.getModule = class AbracadabraModule extends MenuModule { this.doorInstance.run(exeInfo, () => { trackDoorRunEnd(doorTracking); + this.decrementActiveDoorNodeInstances(); // client may have disconnected while process was active - // we're done here if so. @@ -194,9 +205,7 @@ exports.getModule = class AbracadabraModule extends MenuModule { leave() { super.leave(); - if(!this.lastError) { - activeDoorNodeInstances[this.config.name] -= 1; - } + this.decrementActiveDoorNodeInstances(); } finishedLoading() { From 7a98198698dbe1882009f79c0c6f682b110b83d8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 23 Apr 2019 18:17:40 -0600 Subject: [PATCH 020/140] Some doc updates --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 167f5288..b72d42cb 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ See [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) for more * Use [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) * **Discussion on a ENiGMA BBS!** (see Boards below) * IRC: **#enigma-bbs** on **chat.freenode.net** ([webchat](https://webchat.freenode.net/?channels=enigma-bbs)) -* Discussion on [fsxNet](http://bbs.geek.nz/#fsxNet) available on many boards +* FSX_ENG on [fsxNet](http://bbs.geek.nz/#fsxNet) available on many boards * Email: bryan -at- l33t.codes * [Facebook ENiGMA½ group](https://www.facebook.com/groups/enigmabbs/) @@ -76,7 +76,7 @@ Please see [Installation Methods](https://nuskooler.github.io/enigma-bbs/install * [Luciano Ayres](http://www.lucianoayres.com.br/) of [Blocktronics](http://blocktronics.org/), creator of the "Mystery Skulls" default ENiGMA½ theme! * Sudndeath for Xibalba ANSI work! * Jack Phlash for kick ass ENiGMA½ and Xibalba ASCII (Check out [IMPURE60](http://pc.textmod.es/pack/impure60/)!!) -* Avon of [Agency BBS](http://bbs.geek.nz/) and [fsxNet](http://bbs.geek.nz/#fsxNet) for putting up with my experiments to his system +* Avon of [Agency BBS](http://bbs.geek.nz/) and [fsxNet](http://bbs.geek.nz/#fsxNet) for putting up with my experiments to his system and for FSX_ENG! * Maskreet of [Throwback BBS](http://www.throwbackbbs.com/) hosting [DoorParty](http://forums.throwbackbbs.com/)! * [Apam](https://github.com/apamment) of [Magicka](https://magickabbs.com/) * [nail/blocktronics](http://blocktronics.org/tag/nail/) for the [sickmade Xibalba logo](http://pc.textmod.es/pack/blocktronics-420/n-xbalba.ans)! From e5398db07b28a9d8153ec5ad256f60ecce57d4c3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 7 May 2019 21:36:33 -0600 Subject: [PATCH 021/140] WIP on OTP 2FA, stats, etc. --- core/enig_error.js | 1 + core/user_login.js | 103 +++++++++++++++++++++++++-------------------- 2 files changed, 59 insertions(+), 45 deletions(-) diff --git a/core/enig_error.js b/core/enig_error.js index 33771564..08a3312e 100644 --- a/core/enig_error.js +++ b/core/enig_error.js @@ -51,4 +51,5 @@ exports.ErrorReasons = { Inactive : 'INACTIVE', Locked : 'LOCKED', NotAllowed : 'NOTALLOWED', + Invalid2FA : 'INVALID2FA', }; diff --git a/core/user_login.js b/core/user_login.js index 98bdeefb..ed2ba6a5 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -21,7 +21,9 @@ const User = require('./user.js'); const async = require('async'); const _ = require('lodash'); -exports.userLogin = userLogin; +exports.userLogin = userLogin; +exports.recordLogin = recordLogin; +exports.transformLoginError = transformLoginError; function userLogin(client, username, password, options, cb) { if(!cb && _.isFunction(options)) { @@ -50,20 +52,13 @@ function userLogin(client, username, password, options, cb) { client.user.authenticateFactor1(authInfo, err => { if(err) { - client.user.sessionFailedLoginAttempts = _.get(client.user, 'sessionFailedLoginAttempts', 0) + 1; - const disconnect = config.users.failedLogin.disconnect; - if(disconnect > 0 && client.user.sessionFailedLoginAttempts >= disconnect) { - err = Errors.BadLogin('To many failed login attempts', ErrorReasons.TooMany); - } - - client.log.info( { username, ip : client.remoteAddress, reason : err.message }, 'Failed login attempt'); - return cb(err); + return cb(transformLoginError(err, client, username)); } const user = client.user; // Good login; reset any failed attempts - delete user.sessionFailedLoginAttempts; + delete client.sessionFailedLoginAttempts; // // Ensure this user is not already logged in. @@ -104,41 +99,59 @@ function userLogin(client, username, password, options, cb) { Events.emit(Events.getSystemEvents().UserLogin, { user } ); - async.parallel( - [ - function setTheme(callback) { - setClientTheme(client, user.properties[UserProps.ThemeId]); - return callback(null); - }, - function updateSystemLoginCount(callback) { - StatLog.incrementNonPersistentSystemStat(SysProps.LoginsToday, 1); - return StatLog.incrementSystemStat(SysProps.LoginCount, 1, callback); - }, - function recordLastLogin(callback) { - return StatLog.setUserStat(user, UserProps.LastLoginTs, StatLog.now, callback); - }, - function updateUserLoginCount(callback) { - return StatLog.incrementUserStat(user, UserProps.LoginCount, 1, callback); - }, - function recordLoginHistory(callback) { - const loginHistoryMax = Config().statLog.systemEvents.loginHistoryMax; - const historyItem = JSON.stringify({ - userId : user.userId, - sessionId : user.sessionId, - }); + setClientTheme(client, user.properties[UserProps.ThemeId]); + if(user.authenticated) { + return recordLogin(client, cb); + } - return StatLog.appendSystemLogEntry( - SystemLogKeys.UserLoginHistory, - historyItem, - loginHistoryMax, - StatLog.KeepType.Max, - callback - ); - } - ], - err => { - return cb(err); - } - ); + // recordLogin() must happen after 2FA! + return cb(null); }); +} + +function recordLogin(client, cb) { + const user = client.user; + async.parallel( + [ + (callback) => { + StatLog.incrementNonPersistentSystemStat(SysProps.LoginsToday, 1); + return StatLog.incrementSystemStat(SysProps.LoginCount, 1, callback); + }, + (callback) => { + return StatLog.setUserStat(user, UserProps.LastLoginTs, StatLog.now, callback); + }, + (callback) => { + return StatLog.incrementUserStat(user, UserProps.LoginCount, 1, callback); + }, + (callback) => { + const loginHistoryMax = Config().statLog.systemEvents.loginHistoryMax; + const historyItem = JSON.stringify({ + userId : user.userId, + sessionId : user.sessionId, + }); + + return StatLog.appendSystemLogEntry( + SystemLogKeys.UserLoginHistory, + historyItem, + loginHistoryMax, + StatLog.KeepType.Max, + callback + ); + } + ], + err => { + return cb(err); + } + ); +} + +function transformLoginError(err, client, username) { + client.sessionFailedLoginAttempts = _.get(client, 'sessionFailedLoginAttempts', 0) + 1; + const disconnect = Config().users.failedLogin.disconnect; + if(disconnect > 0 && client.sessionFailedLoginAttempts >= disconnect) { + err = Errors.BadLogin('To many failed login attempts', ErrorReasons.TooMany); + } + + client.log.info( { username, ip : client.remoteAddress, reason : err.message }, 'Failed login attempt'); + return err; } \ No newline at end of file From 3c6c8d2a5c66ffc11663772bb316dae6287bce4e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 8 May 2019 21:06:10 -0600 Subject: [PATCH 022/140] Add missing OTP file, minor updates --- core/system_menu_method.js | 6 +- core/user_2fa_otp.js | 188 +++++++++++++++++++++++++++++++++++++ core/user_property.js | 2 +- 3 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 core/user_2fa_otp.js diff --git a/core/system_menu_method.js b/core/system_menu_method.js index b83bea80..ba9bb699 100644 --- a/core/system_menu_method.js +++ b/core/system_menu_method.js @@ -8,7 +8,9 @@ const { userLogin } = require('./user_login.js'); const messageArea = require('./message_area.js'); const { ErrorReasons } = require('./enig_error.js'); const UserProps = require('./user_property.js'); -const { user2FA_OTP } = require('./user_2fa_otp.js'); +const { + loginFactor2_OTP +} = require('./user_2fa_otp.js'); // deps const _ = require('lodash'); @@ -63,7 +65,7 @@ function login(callingMenu, formData, extraArgs, cb) { } function login2FA_OTP(callingMenu, formData, extraArgs, cb) { - user2FA_OTP(callingMenu.client, formData.value.token, err => { + loginFactor2_OTP(callingMenu.client, formData.value.token, err => { if(err) { return handleAuthFailures(callingMenu, err, cb); } diff --git a/core/user_2fa_otp.js b/core/user_2fa_otp.js new file mode 100644 index 00000000..b0df5009 --- /dev/null +++ b/core/user_2fa_otp.js @@ -0,0 +1,188 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const UserProps = require('./user_property.js'); +const { + Errors, + ErrorReasons, +} = require('./enig_error.js'); +const User = require('./user.js'); +const { + recordLogin, + transformLoginError, +} = require('./user_login.js'); + +// deps +const _ = require('lodash'); +const crypto = require('crypto'); +const async = require('async'); + +exports.loginFactor2_OTP = loginFactor2_OTP; +exports.generateNewBackupCodes = generateNewBackupCodes; + +const OTPTypes = exports.OTPTypes = { + RFC6238_TOTP : 'rfc6238_TOTP', // Time-Based, SHA-512 + RFC4266_HOTP : 'rfc4266_HOTP', // HMAC-Based, SHA-512 + GoogleAuthenticator : 'googleAuth', // Google Authenticator is basically TOTP + quirks +}; + +function otpFromType(otpType) { + return { + [ OTPTypes.RFC6238_TOTP ] : () => { + const totp = require('otplib/totp'); + totp.options = { crypto, algorithm : 'sha256' }; + return totp; + }, + [ OTPTypes.RFC4266_HOTP ] : () => { + const hotp = require('otplib/hotp'); + hotp.options = { crypto, algorithm : 'sha256' }; + return hotp; + }, + [ OTPTypes.GoogleAuthenticator ] : () => { + const googleAuth = require('otplib/authenticator'); + googleAuth.options = { crypto }; + return googleAuth; + }, + }[otpType](); +} + +function generateOTPBackupCode() { + const consonants = 'bdfghjklmnprstvz'.split(''); + const vowels = 'aiou'.split(''); + + const bits = []; + const rng = crypto.randomBytes(4); + + for(let i = 0; i < rng.length / 2; ++i) { + const n = rng.readUInt16BE(i * 2); + + const c1 = n & 0x0f; + const v1 = (n >> 4) & 0x03; + const c2 = (n >> 6) & 0x0f; + const v2 = (n >> 10) & 0x03; + const c3 = (n >> 12) & 0x0f; + + bits.push([ + consonants[c1], + vowels[v1], + consonants[c2], + vowels[v2], + consonants[c3], + ].join('')); + } + + return bits.join('-'); +} + +function backupCodePBKDF2(secret, salt, cb) { + return crypto.pbkdf2(secret, salt, 1000, 128, 'sha1', cb); +} + +function generateNewBackupCodes(user, cb) { + // + // Backup codes are not stored in plain text, but rather + // an array of objects: [{salt, code}, ...] + // + const plainCodes = [...Array(6)].map(() => generateOTPBackupCode()); + async.map(plainCodes, (code, nextCode) => { + crypto.randomBytes(16, (err, salt) => { + if(err) { + return nextCode(err); + } + salt = salt.toString('base64'); + backupCodePBKDF2(code, salt, (err, code) => { + if(err) { + return nextCode(err); + } + code = code.toString('base64'); + return nextCode(null, { salt, code }); + }); + }); + }, + (err, codes) => { + if(err) { + return cb(err); + } + + codes = JSON.stringify(codes); + user.persistProperty(UserProps.AuthFactor2OTPBackupCodes, codes, err => { + return cb(err, plainCodes); + }); + }); +} + +function validateAndConsumeBackupCode(user, token, cb) { + try + { + let validCodes = JSON.parse(user.getProperty(UserProps.AuthFactor2OTPBackupCodes)); + async.detect(validCodes, (entry, nextEntry) => { + backupCodePBKDF2(token, entry.salt, (err, code) => { + if(err) { + return nextEntry(err); + } + code = code.toString('base64'); + return nextEntry(null, code === entry.code); + }); + }, + (err, matchingEntry) => { + if(err) { + return cb(err); + } + + if(!matchingEntry) { + return cb(Errors.BadLogin('Invalid OTP value supplied', ErrorReasons.Invalid2FA)); + } + + // We're consuming a match - remove it from available backup codes + validCodes = validCodes.filter(entry => { + return entry.code != matchingEntry.code && entry.salt != matchingEntry.salt; + }); + + validCodes = JSON.stringify(validCodes); + user.persistProperty(UserProps.AuthFactor2OTPBackupCodes, validCodes, err => { + return cb(err); + }); + }); + } catch(e) { + return cb(e); + } +} + +function loginFactor2_OTP(client, token, cb) { + if(client.user.authFactor < User.AuthFactors.Factor1) { + return cb(Errors.AccessDenied('OTP requires prior authentication factor 1')); + } + + const otpType = client.user.getProperty(UserProps.AuthFactor2OTP); + if(!_.values(OTPTypes).includes(otpType)) { + return cb(Errors.Invalid(`Unknown OTP type: ${otpType}`)); + } + + const secret = client.user.getProperty(UserProps.AuthFactor2OTPSecret); + if(!secret) { + return cb(Errors.Invalid('Missing OTP secret')); + } + + const otp = otpFromType(otpType); + const valid = otp.verify( { token, secret } ); + + const allowLogin = () => { + client.user.authFactor = User.AuthFactors.Factor2; + client.user.authenticated = true; + return recordLogin(client, cb); + }; + + if(valid) { + return allowLogin(); + } + + // maybe they punched in a backup code? + validateAndConsumeBackupCode(client.user, token, err => { + if(err) { + return cb(transformLoginError(err, client, client.user.username)); + } + + return allowLogin(); + }); +} diff --git a/core/user_property.js b/core/user_property.js index f3f3b652..02fac923 100644 --- a/core/user_property.js +++ b/core/user_property.js @@ -62,6 +62,6 @@ module.exports = { AuthFactor1Types : 'auth_factor1_types', // List of User.AuthFactor1Types value(s) AuthFactor2OTP : 'auth_factor2_otp', // If present, OTP type for 2FA AuthFactor2OTPSecret : 'auth_factor2_otp_secret', // Secret used in conjunction with OTP 2FA - AuthFactor2OTPScratchCodes : 'auth_factor2_otp_scratch', // JSON array style codes ["code1", "code2", ...] + AuthFactor2OTPBackupCodes : 'auth_factor2_otp_backup', // JSON array of backup codes: [{salt,code}, ...] }; From 6070bc94e75aa2ebe7476f48c3eed36c00cbabad Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 9 May 2019 19:56:04 -0600 Subject: [PATCH 023/140] Good progress on QR support --- core/oputil/oputil_user.js | 62 +++++++++++++++++++++- core/user_2fa_otp.js | 104 ++++++++++++++++++++++++------------- core/user_login.js | 3 ++ package.json | 3 +- yarn.lock | 5 ++ 5 files changed, 139 insertions(+), 38 deletions(-) diff --git a/core/oputil/oputil_user.js b/core/oputil/oputil_user.js index 33327d1d..a2725a71 100644 --- a/core/oputil/oputil_user.js +++ b/core/oputil/oputil_user.js @@ -16,6 +16,7 @@ const UserProps = require('../user_property.js'); const async = require('async'); const _ = require('lodash'); const moment = require('moment'); +const fs = require('fs-extra'); exports.handleUserCommand = handleUserCommand; @@ -343,6 +344,62 @@ Affiliations : ${propOrNA(UserProps.Affiliations)} `); } +function twoFactorAuth(user) { + if(argv._.length < 4) { + return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); + } + + const { + OTPTypes, + prepareOTP, + } = require('../../core/user_2fa_otp.js'); + + async.waterfall( + [ + function validate(callback) { + // :TODO: Prompt for if not supplied + let otpType = argv._[argv._.length - 1]; + otpType = _.find(OTPTypes, t => { + return t.toLowerCase() === otpType; + }); + if(!otpType) { + return callback(Errors.Invalid('Invalid OTP type')); + } + return callback(null, otpType); + }, + function prepare(otpType, callback) { + const otpOpts = { + username : user.username, + qrType : argv['qr-type'] || 'ascii', + }; + prepareOTP(otpType, otpOpts, (err, otpInfo) => { + return callback(err, otpInfo); + }); + }, + function storeOrDisplayQR(otpInfo, callback) { + if(!argv.out) { + return callback(null, otpInfo); + } + + if('-' === argv.out) { + console.info(otpInfo.qr); + return callback(null, otpInfo); + } + + fs.writeFile(argv.out, otpInfo.qr, 'utf8', err => { + return callback(err, otpInfo); + }); + } + ], + (err) => { + if(err) { + console.error(err.message); + } else { + } + } + ); +} + function handleUserCommand() { function errUsage() { return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); @@ -356,7 +413,8 @@ function handleUserCommand() { const usernameIdx = [ 'pw', 'pass', 'passwd', 'password', 'group', - 'mv', 'rename' + 'mv', 'rename', + '2fa', ].includes(action) ? argv._.length - 2 : argv._.length - 1; const userName = argv._[usernameIdx]; @@ -391,6 +449,8 @@ function handleUserCommand() { group : modUserGroups, info : showUserInfo, + + '2fa' : twoFactorAuth, }[action] || errUsage)(user, action); }); } \ No newline at end of file diff --git a/core/user_2fa_otp.js b/core/user_2fa_otp.js index b0df5009..eadb09eb 100644 --- a/core/user_2fa_otp.js +++ b/core/user_2fa_otp.js @@ -12,14 +12,16 @@ const { recordLogin, transformLoginError, } = require('./user_login.js'); +const Config = require('./config.js').get; // deps const _ = require('lodash'); const crypto = require('crypto'); const async = require('async'); +const qrGen = require('qrcode-generator'); -exports.loginFactor2_OTP = loginFactor2_OTP; -exports.generateNewBackupCodes = generateNewBackupCodes; +exports.prepareOTP = prepareOTP; +exports.loginFactor2_OTP = loginFactor2_OTP; const OTPTypes = exports.OTPTypes = { RFC6238_TOTP : 'rfc6238_TOTP', // Time-Based, SHA-512 @@ -28,23 +30,27 @@ const OTPTypes = exports.OTPTypes = { }; function otpFromType(otpType) { - return { - [ OTPTypes.RFC6238_TOTP ] : () => { - const totp = require('otplib/totp'); - totp.options = { crypto, algorithm : 'sha256' }; - return totp; - }, - [ OTPTypes.RFC4266_HOTP ] : () => { - const hotp = require('otplib/hotp'); - hotp.options = { crypto, algorithm : 'sha256' }; - return hotp; - }, - [ OTPTypes.GoogleAuthenticator ] : () => { - const googleAuth = require('otplib/authenticator'); - googleAuth.options = { crypto }; - return googleAuth; - }, - }[otpType](); + try { + return { + [ OTPTypes.RFC6238_TOTP ] : () => { + const totp = require('otplib/totp'); + totp.options = { crypto, algorithm : 'sha256' }; + return totp; + }, + [ OTPTypes.RFC4266_HOTP ] : () => { + const hotp = require('otplib/hotp'); + hotp.options = { crypto, algorithm : 'sha256' }; + return hotp; + }, + [ OTPTypes.GoogleAuthenticator ] : () => { + const googleAuth = require('otplib/authenticator'); + googleAuth.options = { crypto }; + return googleAuth; + }, + }[otpType](); + } catch(e) { + // nothing + } } function generateOTPBackupCode() { @@ -79,13 +85,9 @@ function backupCodePBKDF2(secret, salt, cb) { return crypto.pbkdf2(secret, salt, 1000, 128, 'sha1', cb); } -function generateNewBackupCodes(user, cb) { - // - // Backup codes are not stored in plain text, but rather - // an array of objects: [{salt, code}, ...] - // - const plainCodes = [...Array(6)].map(() => generateOTPBackupCode()); - async.map(plainCodes, (code, nextCode) => { +function generateNewBackupCodes(cb) { + const plainTextCodes = [...Array(6)].map(() => generateOTPBackupCode()); + async.map(plainTextCodes, (code, nextCode) => { crypto.randomBytes(16, (err, salt) => { if(err) { return nextCode(err); @@ -101,14 +103,7 @@ function generateNewBackupCodes(user, cb) { }); }, (err, codes) => { - if(err) { - return cb(err); - } - - codes = JSON.stringify(codes); - user.persistProperty(UserProps.AuthFactor2OTPBackupCodes, codes, err => { - return cb(err, plainCodes); - }); + return cb(err, codes, plainTextCodes); }); } @@ -149,13 +144,51 @@ function validateAndConsumeBackupCode(user, token, cb) { } } +function createQRCode(otp, options, secret) { + const uri = otp.keyuri(options.username || 'user', Config().general.boardName, secret); + const qrCode = qrGen(0, 'L'); + qrCode.addData(uri); + qrCode.make(); + + options.qrType = options.qrType || 'ascii'; + return { + ascii : qrCode.createASCII, + data : qrCode.createDataURL, + img : qrCode.createImgTag, + svg : qrCode.createSvgTag, + }[options.qrType](); +} + +function prepareOTP(otpType, options, cb) { + if(!_.isFunction(cb)) { + cb = options; + options = {}; + } + + const otp = otpFromType(otpType); + if(!otp) { + return cb(Errors.Invalid(`Unknown OTP type: ${otpType}`)); + } + + const secret = OTPTypes.GoogleAuthenticator === otpType ? + otp.generateSecret() : + crypto.randomBytes(64).toString('base64').substr(0, 32); + + generateNewBackupCodes((err, codes, plainTextCodes) => { + const qr = createQRCode(otp, options, secret); + return cb(err, { secret, codes, plainTextCodes, qr } ); + }); +} + function loginFactor2_OTP(client, token, cb) { if(client.user.authFactor < User.AuthFactors.Factor1) { return cb(Errors.AccessDenied('OTP requires prior authentication factor 1')); } const otpType = client.user.getProperty(UserProps.AuthFactor2OTP); - if(!_.values(OTPTypes).includes(otpType)) { + const otp = otpFromType(otpType); + + if(!otp) { return cb(Errors.Invalid(`Unknown OTP type: ${otpType}`)); } @@ -164,7 +197,6 @@ function loginFactor2_OTP(client, token, cb) { return cb(Errors.Invalid('Missing OTP secret')); } - const otp = otpFromType(otpType); const valid = otp.verify( { token, secret } ); const allowLogin = () => { diff --git a/core/user_login.js b/core/user_login.js index ed2ba6a5..bc23e3b3 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -20,6 +20,7 @@ const User = require('./user.js'); // deps const async = require('async'); const _ = require('lodash'); +const assert = require('assert'); exports.userLogin = userLogin; exports.recordLogin = recordLogin; @@ -110,6 +111,8 @@ function userLogin(client, username, password, options, cb) { } function recordLogin(client, cb) { + assert(client.user.authenticated); // don't get in situations where this isn't true + const user = client.user; async.parallel( [ diff --git a/package.json b/package.json index 4a700f1d..bbc8a94f 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,8 @@ "ws": "^6.1.3", "xxhash": "^0.2.4", "yazl": "^2.5.1", - "otplib": "^10.0.1" + "otplib": "^10.0.1", + "qrcode-generator": "1.4.3" }, "devDependencies": {}, "engines": { diff --git a/yarn.lock b/yarn.lock index 09bf3e98..0b081218 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1570,6 +1570,11 @@ punycode@^1.4.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= +qrcode-generator@1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/qrcode-generator/-/qrcode-generator-1.4.3.tgz#4876e8f280e65b6c94615f4c19c484f6b964b199" + integrity sha512-++rVRvMRq5BlHfmAafl8a4ppUntzUxCCUTT2t0siUgqKwdnqRzY8IH6f6WSX5dZUhD2Ul5/MIKuTJddflwrGzw== + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" From 2767f3c4e3caab30c90d7797cd83c0459059c43a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 9 May 2019 20:25:47 -0600 Subject: [PATCH 024/140] Don't store hashed versions of backup codes * Really no point; secret must be in plain-text and only ever used in conjunction with pass/etc. * Better oputil handling --- core/oputil/oputil_user.js | 29 ++++++++++++---- core/user_2fa_otp.js | 70 +++++++++----------------------------- core/user_property.js | 2 +- 3 files changed, 39 insertions(+), 62 deletions(-) diff --git a/core/oputil/oputil_user.js b/core/oputil/oputil_user.js index a2725a71..77e0be68 100644 --- a/core/oputil/oputil_user.js +++ b/core/oputil/oputil_user.js @@ -373,7 +373,7 @@ function twoFactorAuth(user) { qrType : argv['qr-type'] || 'ascii', }; prepareOTP(otpType, otpOpts, (err, otpInfo) => { - return callback(err, otpInfo); + return callback(err, Object.assign(otpInfo, { otpType })); }); }, function storeOrDisplayQR(otpInfo, callback) { @@ -381,20 +381,35 @@ function twoFactorAuth(user) { return callback(null, otpInfo); } - if('-' === argv.out) { - console.info(otpInfo.qr); - return callback(null, otpInfo); - } - fs.writeFile(argv.out, otpInfo.qr, 'utf8', err => { return callback(err, otpInfo); }); + }, + function persist(otpInfo, callback) { + const props = { + [ UserProps.AuthFactor2OTP ] : otpInfo.otpType, + [ UserProps.AuthFactor2OTPSecret ] : otpInfo.secret, + [ UserProps.AuthFactor2OTPBackupCodes ] : JSON.stringify(otpInfo.backupCodes), + }; + user.persistProperties(props, err => { + return callback(err, otpInfo); + }); } ], - (err) => { + (err, otpInfo) => { if(err) { console.error(err.message); } else { + console.info(`OTP enabled for ${user.username}.`); + console.info(`Secret: ${otpInfo.secret}`); + console.info(`Backup codes: ${otpInfo.backupCodes.join(', ')}`); + + if(!argv.out) { + console.info('QR code:'); + console.info(otpInfo.qr); + } else { + console.info(`QR code saved to ${argv.out}`); + } } } ); diff --git a/core/user_2fa_otp.js b/core/user_2fa_otp.js index eadb09eb..83524750 100644 --- a/core/user_2fa_otp.js +++ b/core/user_2fa_otp.js @@ -81,63 +81,25 @@ function generateOTPBackupCode() { return bits.join('-'); } -function backupCodePBKDF2(secret, salt, cb) { - return crypto.pbkdf2(secret, salt, 1000, 128, 'sha1', cb); -} - -function generateNewBackupCodes(cb) { - const plainTextCodes = [...Array(6)].map(() => generateOTPBackupCode()); - async.map(plainTextCodes, (code, nextCode) => { - crypto.randomBytes(16, (err, salt) => { - if(err) { - return nextCode(err); - } - salt = salt.toString('base64'); - backupCodePBKDF2(code, salt, (err, code) => { - if(err) { - return nextCode(err); - } - code = code.toString('base64'); - return nextCode(null, { salt, code }); - }); - }); - }, - (err, codes) => { - return cb(err, codes, plainTextCodes); - }); +function generateNewBackupCodes() { + const codes = [...Array(6)].map(() => generateOTPBackupCode()); + return codes; } function validateAndConsumeBackupCode(user, token, cb) { try { let validCodes = JSON.parse(user.getProperty(UserProps.AuthFactor2OTPBackupCodes)); - async.detect(validCodes, (entry, nextEntry) => { - backupCodePBKDF2(token, entry.salt, (err, code) => { - if(err) { - return nextEntry(err); - } - code = code.toString('base64'); - return nextEntry(null, code === entry.code); - }); - }, - (err, matchingEntry) => { - if(err) { - return cb(err); - } + const matchingCode = validCodes.find(c => c === token); + if(!matchingCode) { + return cb(Errors.BadLogin('Invalid OTP value supplied', ErrorReasons.Invalid2FA)); + } - if(!matchingEntry) { - return cb(Errors.BadLogin('Invalid OTP value supplied', ErrorReasons.Invalid2FA)); - } - - // We're consuming a match - remove it from available backup codes - validCodes = validCodes.filter(entry => { - return entry.code != matchingEntry.code && entry.salt != matchingEntry.salt; - }); - - validCodes = JSON.stringify(validCodes); - user.persistProperty(UserProps.AuthFactor2OTPBackupCodes, validCodes, err => { - return cb(err); - }); + // We're consuming a match - remove it from available backup codes + validCodes = validCodes.filter(c => c !== matchingCode); + validCodes = JSON.stringify(validCodes); + user.persistProperty(UserProps.AuthFactor2OTPBackupCodes, validCodes, err => { + return cb(err); }); } catch(e) { return cb(e); @@ -174,10 +136,10 @@ function prepareOTP(otpType, options, cb) { otp.generateSecret() : crypto.randomBytes(64).toString('base64').substr(0, 32); - generateNewBackupCodes((err, codes, plainTextCodes) => { - const qr = createQRCode(otp, options, secret); - return cb(err, { secret, codes, plainTextCodes, qr } ); - }); + const backupCodes = generateNewBackupCodes(); + const qr = createQRCode(otp, options, secret); + + return cb(null, { secret, backupCodes, qr } ); } function loginFactor2_OTP(client, token, cb) { diff --git a/core/user_property.js b/core/user_property.js index 02fac923..00f640fb 100644 --- a/core/user_property.js +++ b/core/user_property.js @@ -62,6 +62,6 @@ module.exports = { AuthFactor1Types : 'auth_factor1_types', // List of User.AuthFactor1Types value(s) AuthFactor2OTP : 'auth_factor2_otp', // If present, OTP type for 2FA AuthFactor2OTPSecret : 'auth_factor2_otp_secret', // Secret used in conjunction with OTP 2FA - AuthFactor2OTPBackupCodes : 'auth_factor2_otp_backup', // JSON array of backup codes: [{salt,code}, ...] + AuthFactor2OTPBackupCodes : 'auth_factor2_otp_backup', // JSON array of backup codes }; From afa856eaf31af21ed2e5336b0aca30d1bfb10627 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 11 May 2019 00:18:13 -0600 Subject: [PATCH 025/140] 2FA/OTP related stuff to WHATSNEW --- WHATSNEW.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index d4a5bc39..376344fc 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -5,7 +5,9 @@ This document attempts to track **major** changes and additions in ENiGMA½. For + `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. + SSH Public Key Authentication has been added. The system uses a OpenSSH style public key set on the `ssh_public_key` user property. -+ ++ 2-Factor (2FA) authentication is now available using [RFC-4266 - HOTP: HMAC-Based One-Time Password Algorithm)](https://tools.ietf.org/html/rfc4226), [RFC-6238 - TOTP: Time-Based One-Time Password Algorithm](https://tools.ietf.org/html/rfc6238), or [Google Authenticator](http://google-authenticator.com/). QR codes for activation are available as well. One-time backup aka recovery codes can also be used. ++ `oputil.js user 2fa USERNAME TYPE` enables 2-factor authentication for a user. +* `oputil.js user info USERNAME --security` can now display additional security information such as 2FA/OTP. ## 0.0.9-alpha From 401d0a10b117b7513c6d21e9842cefffb9a48f28 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 11 May 2019 00:19:11 -0600 Subject: [PATCH 026/140] Fix 'AR' ACS check --- core/acs_parser.js | 4 ++-- misc/acs_parser.pegjs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/acs_parser.js b/core/acs_parser.js index 3763c8e7..f903d6f1 100644 --- a/core/acs_parser.js +++ b/core/acs_parser.js @@ -994,8 +994,8 @@ function peg$parse(input, options) { return false; } switch(value) { - case 1 : return user.authFactor >= User.AuthFactors.Factor1; - case 2 : return user.authFactor >= User.AuthFActors.Factor2; + case 1 : return true; + case 2 : return user.getProperty(UserProps.AuthFactor2OTP) ? true : false; default : return false; } }, diff --git a/misc/acs_parser.pegjs b/misc/acs_parser.pegjs index 060344a8..25e5853a 100644 --- a/misc/acs_parser.pegjs +++ b/misc/acs_parser.pegjs @@ -150,8 +150,8 @@ return false; } switch(value) { - case 1 : return user.authFactor >= User.AuthFactors.Factor1; - case 2 : return user.authFactor >= User.AuthFActors.Factor2; + case 1 : return true; + case 2 : return user.getProperty(UserProps.AuthFactor2OTP) ? true : false; default : return false; } }, From 6953cdf1590e25684dda4de7c95b99eb5c61fc00 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 11 May 2019 00:20:02 -0600 Subject: [PATCH 027/140] QR's can't alwasy be created --- core/user_2fa_otp.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/core/user_2fa_otp.js b/core/user_2fa_otp.js index 83524750..2f51f105 100644 --- a/core/user_2fa_otp.js +++ b/core/user_2fa_otp.js @@ -107,18 +107,22 @@ function validateAndConsumeBackupCode(user, token, cb) { } function createQRCode(otp, options, secret) { - const uri = otp.keyuri(options.username || 'user', Config().general.boardName, secret); - const qrCode = qrGen(0, 'L'); - qrCode.addData(uri); - qrCode.make(); + try { + const uri = otp.keyuri(options.username || 'user', Config().general.boardName, secret); + const qrCode = qrGen(0, 'L'); + qrCode.addData(uri); + qrCode.make(); - options.qrType = options.qrType || 'ascii'; - return { - ascii : qrCode.createASCII, - data : qrCode.createDataURL, - img : qrCode.createImgTag, - svg : qrCode.createSvgTag, - }[options.qrType](); + options.qrType = options.qrType || 'ascii'; + return { + ascii : qrCode.createASCII, + data : qrCode.createDataURL, + img : qrCode.createImgTag, + svg : qrCode.createSvgTag, + }[options.qrType](); + } catch(e) { + return; + } } function prepareOTP(otpType, options, cb) { From d215919bffb35dc019cdfc6b3343fe0c101b271c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 11 May 2019 00:21:42 -0600 Subject: [PATCH 028/140] Improvements to oputil * Major update to --help * '2fa' is now '2fa-otp' or just 'otp' * Better 2fa-otp output & handling --- core/oputil/oputil_file_base.js | 6 +- core/oputil/oputil_help.js | 210 ++++++++++++++++++++------------ core/oputil/oputil_user.js | 61 +++++++--- 3 files changed, 180 insertions(+), 97 deletions(-) diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index 4dc25fd2..efefa05b 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -209,7 +209,7 @@ function scanFileAreaForChanges(areaInfo, options, cb) { async.series( [ function quickCheck(next) { - if(!options.quick) { + if(options['full-scan']) { return next(null); } @@ -476,8 +476,8 @@ function scanFileAreas() { options.tags = tags.split(','); } - options.descFile = argv['desc-file']; // --desc-file or --desc-file PATH - options.quick = argv.quick; + options.descFile = argv['desc-file']; // --desc-file or --desc-file PATH + options['full-scan'] = argv['-full-scan']; options.areaAndStorageInfo = getAreaAndStorage(argv._.slice(2)); diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index 6f485f0a..3e9cb60a 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -9,115 +9,169 @@ exports.getHelpFor = getHelpFor; const usageHelp = exports.USAGE_HELP = { General : `usage: oputil.js [--version] [--help] - [] + [] -global args: - -c, --config PATH specify config path (${getDefaultConfigPath()}) - -n, --no-prompt assume defaults/don't prompt for input where possible +global arguments: + -c, --config PATH Specify config path (${getDefaultConfigPath()}) + -n, --no-prompt Assume defaults (don't prompt for input where possible) commands: - user user utilities - config config file management - fb file base management - mb message base management + user User management + config Configuration management + fb File base management + mb Message base management `, User : -`usage: oputil.js user [] +`usage: oputil.js user [] -actions: - info USERNAME display information about a user - pw USERNAME PASSWORD set a user's password - aliases: password, passwd - rm USERNAME permanently removes user from system - aliases: remove, delete, del - rename USERNAME NEWNAME rename a user - aliases: mv - activate USERNAME set status to active - deactivate USERNAME set status to inactive - disable USERNAME set status to disabled - lock USERNAME set status to locked - group USERNAME [+|-]GROUP adds (+) or removes (-) user from a group +Actions: + info USERNAME Display information about a user + + pw USERNAME PASSWORD Set a user's password + (passwd|password) + + rm USERNAME Permanently removes user from system + (del|delete|remove) + + rename USERNAME NEWNAME Rename a user + (mv) + + 2fa-otp USERNAME SPEC Enable Two Factor Authentication (2FA) + (otp) + + Valid specs: + totp : Time-Based One-Time Password Algorithm (RFC-6238) + hotp : HMAC-Based One-Time Password Algorithm (RFC-4266) + google : Google Authenticator + + activate USERNAME Set a user's status to "active" + + deactivate USERNAME Set a user's status to "inactive" + + disable USERNAME Set a user's status to "disabled" + + lock USERNAME Set a user's status to "locked" + + group USERNAME [+|-]GROUP Adds (+) or removes (-) user from a group + +info arguments: + --security Include security information in output + +2fa-otp arguments: + --qr-type TYPE Specify QR code type + + Valid QR types: + ascii : Plain ASCII (default) + data : HTML data URL + img : HTML image tag + svg : SVG image + + --out PATH Path to write QR code to. defaults to stdout `, Config : -`usage: oputil.js config [] +`usage: oputil.js config [] -actions: - new generate a new/initial configuration - cat cat current configuration to stdout +Actions: + new Generate a new / default configuration -cat args: - --no-color disable color - --no-comments strip any comments + cat Write current configuration to stdout + +cat arguments: + --no-color Disable color + --no-comments Strip any comments `, FileBase : -`usage: oputil.js fb [] +`usage: oputil.js fb [] -actions: - scan AREA_TAG[@STORAGE_TAG] scan specified area - may also contain optional GLOB as last parameter, - for example: scan some_area *.zip +Actions: + scan AREA_TAG[@STORAGE_TAG] Scan specified area - info CRITERIA display information about areas and/or files - matching CRITERIA. + May contain optional GLOB as last parameter. + Example: ./oputil.js fb scan d0pew4r3z *.zip - mv SRC [SRC...] DST move entry(s) from SRC to DST - SRC: FILENAME_WC|SHA|FILE_ID|AREA_TAG[@STORAGE_TAG] - DST: AREA_TAG[@STORAGE_TAG] + info CRITERIA Display information about areas and/or files - rm SRC [SRC...] remove entry(s) from the system matching SRC - SRC: FILENAME_WC|SHA|FILE_ID|AREA_TAG[@STORAGE_TAG] - desc CRITERIA sets a new file description for file base entry - matching CRITERIA. Launches an external editor using - $VISUAL, $EDITOR, or vim/notepad. - import-areas FILEGATE.ZXX import file base areas using FileGate RAID type format + mv SRC [SRC...] DST Move matching entry(s) + (move) -scan args: - --tags TAG1,TAG2,... specify tag(s) to assign to discovered entries + Source may be any of the following: + - Filename including '*' wildcards + - SHA-1 + - File ID + - Area tag with optional @storageTag suffix + Destination is area tag with optional @storageTag suffix - --desc-file [PATH] prefer file descriptions from supplied path over other - other sources such as FILE_ID.DIZ. Path must point to - a valid FILES.BBS or DESCRIPT.ION file. - --update attempt to update information for existing entries - --quick perform quick scan + rm SRC [SRC...] Remove entry(s) from the system + (del|delete|remove) -info args: - --show-desc display short description, if any + Source may be any of the following: + - Filename including '*' wildcards + - SHA-1 + - File ID + - Area tag with optional @storageTag suffix -remove args: - --phys-file also remove underlying physical file + desc CRITERIA Updates an file base entry's description -import-areas args: - --type TYPE sets import areas type. valid options are "zxx" or "na" - --create-dirs create backing storage directories + Launches an external editor using $VISUAL, $EDITOR, or vim/notepad. + + import-areas FILEGATE.ZXX Import file base areas using FileGate RAID type format + +scan arguments: + --tags TAG1,TAG2,... Specify hashtag(s) to assign to discovered entries + + --desc-file [PATH] Prefer file descriptions from supplied input file + + If a file description can be found in the supplied input file, prefer that description + over other sources such related FILE_ID.DIZ. Path must point to a valid FILES.BBS or + DESCRIPT.ION file. + + --update Attempt to update information for existing entries + --full-scan Perform a full scan (default is quick) + +info arguments: + --show-desc Display short description, if any + +remove arguments: + --phys-file Also remove underlying physical file + +import-areas arguments: + --type TYPE Sets import areas type + + Valid types are are "zxx" or "na". + + --create-dirs Also create backing storage directories `, FileOpsInfo : ` -general information: - AREA_TAG[@STORAGE_TAG] can specify an area tag and optionally, a storage specific tag - example: retro@bbs +General Information: + Generally an area tag can also include an optional storage tag. For example, the + area of 'bbswarez' stored using 'bbswarez_main': bbswarez@bbswarez_main - CRITERIA file base entry criteria. in general, can be AREA_TAG, SHA, - FILE_ID, or FILENAME_WC. - - FILENAME_WC filename with * and ? wildcard support. may match 0:n entries - SHA full or partial SHA-256 - FILE_ID a file identifier. see file.sqlite3 + When performing an initial import of a large area or storage backing, --full-scan + is the best option. If re-scanning an area for updates a standard / quick scan is + generally good enough. + + File ID's are those found in file.sqlite3. `, MessageBase : -`usage: oputil.js mb [] +`usage: oputil.js mb [] actions: - areafix CMD1 CMD2 ... ADDR sends an AreaFix NetMail to ADDR with the supplied command(s) - one or more commands may be supplied. commands that are multi - part such as "%COMPRESS ZIP" should be quoted. - import-areas PATH import areas using fidonet *.NA or AREAS.BBS file from PATH + areafix CMD1 CMD2 ... ADDR Sends an AreaFix NetMail -import-areas args: - --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 comma separated uplinks - --type TYPE area import type. valid options are "bbs" and "na" + NetMail is sent to supplied address with the supplied command(s). Multi-part commands + such as "%COMPRESS ZIP" should be quoted. + + import-areas PATH Import areas using FidoNet *.NA or AREAS.BBS file + +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". ` }; diff --git a/core/oputil/oputil_user.js b/core/oputil/oputil_user.js index 77e0be68..92705bff 100644 --- a/core/oputil/oputil_user.js +++ b/core/oputil/oputil_user.js @@ -329,7 +329,7 @@ function showUserInfo(user) { return user.properties[p] || 'N/A'; }; - console.info(`User information: + const stdInfo = `User information: Username : ${user.username}${user.isRoot() ? ' (root/SysOp)' : ''} Real name : ${propOrNA(UserProps.RealName)} ID : ${user.userId} @@ -340,11 +340,29 @@ Last login : ${lastLogin()} Login count : ${propOrNA(UserProps.LoginCount)} Email : ${propOrNA(UserProps.EmailAddress)} Location : ${propOrNA(UserProps.Location)} -Affiliations : ${propOrNA(UserProps.Affiliations)} -`); +Affiliations : ${propOrNA(UserProps.Affiliations)}`; + let secInfo = ''; + if(argv.security) { + const otp = user.getProperty(UserProps.AuthFactor2OTP); + if(otp) { + const backupCodesOrNa = () => { + try + { + return JSON.parse(user.getProperty(UserProps.AuthFactor2OTPBackupCodes)).join(', '); + } catch(e) { + return 'N/A'; + } + }; + secInfo = `\n2FA OTP : ${otp} +OTP secret : ${user.getProperty(UserProps.AuthFactor2OTPSecret) || 'N/A'} +OTP Backup : ${backupCodesOrNa()}`; + } + } + + console.info(`${stdInfo}${secInfo}`); } -function twoFactorAuth(user) { +function twoFactorAuthOTP(user) { if(argv._.length < 4) { return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); } @@ -359,8 +377,15 @@ function twoFactorAuth(user) { function validate(callback) { // :TODO: Prompt for if not supplied let otpType = argv._[argv._.length - 1]; + + // allow aliases for OTP types + otpType = { + google : OTPTypes.GoogleAuthenticator, + hotp : OTPTypes.RFC4266_HOTP, + totp : OTPTypes.RFC6238_TOTP, + }[otpType] || otpType; otpType = _.find(OTPTypes, t => { - return t.toLowerCase() === otpType; + return t.toLowerCase() === otpType.toLowerCase(); }); if(!otpType) { return callback(Errors.Invalid('Invalid OTP type')); @@ -377,7 +402,7 @@ function twoFactorAuth(user) { }); }, function storeOrDisplayQR(otpInfo, callback) { - if(!argv.out) { + if(!argv.out || !otpInfo.qr) { return callback(null, otpInfo); } @@ -400,15 +425,18 @@ function twoFactorAuth(user) { if(err) { console.error(err.message); } else { - console.info(`OTP enabled for ${user.username}.`); - console.info(`Secret: ${otpInfo.secret}`); - console.info(`Backup codes: ${otpInfo.backupCodes.join(', ')}`); + console.info(`OTP enabled for : ${user.username}`); + console.info(`Secret : ${otpInfo.secret}`); + console.info(`Backup codes : ${otpInfo.backupCodes.join(', ')}`); - if(!argv.out) { - console.info('QR code:'); - console.info(otpInfo.qr); - } else { - console.info(`QR code saved to ${argv.out}`); + if(otpInfo.qr) { + if(!argv.out) { + console.info('--- Begin QR ---'); + console.info(otpInfo.qr); + console.info('--- End QR ---'); + } else { + console.info(`QR code saved to ${argv.out}`); + } } } } @@ -429,7 +457,7 @@ function handleUserCommand() { 'pw', 'pass', 'passwd', 'password', 'group', 'mv', 'rename', - '2fa', + '2fa-otp', 'otp' ].includes(action) ? argv._.length - 2 : argv._.length - 1; const userName = argv._[usernameIdx]; @@ -465,7 +493,8 @@ function handleUserCommand() { info : showUserInfo, - '2fa' : twoFactorAuth, + '2fa-otp' : twoFactorAuthOTP, + otp : twoFactorAuthOTP, }[action] || errUsage)(user, action); }); } \ No newline at end of file From b0d081ad044f209d2d71b0572a20f491bdda55b9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 13 May 2019 21:27:59 -0600 Subject: [PATCH 029/140] Fix to read direct / full path & 'readSauce' option of art --- core/art.js | 2 +- core/theme.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/art.js b/core/art.js index d709d366..19fa41e8 100644 --- a/core/art.js +++ b/core/art.js @@ -144,7 +144,7 @@ function getArt(name, options, cb) { // If an extension is provided, just read the file now if('' !== ext) { - const directPath = paths.join(options.basePath, name); + const directPath = paths.isAbsolute(name) ? name : paths.join(options.basePath, name); return getArtFromPath(directPath, options, cb); } diff --git a/core/theme.js b/core/theme.js index 9978fde3..8460cf24 100644 --- a/core/theme.js +++ b/core/theme.js @@ -439,7 +439,7 @@ function getThemeArt(options, cb) { // :TODO: replace asAnsi stuff with something like retrieveAs = 'ansi' | 'pipe' | ... // :TODO: Some of these options should only be set if not provided! options.asAnsi = true; // always convert to ANSI - options.readSauce = true; // read SAUCE, if avail + options.readSauce = _.get(options, 'readSauce', true); // read SAUCE, if avail options.random = _.get(options, 'random', true); // FILENAME.EXT support // From 228d3e3ae788ae9c6a80458c0092d203bb3fda39 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 13 May 2019 21:31:34 -0600 Subject: [PATCH 030/140] Improve legacy EOF detector: Must be at least >= size of SAUCE --- core/art.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/art.js b/core/art.js index 19fa41e8..38594985 100644 --- a/core/art.js +++ b/core/art.js @@ -57,6 +57,9 @@ function sliceAtEOF(data, eofMarker) { break; } } + if(eof === data.length || eof < 128) { + return data; + } return data.slice(0, eof); } From da91bf01912b73f070500507748ab50df74f628b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 15 May 2019 21:51:44 -0600 Subject: [PATCH 031/140] Fix real names during message save --- core/fse.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/fse.js b/core/fse.js index 1d9d6dd7..d86961b2 100644 --- a/core/fse.js +++ b/core/fse.js @@ -329,7 +329,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul const msgOpts = { areaTag : this.messageAreaTag, toUserName : headerValues.to, - fromUserName : this.client.user.username, + fromUserName : this.client.user.getProperty(UserProps.RealName) || this.client.user.username, subject : headerValues.subject, // :TODO: don't hard code 1 here: message : this.viewControllers.body.getView(MciViewIds.body.message).getData( { forceLineTerms : this.replyIsAnsi } ), From 9c2b3be0b1b7fad96b02ff31294c3ac75ba31af9 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sat, 18 May 2019 00:25:35 +0100 Subject: [PATCH 032/140] MRC WIP --- core/config.js | 69 +++++--- core/listening_server.js | 2 +- core/module_util.js | 1 + core/mrc.js | 233 ++++++++++++++++++++++++ core/servers/chat/mrc_multiplexer.js | 255 +++++++++++++++++++++++++++ 5 files changed, 530 insertions(+), 30 deletions(-) create mode 100644 core/mrc.js create mode 100644 core/servers/chat/mrc_multiplexer.js diff --git a/core/config.js b/core/config.js index 5c1f9c42..854e65c8 100644 --- a/core/config.js +++ b/core/config.js @@ -212,7 +212,8 @@ function getDefaultConfig() { badUserNames : [ 'sysop', 'admin', 'administrator', 'root', 'all', - 'areamgr', 'filemgr', 'filefix', 'areafix', 'allfix' + 'areamgr', 'filemgr', 'filefix', 'areafix', 'allfix', + 'server', 'client', 'notme' ], preAuthIdleLogoutSeconds : 60 * 3, // 3m @@ -253,6 +254,7 @@ function getDefaultConfig() { mods : paths.join(__dirname, './../mods/'), loginServers : paths.join(__dirname, './servers/login/'), contentServers : paths.join(__dirname, './servers/content/'), + chatServers : paths.join(__dirname, './servers/chat/'), scannerTossers : paths.join(__dirname, './scanner_tossers/'), mailers : paths.join(__dirname, './mailers/') , @@ -447,6 +449,15 @@ function getDefaultConfig() { } }, + chatServers : { + mrc: { + enabled : true, + multiplexerPort : 5000, + serverHostname : "mrc.bottomlessabyss.com", + serverPort : 5000 + } + }, + infoExtractUtils : { Exiftool2Desc : { cmd : `${__dirname}/../util/exiftool2desc.js`, // ensure chmod +x @@ -965,38 +976,38 @@ function getDefaultConfig() { eventScheduler : { events : { - dailyMaintenance : { - schedule : 'at 11:59pm', - action : '@method:core/misc_scheduled_events.js:dailyMaintenanceScheduledEvent', - }, - trimMessageAreas : { - // may optionally use [or ]@watch:/path/to/file - schedule : 'every 24 hours', + // dailyMaintenance : { + // schedule : 'at 11:59pm', + // action : '@method:core/misc_scheduled_events.js:dailyMaintenanceScheduledEvent', + // }, + // trimMessageAreas : { + // // may optionally use [or ]@watch:/path/to/file + // schedule : 'every 24 hours', - // action: - // - @method:path/to/module.js:theMethodName - // (path is relative to ENiGMA base dir) - // - // - @execute:/path/to/something/executable.sh - // - action : '@method:core/message_area.js:trimMessageAreasScheduledEvent', - }, + // // action: + // // - @method:path/to/module.js:theMethodName + // // (path is relative to ENiGMA base dir) + // // + // // - @execute:/path/to/something/executable.sh + // // + // action : '@method:core/message_area.js:trimMessageAreasScheduledEvent', + // }, - nntpMaintenance : { - schedule : 'every 12 hours', // should generally be < trimMessageAreas interval - action : '@method:core/servers/content/nntp.js:performMaintenanceTask', - }, + // nntpMaintenance : { + // schedule : 'every 12 hours', // should generally be < trimMessageAreas interval + // action : '@method:core/servers/content/nntp.js:performMaintenanceTask', + // }, - updateFileAreaStats : { - schedule : 'every 1 hours', - action : '@method:core/file_base_area.js:updateAreaStatsScheduledEvent', - }, + // updateFileAreaStats : { + // schedule : 'every 1 hours', + // action : '@method:core/file_base_area.js:updateAreaStatsScheduledEvent', + // }, - forgotPasswordMaintenance : { - schedule : 'every 24 hours', - action : '@method:core/web_password_reset.js:performMaintenanceTask', - args : [ '24 hours' ] // items older than this will be removed - }, + // forgotPasswordMaintenance : { + // schedule : 'every 24 hours', + // action : '@method:core/web_password_reset.js:performMaintenanceTask', + // args : [ '24 hours' ] // items older than this will be removed + // }, // // Enable the following entry in your config.hjson to periodically create/update diff --git a/core/listening_server.js b/core/listening_server.js index aa573fa1..7cb7405e 100644 --- a/core/listening_server.js +++ b/core/listening_server.js @@ -28,7 +28,7 @@ function getServer(packageName) { function startListening(cb) { const moduleUtil = require('./module_util.js'); // late load so we get Config - async.each( [ 'login', 'content' ], (category, next) => { + async.each( [ 'login', 'content', 'chat' ], (category, next) => { moduleUtil.loadModulesForCategory(`${category}Servers`, (module, nextModule) => { const moduleInst = new module.getModule(); try { diff --git a/core/module_util.js b/core/module_util.js index f61929d2..033d6094 100644 --- a/core/module_util.js +++ b/core/module_util.js @@ -117,6 +117,7 @@ function getModulePaths() { config.paths.mods, config.paths.loginServers, config.paths.contentServers, + config.paths.chatServers, config.paths.scannerTossers, ]; } diff --git a/core/mrc.js b/core/mrc.js new file mode 100644 index 00000000..b6e68463 --- /dev/null +++ b/core/mrc.js @@ -0,0 +1,233 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const Log = require('./logger.js').log; +const { MenuModule } = require('./menu_module.js'); +const { Errors } = require('./enig_error.js'); +const { + pipeToAnsi, + stripMciColorCodes +} = require('./color_codes.js'); +const stringFormat = require('./string_format.js'); +const { getThemeArt } = require('./theme.js'); + + +// deps +const _ = require('lodash'); +const async = require('async'); +const net = require('net'); +const moment = require('moment'); + +exports.moduleInfo = { + name : 'MRC Client', + desc : 'Connects to an MRC chat server', + author : 'RiPuk', + packageName : 'codes.l33t.enigma.mrc.client', +}; + +const FormIds = { + mrcChat : 0, +}; + +var MciViewIds = { + mrcChat : { + chatLog : 1, + inputArea : 2, + roomName : 3, + roomTopic : 4, + + customRangeStart : 10, // 10+ = customs + } +}; + +const state = { + socket: '', + alias: '', + room: '', + room_topic: '', + nicks: [], + last_ping: 0 +}; + + +exports.getModule = class mrcModule extends MenuModule { + constructor(options) { + super(options); + + this.log = Log.child( { module : 'MRC' } ); + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); + state.alias = this.client.user.username; + + + this.menuMethods = { + sendChatMessage : (formData, extraArgs, cb) => { + // const message = _.get(formData.value, 'inputArea', '').trim(); + + const inputAreaView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.inputArea); + const inputData = inputAreaView.getData(); + const textFormatObj = { + fromUserName : state.alias, + message : inputData + }; + + const messageFormat = + this.config.messageFormat || + '|00|10<|02{fromUserName}|10>|00 |03{message}|00'; + + try { + sendChat(stringFormat(messageFormat, textFormatObj)); + } catch(e) { + self.client.log.warn( { error : e.message }, 'MRC error'); + } + inputAreaView.clearText(); + + return cb(null); + + } + } + } + + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } + + async.series( + [ + // (callback) => { + // console.log("stop idle monitor") + // this.client.stopIdleMonitor(); + // return(callback); + // }, + (callback) => { + return this.prepViewController('mrcChat', FormIds.mrcChat, mciData.menu, callback); + }, + (callback) => { + return this.validateMCIByViewIds('mrcChat', [ MciViewIds.mrcChat.chatLog, MciViewIds.mrcChat.inputArea ], callback); + }, + (callback) => { + const connectOpts = { + port : 5000, + host : "localhost", + }; + + // connect to multiplexer + state.socket = net.createConnection(connectOpts, () => { + // handshake with multiplexer + state.socket.write(`--DUDE-ITS--|${state.alias}\n`); + + + sendClientConnect() + + // send register to central MRC every 60s + setInterval(function () { + sendHeartbeat(state.socket) + }, 60000); + }); + + // when we get data, process it + state.socket.on('data', data => { + data = data.toString(); + this.processReceivedMessage(data); + this.viewControllers.mrcChat.switchFocus(MciViewIds.mrcChat.inputArea); + }); + + return(callback); + } + ], + err => { + return cb(err); + } + ); + }); + } + + processReceivedMessage(blob) { + blob.split('\n').forEach( message => { + + try { + message = JSON.parse(message) + } catch (e) { + return + } + + if (message.from_user == 'SERVER') { + const params = message.body.split(':'); + + switch (params[0]) { + case 'BANNER': + const chatMessageView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); + chatMessageView.addText(pipeToAnsi(params[1].replace(/^\s+/, ''))); + chatMessageView.redraw(); + + case 'ROOMTOPIC': + this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomName).setText(params[1]); + this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomTopic).setText(params[2]); + + case 'USERLIST': + state.nicks = params[1].split(','); + + break; + } + + } else { + // if we're here then we want to show it to the user + const chatMessageView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); + const currentTime = moment().format(this.client.currentTheme.helpers.getTimeFormat()); + chatMessageView.addText(pipeToAnsi("|08" + currentTime + "|00 " + message.body)); + chatMessageView.redraw(); + } + + return; + + }); + } + +}; + + +function sendMessage(to_user, to_site, to_room, body) { + // drop message if user just mashes enter + if (body == '' || body == state.alias) return; + + // otherwise construct message + const message = { + from_room: state.room, + to_user: to_user, + to_site: to_site, + to_room: to_room, + body: body + } + Log.debug({module: 'mrcclient', message: message}, 'Sending message to MRC multiplexer'); + // TODO: check socket still exists here + state.socket.write(JSON.stringify(message) + '\n'); +} + +function sendChat(message,to_user) { + sendMessage(to_user || '', '', state.room, message) +} + +function sendServerCommand(command, to_site) { + Log.debug({ module: 'mrc', command: command }, 'Sending server command'); + sendMessage('SERVER', to_site || '', state.room, command); + return; +} + +function sendHeartbeat() { + sendServerCommand('IAMHERE'); + return; +} + +function sendClientConnect() { + sendHeartbeat(); + joinRoom('lobby'); + sendServerCommand('BANNERS'); + sendServerCommand('MOTD'); + return; +} + +function joinRoom(room) { + sendServerCommand(`NEWROOM:${state.room}:${room}`); +} diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js new file mode 100644 index 00000000..5419088a --- /dev/null +++ b/core/servers/chat/mrc_multiplexer.js @@ -0,0 +1,255 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const Log = require('../../logger.js').log; +const { ServerModule } = require('../../server_module.js'); +const Config = require('../../config.js').get; +const { Errors } = require('../../enig_error.js'); +const { wordWrapText } = require('../../word_wrap.js'); +const { stripMciColorCodes } = require('../../color_codes.js'); + +// deps +const net = require('net'); +const _ = require('lodash'); +const os = require('os'); + +// MRC +const PROTOCOL_VERSION = '1.2.9'; + +const ModuleInfo = exports.moduleInfo = { + name : 'MRC', + desc : 'An MRC Chat Multiplexer', + author : 'RiPuk', + packageName : 'codes.l33t.enigma.mrc.server', + notes : 'https://bbswiki.bottomlessabyss.net/index.php?title=MRC_Chat_platform', +}; + +const connectedSockets = new Set(); +let mrcCentralConnection = ''; + +exports.getModule = class MrcModule extends ServerModule { + constructor() { + super(); + + this.log = Log.child( { server : 'MRC' } ); + + } + + createServer(cb) { + if (!this.enabled) { + return cb(null); + } + + const config = Config(); + const boardName = config.general.boardName + const enigmaVersion = "ENiGMA-BBS_" + require('../../../package.json').version + + const mrcConnectOpts = { + port : 5000, + host : "mrc.bottomlessabyss.net" + }; + + const handshake = `${boardName}~${enigmaVersion}/${os.platform()}-${os.arch()}/${PROTOCOL_VERSION}` + this.log.debug({ handshake : handshake }, "Handshaking with MRC server") + + // create connection to MRC server + this.mrcClient = net.createConnection(mrcConnectOpts, () => { + this.mrcClient.write(handshake); + this.log.info(mrcConnectOpts, 'Connected to MRC server'); + mrcCentralConnection = this.mrcClient + }); + + // do things when we get data from MRC central + this.mrcClient.on('data', (data) => { + // split on \n to deal with getting messages in batches + data.toString().split('\n').forEach( item => { + if (item == '') return; + + this.log.debug( { data : item } , `Received data`); + let message = this.parseMessage(item); + this.log.debug(message, `Parsed data`); + + this.receiveFromMRC(this.mrcClient, message); + }); + }); + + this.mrcClient.on('end', () => { + this.log.info(mrcConnectOpts, 'Disconnected from MRC server'); + }); + + this.mrcClient.on('error', err => { + Log.info( { error : err.message }, 'MRC server error'); + }); + + // start a local server for clients to connect to + this.server = net.createServer( function(socket) { + socket.setEncoding('ascii'); + connectedSockets.add(socket); + + socket.on('data', data => { + // split on \n to deal with getting messages in batches + data.toString().split('\n').forEach( item => { + if (item == '') return; + + // save username with socket + if(item.startsWith('--DUDE-ITS--')) { + socket.username = item.split('|')[1]; + Log.debug( { server : 'MRC', user: socket.username } , `User connected`); + } + else { + receiveFromClient(socket.username, item); + } + }); + + }); + + socket.on('end', function() { + connectedSockets.delete(socket); + }); + + socket.on('error', err => { + if('ECONNRESET' !== err.code) { // normal + console.log(err.message); + } + }); + }); + + + return cb(null); + } + + listen(cb) { + if (!this.enabled) { + return cb(null); + } + + const config = Config(); + + const port = parseInt(config.chatServers.mrc.multiplexerPort); + if(isNaN(port)) { + this.log.warn( { port : config.chatServers.mrc.multiplexerPort, server : ModuleInfo.name }, 'Invalid port' ); + return cb(Errors.Invalid(`Invalid port: ${config.chatServers.mrc.multiplexerPort}`)); + } + Log.info( { server : ModuleInfo.name, port : config.chatServers.mrc.multiplexerPort }, 'MRC multiplexer local listener starting up'); + return this.server.listen(port, cb); + } + + get enabled() { + return _.get(Config(), 'chatServers.mrc.enabled', false) && this.isConfigured(); + } + + isConfigured() { + const config = Config(); + return _.isNumber(_.get(config, 'chatServers.mrc.multiplexerPort')); + } + + sendToClient(message, username) { + connectedSockets.forEach( (client) => { + this.log.debug({ server : 'MRC', username : client.username, message : message }, 'Forwarding message to connected user') + client.write(JSON.stringify(message) + '\n'); + }); + } + + receiveFromMRC(socket, message) { + + const config = Config(); + const siteName = slugify(config.general.boardName) + + if (message.from_user == 'SERVER' && message.body == 'HELLO') { + // initial server hello, can ignore + return; + + } else if (message.from_user == 'SERVER' && message.body.toUpperCase() == 'PING') { + // reply to heartbeat + // this.log.debug('Respond to heartbeat'); + let message = sendToMrcServer(socket, 'CLIENT', '', 'SERVER', 'ALL', '', `IMALIVE:${siteName}`); + return message; + + } else { + // if not a heartbeat, and we have clients then we need to send something to them + //console.log(this.connectedSockets); + this.sendToClient(message); + return; + } + } + + // split raw data received into an object we can work with + parseMessage(line) { + const msg = line.split('~'); + if (msg.length < 7) { + return; + } + + return { + from_user: msg[0], + from_site: msg[1], + from_room: msg[2], + to_user: msg[3], + to_site: msg[4], + to_room: msg[5], + body: msg[6] + }; + } + +}; + + +// User / site name must be ASCII 33-125, no MCI, 30 chars max, underscores +function sanitiseName(str) { + return str.replace( + /\s/g, '_' + ).replace( + /[^\x21-\x7D]|(\|\w\w)/g, '' // Non-printable & MCI + ).substr( + 0, 30 + ); +} + +function sanitiseRoomName(message) { + return message.replace(/[^\x21-\x7D]|(\|\w\w)/g, '').substr(0, 30); +} + +function sanitiseMessage(message) { + return message.replace(/[^\x20-\x7D]/g, ''); +} + +function receiveFromClient(username, message) { + try { + message = JSON.parse(message) + message.from_user = username + } catch (e) { + Log.debug({ server : 'MRC', user : username, message : message }, 'Dodgy message received from client'); + } + + sendToMrcServer(mrcCentralConnection, message.from_user, message.from_room, message.to_user, message.to_site, message.to_room, message.body) +} + +// send a message back to the mrc central server +function sendToMrcServer(socket, fromUser, fromRoom, toUser, toSite, toRoom, messageBody) { + const config = Config(); + const siteName = slugify(config.general.boardName) + + const line = [ + fromUser, + siteName, + sanitiseRoomName(fromRoom), + sanitiseName(toUser || ''), + sanitiseName(toSite || ''), + sanitiseRoomName(toRoom || ''), + sanitiseMessage(messageBody) + ].join('~') + '~'; + + Log.debug({ server : 'MRC', data : line }, 'Sending data'); + return socket.write(line + '\n'); +} + +function slugify(text) +{ + return text.toString() + .replace(/\s+/g, '_') // Replace spaces with - + .replace(/[^\w\-]+/g, '') // Remove all non-word chars + .replace(/\-\-+/g, '_') // Replace multiple - with single - + .replace(/^-+/, '') // Trim - from start of text + .replace(/-+$/, ''); // Trim - from end of text +} \ No newline at end of file From 67ecad4e1a746fccf475559af4937207ee4d2bec Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 19 May 2019 00:01:58 +0100 Subject: [PATCH 033/140] Implemented most MRC server calls --- art/themes/luciano_blocktronics/mrc.ans | Bin 0 -> 881 bytes core/mrc.js | 191 +++++++++++++++++++----- core/servers/chat/mrc_multiplexer.js | 5 +- 3 files changed, 159 insertions(+), 37 deletions(-) create mode 100644 art/themes/luciano_blocktronics/mrc.ans diff --git a/art/themes/luciano_blocktronics/mrc.ans b/art/themes/luciano_blocktronics/mrc.ans new file mode 100644 index 0000000000000000000000000000000000000000..a1bbd030f30251860bafab4e8e98625373ec9843 GIT binary patch literal 881 zcmb_a!AiqG5KZwSxq1P%#5zp^ZjMTwdPc zx9b!({Js8ILtFahVtZ{j?95op7$M}Ytv4-f4`(Uu02COIm&>;PNfvwZfeKkG=Qwdg zZ=bu>)+vI3g7Nk*=98DAWDGj-UyHLyFu}#?iRX<*Pu{s_*V*)b7QQ}A=&yf&1A$QO A@Bjb+ literal 0 HcmV?d00001 diff --git a/core/mrc.js b/core/mrc.js index b6e68463..681944ab 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -6,10 +6,10 @@ const Log = require('./logger.js').log; const { MenuModule } = require('./menu_module.js'); const { Errors } = require('./enig_error.js'); const { - pipeToAnsi, - stripMciColorCodes + pipeToAnsi } = require('./color_codes.js'); const stringFormat = require('./string_format.js'); +const StringUtil = require('./string_util.js') const { getThemeArt } = require('./theme.js'); @@ -24,8 +24,13 @@ exports.moduleInfo = { desc : 'Connects to an MRC chat server', author : 'RiPuk', packageName : 'codes.l33t.enigma.mrc.client', + + // Whilst this module was put together by me (RiPuk), it should be noted that a lot of the ideas (and even some code snippets) were + // borrowed from the Synchronet implementation of MRC by echicken. So...thanks, your code was very helpful in putting this together. + // Source at http://cvs.synchro.net/cgi-bin/viewcvs.cgi/xtrn/mrc/. }; + const FormIds = { mrcChat : 0, }; @@ -36,7 +41,8 @@ var MciViewIds = { inputArea : 2, roomName : 3, roomTopic : 4, - + mrcUsers : 5, + mrcBbses : 6, customRangeStart : 10, // 10+ = customs } }; @@ -61,29 +67,31 @@ exports.getModule = class mrcModule extends MenuModule { this.menuMethods = { + sendChatMessage : (formData, extraArgs, cb) => { - // const message = _.get(formData.value, 'inputArea', '').trim(); const inputAreaView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.inputArea); const inputData = inputAreaView.getData(); - const textFormatObj = { - fromUserName : state.alias, - message : inputData - }; - - const messageFormat = - this.config.messageFormat || - '|00|10<|02{fromUserName}|10>|00 |03{message}|00'; - - try { - sendChat(stringFormat(messageFormat, textFormatObj)); - } catch(e) { - self.client.log.warn( { error : e.message }, 'MRC error'); - } + + this.processSentMessage(inputData); inputAreaView.clearText(); return cb(null); - + }, + + movementKeyPressed : (formData, extraArgs, cb) => { + const bodyView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); // :TODO: use const here vs magic # + console.log("got arrow key"); + switch(formData.key.name) { + case 'down arrow' : bodyView.scrollDocumentUp(); break; + case 'up arrow' : bodyView.scrollDocumentDown(); break; + case 'page up' : bodyView.keyPressPageUp(); break; + case 'page down' : bodyView.keyPressPageDown(); break; + } + + this.viewControllers.mrcChat.switchFocus(MciViewIds.mrcChat.inputArea); + + return cb(null); } } } @@ -117,13 +125,13 @@ exports.getModule = class mrcModule extends MenuModule { state.socket = net.createConnection(connectOpts, () => { // handshake with multiplexer state.socket.write(`--DUDE-ITS--|${state.alias}\n`); - sendClientConnect() - // send register to central MRC every 60s + // send register to central MRC and get stats every 60s setInterval(function () { sendHeartbeat(state.socket) + sendServerCommand('STATS') }, 60000); }); @@ -131,7 +139,6 @@ exports.getModule = class mrcModule extends MenuModule { state.socket.on('data', data => { data = data.toString(); this.processReceivedMessage(data); - this.viewControllers.mrcChat.switchFocus(MciViewIds.mrcChat.inputArea); }); return(callback); @@ -153,37 +160,146 @@ exports.getModule = class mrcModule extends MenuModule { return } + const chatMessageView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); + if (message.from_user == 'SERVER') { const params = message.body.split(':'); switch (params[0]) { case 'BANNER': - const chatMessageView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); chatMessageView.addText(pipeToAnsi(params[1].replace(/^\s+/, ''))); chatMessageView.redraw(); + break; case 'ROOMTOPIC': - this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomName).setText(params[1]); - this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomTopic).setText(params[2]); - + this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomName).setText(`#${params[1]}`); + this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomTopic).setText(pipeToAnsi(params[2])); + state.room = params[1] + break; + case 'USERLIST': state.nicks = params[1].split(','); + break; + + case 'STATS': + const stats = params[1].split(' '); + this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.mrcUsers).setText(stats[2]); + this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.mrcBbses).setText(stats[0]); - break; + break; + + default: + chatMessageView.addText(pipeToAnsi(message.body)); + break; } } else { - // if we're here then we want to show it to the user - const chatMessageView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); - const currentTime = moment().format(this.client.currentTheme.helpers.getTimeFormat()); - chatMessageView.addText(pipeToAnsi("|08" + currentTime + "|00 " + message.body)); - chatMessageView.redraw(); + if (message.from_user == state.alias && message.to_user == "NOTME") { + // don't deliver NOTME messages + return; + } else { + // if we're here then we want to show it to the user + const currentTime = moment().format(this.client.currentTheme.helpers.getTimeFormat()); + chatMessageView.addText(pipeToAnsi("|08" + currentTime + "|00 " + message.body + "|00")); + chatMessageView.redraw(); + } } + this.viewControllers.mrcChat.switchFocus(MciViewIds.mrcChat.inputArea); return; - }); } + + processSentMessage(message) { + + if (message.startsWith('/')) { + const cmd = message.split(' '); + cmd[0] = cmd[0].substr(1).toLowerCase(); + + switch (cmd[0]) { + case 'rainbow': + const line = message.replace(/^\/rainbow\s/, '').split(' ').reduce(function (a, c) { + var cc = Math.floor((Math.random() * 31) + 1).toString().padStart(2, '0'); + a += `|${cc}${c}|00 ` + return a; + }, '').substr(0, 140).replace(/\\s\|\d*$/, ''); + + this.processSentMessage(line) + break; + + case 'l33t': + this.processSentMessage(StringUtil.stylizeString(message.substr(5), 'l33t')); + break; + + case 'kewl': + const text_modes = Array('f','v','V','i','M'); + const mode = text_modes[Math.floor(Math.random() * text_modes.length)]; + this.processSentMessage(StringUtil.stylizeString(message.substr(5), mode)); + break; + + case 'whoon': + sendServerCommand('WHOON'); + break; + + case 'motd': + sendServerCommand('MOTD'); + break; + + case 'meetups': + sendServerCommand('MEETUPS'); + break; + + case 'bbses': + sendServerCommand('CONNECTED'); + break; + + case 'topic': + sendServerCommand(`NEWTOPIC:${state.room}:${message.substr(7)}`) + break; + + case 'join': + joinRoom(cmd[1]); + break; + + case 'chatters': + sendServerCommand('CHATTERS'); + break; + + case 'rooms': + sendServerCommand('LIST'); + break; + + case 'clear': + const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog) + chatLogView.setText(''); + sendServerCommand('STATS'); + // chatLogView.redraw(); + break; + + default: + break; + } + + } else { + // just format and send + const textFormatObj = { + fromUserName : state.alias, + message : message + }; + + const messageFormat = + this.config.messageFormat || + '|00|10<|02{fromUserName}|10>|00 |03{message}|00'; + + try { + sendChat(stringFormat(messageFormat, textFormatObj)); + } catch(e) { + self.client.log.warn( { error : e.message }, 'MRC error'); + } + } + + return; + } }; @@ -205,7 +321,7 @@ function sendMessage(to_user, to_site, to_room, body) { state.socket.write(JSON.stringify(message) + '\n'); } -function sendChat(message,to_user) { +function sendChat(message, to_user) { sendMessage(to_user || '', '', state.room, message) } @@ -222,12 +338,15 @@ function sendHeartbeat() { function sendClientConnect() { sendHeartbeat(); - joinRoom('lobby'); - sendServerCommand('BANNERS'); sendServerCommand('MOTD'); + sendServerCommand('STATS'); + joinRoom('lobby'); return; } function joinRoom(room) { + // room names are displayed with a # but referred to without. confusing. + room = room.replace(/^#/, ''); sendServerCommand(`NEWROOM:${state.room}:${room}`); + sendServerCommand('USERLIST') } diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js index 5419088a..87f78a33 100644 --- a/core/servers/chat/mrc_multiplexer.js +++ b/core/servers/chat/mrc_multiplexer.js @@ -46,7 +46,7 @@ exports.getModule = class MrcModule extends ServerModule { const enigmaVersion = "ENiGMA-BBS_" + require('../../../package.json').version const mrcConnectOpts = { - port : 5000, + port : 50000, host : "mrc.bottomlessabyss.net" }; @@ -65,6 +65,9 @@ exports.getModule = class MrcModule extends ServerModule { // split on \n to deal with getting messages in batches data.toString().split('\n').forEach( item => { if (item == '') return; + console.log('start') + console.log(item) + console.log('end') this.log.debug( { data : item } , `Received data`); let message = this.parseMessage(item); From 92528fc16f73c5b407791fdbf2d74241a44a151b Mon Sep 17 00:00:00 2001 From: David Stephens Date: Mon, 20 May 2019 23:37:32 +0100 Subject: [PATCH 034/140] MRC bug squashing --- core/mrc.js | 346 +++++++++++++++------------ core/servers/chat/mrc_multiplexer.js | 29 +-- 2 files changed, 207 insertions(+), 168 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index 681944ab..7cf2661d 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -10,8 +10,6 @@ const { } = require('./color_codes.js'); const stringFormat = require('./string_format.js'); const StringUtil = require('./string_util.js') -const { getThemeArt } = require('./theme.js'); - // deps const _ = require('lodash'); @@ -30,7 +28,6 @@ exports.moduleInfo = { // Source at http://cvs.synchro.net/cgi-bin/viewcvs.cgi/xtrn/mrc/. }; - const FormIds = { mrcChat : 0, }; @@ -47,15 +44,28 @@ var MciViewIds = { } }; -const state = { - socket: '', - alias: '', - room: '', - room_topic: '', - nicks: [], - last_ping: 0 -}; + +// TODO: this is a bit shit, could maybe do it with an ansi instead +const helpText = ` +General Chat: +/rooms - List of current rooms +/join - Join a room +/pm - Send a private message +/clear - Clear the chat log +---- +/whoon - Who's on what BBS +/chatters - Who's in what room +/topic - Set the topic +/meetups - MRC MeetUps +/bbses - BBS's connected +/info - Info about specific BBS +--- +/l33t - l337 5p34k +/kewl - BBS KeWL SPeaK +/rainbow - Crazy rainbow text +`; + exports.getModule = class mrcModule extends MenuModule { constructor(options) { @@ -63,9 +73,16 @@ exports.getModule = class mrcModule extends MenuModule { this.log = Log.child( { module : 'MRC' } ); this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); - state.alias = this.client.user.username; - - + + this.state = { + socket: '', + alias: this.client.user.username, + room: '', + room_topic: '', + nicks: [], + last_ping: 0 + }; + this.menuMethods = { sendChatMessage : (formData, extraArgs, cb) => { @@ -81,7 +98,6 @@ exports.getModule = class mrcModule extends MenuModule { movementKeyPressed : (formData, extraArgs, cb) => { const bodyView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); // :TODO: use const here vs magic # - console.log("got arrow key"); switch(formData.key.name) { case 'down arrow' : bodyView.scrollDocumentUp(); break; case 'up arrow' : bodyView.scrollDocumentDown(); break; @@ -103,12 +119,7 @@ exports.getModule = class mrcModule extends MenuModule { } async.series( - [ - // (callback) => { - // console.log("stop idle monitor") - // this.client.stopIdleMonitor(); - // return(callback); - // }, + [ (callback) => { return this.prepViewController('mrcChat', FormIds.mrcChat, mciData.menu, callback); }, @@ -122,21 +133,22 @@ exports.getModule = class mrcModule extends MenuModule { }; // connect to multiplexer - state.socket = net.createConnection(connectOpts, () => { + this.state.socket = net.createConnection(connectOpts, () => { + const self = this; // handshake with multiplexer - state.socket.write(`--DUDE-ITS--|${state.alias}\n`); + self.state.socket.write(`--DUDE-ITS--|${self.state.alias}\n`); - sendClientConnect() + self.clientConnect(); // send register to central MRC and get stats every 60s - setInterval(function () { - sendHeartbeat(state.socket) - sendServerCommand('STATS') - }, 60000); + setInterval(function () { + self.sendHeartbeat(); + self.sendServerCommand('STATS') + }, 60000); }); // when we get data, process it - state.socket.on('data', data => { + this.state.socket.on('data', data => { data = data.toString(); this.processReceivedMessage(data); }); @@ -162,29 +174,31 @@ exports.getModule = class mrcModule extends MenuModule { const chatMessageView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); + if (message.from_user == 'SERVER') { const params = message.body.split(':'); switch (params[0]) { case 'BANNER': chatMessageView.addText(pipeToAnsi(params[1].replace(/^\s+/, ''))); - chatMessageView.redraw(); break; case 'ROOMTOPIC': this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomName).setText(`#${params[1]}`); this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomTopic).setText(pipeToAnsi(params[2])); - state.room = params[1] + this.state.room = params[1]; break; case 'USERLIST': - state.nicks = params[1].split(','); + this.state.nicks = params[1].split(','); break; case 'STATS': + console.log("got stats back") const stats = params[1].split(' '); this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.mrcUsers).setText(stats[2]); this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.mrcBbses).setText(stats[0]); + this.state.last_ping = stats[1]; break; @@ -194,159 +208,183 @@ exports.getModule = class mrcModule extends MenuModule { } } else { - if (message.from_user == state.alias && message.to_user == "NOTME") { + if (message.from_user == this.state.alias && message.to_user == "NOTME") { // don't deliver NOTME messages return; } else { // if we're here then we want to show it to the user const currentTime = moment().format(this.client.currentTheme.helpers.getTimeFormat()); chatMessageView.addText(pipeToAnsi("|08" + currentTime + "|00 " + message.body + "|00")); - chatMessageView.redraw(); } } this.viewControllers.mrcChat.switchFocus(MciViewIds.mrcChat.inputArea); - return; }); } - processSentMessage(message) { - + processSentMessage(message, to_user) { if (message.startsWith('/')) { - const cmd = message.split(' '); - cmd[0] = cmd[0].substr(1).toLowerCase(); - switch (cmd[0]) { - case 'rainbow': - const line = message.replace(/^\/rainbow\s/, '').split(' ').reduce(function (a, c) { - var cc = Math.floor((Math.random() * 31) + 1).toString().padStart(2, '0'); - a += `|${cc}${c}|00 ` - return a; - }, '').substr(0, 140).replace(/\\s\|\d*$/, ''); - - this.processSentMessage(line) - break; - - case 'l33t': - this.processSentMessage(StringUtil.stylizeString(message.substr(5), 'l33t')); - break; - - case 'kewl': - const text_modes = Array('f','v','V','i','M'); - const mode = text_modes[Math.floor(Math.random() * text_modes.length)]; - this.processSentMessage(StringUtil.stylizeString(message.substr(5), mode)); - break; - - case 'whoon': - sendServerCommand('WHOON'); - break; - - case 'motd': - sendServerCommand('MOTD'); - break; - - case 'meetups': - sendServerCommand('MEETUPS'); - break; - - case 'bbses': - sendServerCommand('CONNECTED'); - break; - - case 'topic': - sendServerCommand(`NEWTOPIC:${state.room}:${message.substr(7)}`) - break; - - case 'join': - joinRoom(cmd[1]); - break; - - case 'chatters': - sendServerCommand('CHATTERS'); - break; - - case 'rooms': - sendServerCommand('LIST'); - break; - - case 'clear': - const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog) - chatLogView.setText(''); - sendServerCommand('STATS'); - // chatLogView.redraw(); - break; - - default: - break; - } + this.processSlashCommand(message) } else { + if (message == '') { + this.sendServerCommand('STATS'); + return; + } + // just format and send const textFormatObj = { - fromUserName : state.alias, + fromUserName : this.state.alias, message : message }; - + const messageFormat = this.config.messageFormat || '|00|10<|02{fromUserName}|10>|00 |03{message}|00'; - + try { - sendChat(stringFormat(messageFormat, textFormatObj)); + this.sendChat(stringFormat(messageFormat, textFormatObj), to_user || ''); } catch(e) { - self.client.log.warn( { error : e.message }, 'MRC error'); + this.client.log.warn( { error : e.message }, 'MRC error'); } } - - return; + + } + + processSlashCommand(message) { + // get the chat log view in case we need it + const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog) + + const cmd = message.split(' '); + cmd[0] = cmd[0].substr(1).toLowerCase(); + + switch (cmd[0]) { + case 'pm': + this.processSentMessage(cmd[2], cmd[1]) + break; + case 'rainbow': + // this is brutal, but i love it + const line = message.replace(/^\/rainbow\s/, '').split(' ').reduce(function (a, c) { + var cc = Math.floor((Math.random() * 31) + 1).toString().padStart(2, '0'); + a += `|${cc}${c}|00 `; + return a; + }, '').substr(0, 140).replace(/\\s\|\d*$/, ''); + + this.processSentMessage(line); + break; + + case 'l33t': + this.processSentMessage(StringUtil.stylizeString(message.substr(5), 'l33t')); + break; + + case 'kewl': + const text_modes = Array('f','v','V','i','M'); + const mode = text_modes[Math.floor(Math.random() * text_modes.length)]; + this.processSentMessage(StringUtil.stylizeString(message.substr(5), mode)); + break; + + case 'whoon': + this.sendServerCommand('WHOON'); + break; + + case 'motd': + this.sendServerCommand('MOTD'); + break; + + case 'meetups': + this.sendServerCommand('MEETUPS'); + break; + + case 'bbses': + this.sendServerCommand('CONNECTED'); + break; + + case 'topic': + this.sendServerCommand(`NEWTOPIC:${this.state.room}:${message.substr(7)}`) + break; + + case 'info': + this.sendServerCommand(`INFO ${cmd[1]}`); + break; + + case 'join': + this.joinRoom(cmd[1]); + break; + + case 'chatters': + this.sendServerCommand('CHATTERS'); + break; + + case 'rooms': + this.sendServerCommand('LIST'); + break; + + case 'clear': + chatLogView.setText(''); + break; + + case '?': + chatLogView.addText(helpText); + break; + + default: + + break; + } + + // just do something to get the cursor back to the right place ¯\_(ツ)_/¯ + this.sendServerCommand('STATS'); + + }; + + sendMessage(to_user, to_site, to_room, body) { + + const message = { + from_user: this.state.alias, + from_room: this.state.room, + to_user: to_user, + to_site: to_site, + to_room: to_room, + body: body + }; + + this.log.debug({ message: message }, 'Sending message to MRC multiplexer'); + // TODO: check socket still exists here + + this.state.socket.write(JSON.stringify(message) + '\n'); + }; + + sendServerCommand(command, to_site) { + Log.debug({ module: 'mrc', command: command }, 'Sending server command'); + this.sendMessage('SERVER', to_site || '', this.state.room, command); + }; + + + sendHeartbeat() { + this.sendServerCommand('IAMHERE'); + } + + joinRoom(room) { + // room names are displayed with a # but referred to without. confusing. + room = room.replace(/^#/, ''); + this.state.room = room; + this.sendServerCommand(`NEWROOM:${this.state.room}:${room}`); + this.sendServerCommand('USERLIST') + } + + clientConnect() { + this.sendServerCommand('MOTD'); + this.joinRoom('lobby'); + this.sendServerCommand('STATS'); + this.sendHeartbeat(); + } + + sendChat(message, to_user) { + this.sendMessage(to_user || '', '', this.state.room, message) } - }; -function sendMessage(to_user, to_site, to_room, body) { - // drop message if user just mashes enter - if (body == '' || body == state.alias) return; - - // otherwise construct message - const message = { - from_room: state.room, - to_user: to_user, - to_site: to_site, - to_room: to_room, - body: body - } - Log.debug({module: 'mrcclient', message: message}, 'Sending message to MRC multiplexer'); - // TODO: check socket still exists here - state.socket.write(JSON.stringify(message) + '\n'); -} -function sendChat(message, to_user) { - sendMessage(to_user || '', '', state.room, message) -} -function sendServerCommand(command, to_site) { - Log.debug({ module: 'mrc', command: command }, 'Sending server command'); - sendMessage('SERVER', to_site || '', state.room, command); - return; -} - -function sendHeartbeat() { - sendServerCommand('IAMHERE'); - return; -} - -function sendClientConnect() { - sendHeartbeat(); - sendServerCommand('MOTD'); - sendServerCommand('STATS'); - joinRoom('lobby'); - return; -} - -function joinRoom(room) { - // room names are displayed with a # but referred to without. confusing. - room = room.replace(/^#/, ''); - sendServerCommand(`NEWROOM:${state.room}:${room}`); - sendServerCommand('USERLIST') -} diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js index 87f78a33..ef0138f8 100644 --- a/core/servers/chat/mrc_multiplexer.js +++ b/core/servers/chat/mrc_multiplexer.js @@ -14,6 +14,7 @@ const net = require('net'); const _ = require('lodash'); const os = require('os'); + // MRC const PROTOCOL_VERSION = '1.2.9'; @@ -65,15 +66,13 @@ exports.getModule = class MrcModule extends ServerModule { // split on \n to deal with getting messages in batches data.toString().split('\n').forEach( item => { if (item == '') return; - console.log('start') - console.log(item) - console.log('end') this.log.debug( { data : item } , `Received data`); let message = this.parseMessage(item); this.log.debug(message, `Parsed data`); - - this.receiveFromMRC(this.mrcClient, message); + if (message) { + this.receiveFromMRC(this.mrcClient, message); + } }); }); @@ -82,13 +81,12 @@ exports.getModule = class MrcModule extends ServerModule { }); this.mrcClient.on('error', err => { - Log.info( { error : err.message }, 'MRC server error'); + this.log.info( { error : err.message }, 'MRC server error'); }); // start a local server for clients to connect to this.server = net.createServer( function(socket) { socket.setEncoding('ascii'); - connectedSockets.add(socket); socket.on('data', data => { // split on \n to deal with getting messages in batches @@ -96,7 +94,8 @@ exports.getModule = class MrcModule extends ServerModule { if (item == '') return; // save username with socket - if(item.startsWith('--DUDE-ITS--')) { + if(item.startsWith('--DUDE-ITS--')) { + connectedSockets.add(socket); socket.username = item.split('|')[1]; Log.debug( { server : 'MRC', user: socket.username } , `User connected`); } @@ -147,10 +146,12 @@ exports.getModule = class MrcModule extends ServerModule { return _.isNumber(_.get(config, 'chatServers.mrc.multiplexerPort')); } - sendToClient(message, username) { + sendToClient(message) { connectedSockets.forEach( (client) => { - this.log.debug({ server : 'MRC', username : client.username, message : message }, 'Forwarding message to connected user') - client.write(JSON.stringify(message) + '\n'); + if (message.to_user == '' || message.to_user == client.username || message.to_user == 'CLIENT') { + // this.log.debug({ server : 'MRC', username : client.username, message : message }, 'Forwarding message to connected user') + client.write(JSON.stringify(message) + '\n'); + } }); } @@ -172,7 +173,8 @@ exports.getModule = class MrcModule extends ServerModule { } else { // if not a heartbeat, and we have clients then we need to send something to them //console.log(this.connectedSockets); - this.sendToClient(message); + this.sendToClient(message); + return; } } @@ -220,7 +222,6 @@ function sanitiseMessage(message) { function receiveFromClient(username, message) { try { message = JSON.parse(message) - message.from_user = username } catch (e) { Log.debug({ server : 'MRC', user : username, message : message }, 'Dodgy message received from client'); } @@ -250,7 +251,7 @@ function sendToMrcServer(socket, fromUser, fromRoom, toUser, toSite, toRoom, mes function slugify(text) { return text.toString() - .replace(/\s+/g, '_') // Replace spaces with - + .replace(/\s+/g, '_') // Replace spaces with _ .replace(/[^\w\-]+/g, '') // Remove all non-word chars .replace(/\-\-+/g, '_') // Replace multiple - with single - .replace(/^-+/, '') // Trim - from start of text From 9f4f1fca13203fbd7cf46f36d47ba929b7c21588 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Wed, 22 May 2019 23:43:41 +0100 Subject: [PATCH 035/140] Refactor and rename of MRC client and multiplexer --- core/mrc.js | 162 ++++++++++++++++----------- core/servers/chat/mrc_multiplexer.js | 119 +++++++++----------- 2 files changed, 152 insertions(+), 129 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index 7cf2661d..5320c999 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -4,12 +4,11 @@ // ENiGMA½ const Log = require('./logger.js').log; const { MenuModule } = require('./menu_module.js'); -const { Errors } = require('./enig_error.js'); const { pipeToAnsi } = require('./color_codes.js'); const stringFormat = require('./string_format.js'); -const StringUtil = require('./string_util.js') +const StringUtil = require('./string_util.js'); // deps const _ = require('lodash'); @@ -23,7 +22,7 @@ exports.moduleInfo = { author : 'RiPuk', packageName : 'codes.l33t.enigma.mrc.client', - // Whilst this module was put together by me (RiPuk), it should be noted that a lot of the ideas (and even some code snippets) were + // Whilst this module was put together by me (RiPuk), it should be noted that a lot of the ideas (and even some code snippets) were // borrowed from the Synchronet implementation of MRC by echicken. So...thanks, your code was very helpful in putting this together. // Source at http://cvs.synchro.net/cgi-bin/viewcvs.cgi/xtrn/mrc/. }; @@ -45,7 +44,7 @@ var MciViewIds = { }; - + // TODO: this is a bit shit, could maybe do it with an ansi instead const helpText = ` General Chat: @@ -73,7 +72,7 @@ exports.getModule = class mrcModule extends MenuModule { this.log = Log.child( { module : 'MRC' } ); this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); - + this.state = { socket: '', alias: this.client.user.username, @@ -82,17 +81,17 @@ exports.getModule = class mrcModule extends MenuModule { nicks: [], last_ping: 0 }; - + this.menuMethods = { sendChatMessage : (formData, extraArgs, cb) => { - + const inputAreaView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.inputArea); const inputData = inputAreaView.getData(); - this.processSentMessage(inputData); + this.processOutgoingMessage(inputData); inputAreaView.clearText(); - + return cb(null); }, @@ -109,7 +108,7 @@ exports.getModule = class mrcModule extends MenuModule { return cb(null); } - } + }; } mciReady(mciData, cb) { @@ -129,7 +128,7 @@ exports.getModule = class mrcModule extends MenuModule { (callback) => { const connectOpts = { port : 5000, - host : "localhost", + host : 'localhost', }; // connect to multiplexer @@ -143,7 +142,7 @@ exports.getModule = class mrcModule extends MenuModule { // send register to central MRC and get stats every 60s setInterval(function () { self.sendHeartbeat(); - self.sendServerCommand('STATS') + self.sendServerMessage('STATS'); }, 60000); }); @@ -159,28 +158,36 @@ exports.getModule = class mrcModule extends MenuModule { err => { return cb(err); } - ); + ); }); } + /** + * Adds a message to the chat log on screen + */ + addMessageToChatLog(message) { + const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); + chatLogView.addText(pipeToAnsi(message)); + } + + /** + * Processes data received back from the MRC multiplexer + */ processReceivedMessage(blob) { blob.split('\n').forEach( message => { try { - message = JSON.parse(message) + message = JSON.parse(message); } catch (e) { - return + return; } - const chatMessageView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); - - if (message.from_user == 'SERVER') { const params = message.body.split(':'); switch (params[0]) { case 'BANNER': - chatMessageView.addText(pipeToAnsi(params[1].replace(/^\s+/, ''))); + this.addMessageToChatLog(params[1].replace(/^\s+/, '')); break; case 'ROOMTOPIC': @@ -188,13 +195,12 @@ exports.getModule = class mrcModule extends MenuModule { this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomTopic).setText(pipeToAnsi(params[2])); this.state.room = params[1]; break; - + case 'USERLIST': this.state.nicks = params[1].split(','); break; - + case 'STATS': - console.log("got stats back") const stats = params[1].split(' '); this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.mrcUsers).setText(stats[2]); this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.mrcBbses).setText(stats[0]); @@ -203,18 +209,25 @@ exports.getModule = class mrcModule extends MenuModule { break; default: - chatMessageView.addText(pipeToAnsi(message.body)); + this.addMessageToChatLog(message.body); break; } } else { - if (message.from_user == this.state.alias && message.to_user == "NOTME") { + if (message.from_user == this.state.alias && message.to_user == 'NOTME') { // don't deliver NOTME messages return; } else { // if we're here then we want to show it to the user const currentTime = moment().format(this.client.currentTheme.helpers.getTimeFormat()); - chatMessageView.addText(pipeToAnsi("|08" + currentTime + "|00 " + message.body + "|00")); + + if (message.to_user == this.state.alias) { + // it's a pm + this.addMessageToChatLog('|08' + currentTime + ' |14PM|00 ' + message.body + '|00'); + } else { + // it's not a pm + this.addMessageToChatLog('|08' + currentTime + '|00 ' + message.body + '|00'); + } } } @@ -222,14 +235,17 @@ exports.getModule = class mrcModule extends MenuModule { }); } - processSentMessage(message, to_user) { + /** + * Receives the message input from the user and does something with it based on what it is + */ + processOutgoingMessage(message, to_user) { if (message.startsWith('/')) { - this.processSlashCommand(message) + this.processSlashCommand(message); } else { if (message == '') { - this.sendServerCommand('STATS'); + this.sendServerMessage('STATS'); return; } @@ -244,7 +260,8 @@ exports.getModule = class mrcModule extends MenuModule { '|00|10<|02{fromUserName}|10>|00 |03{message}|00'; try { - this.sendChat(stringFormat(messageFormat, textFormatObj), to_user || ''); + const formattedMessage = stringFormat(messageFormat, textFormatObj); + this.sendMessageToMultiplexer(to_user || '', '', this.state.room, formattedMessage); } catch(e) { this.client.log.warn( { error : e.message }, 'MRC error'); } @@ -252,60 +269,63 @@ exports.getModule = class mrcModule extends MenuModule { } + /** + * Processes a message that begins with a slash + */ processSlashCommand(message) { // get the chat log view in case we need it - const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog) + const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); const cmd = message.split(' '); cmd[0] = cmd[0].substr(1).toLowerCase(); switch (cmd[0]) { case 'pm': - this.processSentMessage(cmd[2], cmd[1]) + this.processOutgoingMessage(cmd[2], cmd[1]); break; case 'rainbow': // this is brutal, but i love it const line = message.replace(/^\/rainbow\s/, '').split(' ').reduce(function (a, c) { - var cc = Math.floor((Math.random() * 31) + 1).toString().padStart(2, '0'); + const cc = Math.floor((Math.random() * 31) + 1).toString().padStart(2, '0'); a += `|${cc}${c}|00 `; return a; }, '').substr(0, 140).replace(/\\s\|\d*$/, ''); - this.processSentMessage(line); + this.processOutgoingMessage(line); break; case 'l33t': - this.processSentMessage(StringUtil.stylizeString(message.substr(5), 'l33t')); + this.processOutgoingMessage(StringUtil.stylizeString(message.substr(5), 'l33t')); break; case 'kewl': const text_modes = Array('f','v','V','i','M'); const mode = text_modes[Math.floor(Math.random() * text_modes.length)]; - this.processSentMessage(StringUtil.stylizeString(message.substr(5), mode)); + this.processOutgoingMessage(StringUtil.stylizeString(message.substr(5), mode)); break; case 'whoon': - this.sendServerCommand('WHOON'); + this.sendServerMessage('WHOON'); break; case 'motd': - this.sendServerCommand('MOTD'); + this.sendServerMessage('MOTD'); break; case 'meetups': - this.sendServerCommand('MEETUPS'); + this.sendServerMessage('MEETUPS'); break; case 'bbses': - this.sendServerCommand('CONNECTED'); + this.sendServerMessage('CONNECTED'); break; case 'topic': - this.sendServerCommand(`NEWTOPIC:${this.state.room}:${message.substr(7)}`) + this.sendServerMessage(`NEWTOPIC:${this.state.room}:${message.substr(7)}`); break; case 'info': - this.sendServerCommand(`INFO ${cmd[1]}`); + this.sendServerMessage(`INFO ${cmd[1]}`); break; case 'join': @@ -313,11 +333,11 @@ exports.getModule = class mrcModule extends MenuModule { break; case 'chatters': - this.sendServerCommand('CHATTERS'); + this.sendServerMessage('CHATTERS'); break; case 'rooms': - this.sendServerCommand('LIST'); + this.sendServerMessage('LIST'); break; case 'clear': @@ -325,20 +345,23 @@ exports.getModule = class mrcModule extends MenuModule { break; case '?': - chatLogView.addText(helpText); + this.addMessageToChatLog(helpText); break; default: - + break; } // just do something to get the cursor back to the right place ¯\_(ツ)_/¯ - this.sendServerCommand('STATS'); + this.sendServerMessage('STATS'); - }; + } - sendMessage(to_user, to_site, to_room, body) { + /** + * Creates a json object, stringifies it and sends it to the MRC multiplexer + */ + sendMessageToMultiplexer(to_user, to_site, to_room, body) { const message = { from_user: this.state.alias, @@ -353,36 +376,43 @@ exports.getModule = class mrcModule extends MenuModule { // TODO: check socket still exists here this.state.socket.write(JSON.stringify(message) + '\n'); - }; - - sendServerCommand(command, to_site) { - Log.debug({ module: 'mrc', command: command }, 'Sending server command'); - this.sendMessage('SERVER', to_site || '', this.state.room, command); - }; - - - sendHeartbeat() { - this.sendServerCommand('IAMHERE'); } + /** + * Sends an MRC 'server' message + */ + sendServerMessage(command, to_site) { + Log.debug({ module: 'mrc', command: command }, 'Sending server command'); + this.sendMessageToMultiplexer('SERVER', to_site || '', this.state.room, command); + } + + /** + * Sends a heartbeat to the MRC server + */ + sendHeartbeat() { + this.sendServerMessage('IAMHERE'); + } + + /** + * Joins a room, unsurprisingly + */ joinRoom(room) { // room names are displayed with a # but referred to without. confusing. room = room.replace(/^#/, ''); this.state.room = room; - this.sendServerCommand(`NEWROOM:${this.state.room}:${room}`); - this.sendServerCommand('USERLIST') + this.sendServerMessage(`NEWROOM:${this.state.room}:${room}`); + this.sendServerMessage('USERLIST'); } + /** + * Things that happen when a local user connects to the MRC multiplexer + */ clientConnect() { - this.sendServerCommand('MOTD'); + this.sendServerMessage('MOTD'); this.joinRoom('lobby'); - this.sendServerCommand('STATS'); + this.sendServerMessage('STATS'); this.sendHeartbeat(); } - - sendChat(message, to_user) { - this.sendMessage(to_user || '', '', this.state.room, message) - } }; diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js index ef0138f8..9410a237 100644 --- a/core/servers/chat/mrc_multiplexer.js +++ b/core/servers/chat/mrc_multiplexer.js @@ -6,8 +6,6 @@ const Log = require('../../logger.js').log; const { ServerModule } = require('../../server_module.js'); const Config = require('../../config.js').get; const { Errors } = require('../../enig_error.js'); -const { wordWrapText } = require('../../word_wrap.js'); -const { stripMciColorCodes } = require('../../color_codes.js'); // deps const net = require('net'); @@ -41,24 +39,26 @@ exports.getModule = class MrcModule extends ServerModule { if (!this.enabled) { return cb(null); } - + + const self = this; + const config = Config(); - const boardName = config.general.boardName - const enigmaVersion = "ENiGMA-BBS_" + require('../../../package.json').version + const boardName = config.general.boardName; + const enigmaVersion = 'ENiGMA-BBS_' + require('../../../package.json').version; const mrcConnectOpts = { port : 50000, - host : "mrc.bottomlessabyss.net" + host : 'mrc.bottomlessabyss.net' }; - const handshake = `${boardName}~${enigmaVersion}/${os.platform()}-${os.arch()}/${PROTOCOL_VERSION}` - this.log.debug({ handshake : handshake }, "Handshaking with MRC server") + const handshake = `${boardName}~${enigmaVersion}/${os.platform()}-${os.arch()}/${PROTOCOL_VERSION}`; + this.log.debug({ handshake : handshake }, 'Handshaking with MRC server'); // create connection to MRC server this.mrcClient = net.createConnection(mrcConnectOpts, () => { this.mrcClient.write(handshake); this.log.info(mrcConnectOpts, 'Connected to MRC server'); - mrcCentralConnection = this.mrcClient + mrcCentralConnection = this.mrcClient; }); // do things when we get data from MRC central @@ -66,10 +66,10 @@ exports.getModule = class MrcModule extends ServerModule { // split on \n to deal with getting messages in batches data.toString().split('\n').forEach( item => { if (item == '') return; - - this.log.debug( { data : item } , `Received data`); + + this.log.debug( { data : item } , 'Received data'); let message = this.parseMessage(item); - this.log.debug(message, `Parsed data`); + this.log.debug(message, 'Parsed data'); if (message) { this.receiveFromMRC(this.mrcClient, message); } @@ -87,20 +87,20 @@ exports.getModule = class MrcModule extends ServerModule { // start a local server for clients to connect to this.server = net.createServer( function(socket) { socket.setEncoding('ascii'); - + socket.on('data', data => { // split on \n to deal with getting messages in batches data.toString().split('\n').forEach( item => { + if (item == '') return; // save username with socket if(item.startsWith('--DUDE-ITS--')) { connectedSockets.add(socket); socket.username = item.split('|')[1]; - Log.debug( { server : 'MRC', user: socket.username } , `User connected`); - } - else { - receiveFromClient(socket.username, item); + Log.debug( { server : 'MRC', user: socket.username } , 'User connected'); + } else { + self.receiveFromClient(socket.username, item); } }); @@ -112,7 +112,7 @@ exports.getModule = class MrcModule extends ServerModule { socket.on('error', err => { if('ECONNRESET' !== err.code) { // normal - console.log(err.message); + this.log.error( { error: err.message }, 'MRC error' ); } }); }); @@ -156,26 +156,21 @@ exports.getModule = class MrcModule extends ServerModule { } receiveFromMRC(socket, message) { - + const config = Config(); - const siteName = slugify(config.general.boardName) + const siteName = slugify(config.general.boardName); if (message.from_user == 'SERVER' && message.body == 'HELLO') { // initial server hello, can ignore - return; } else if (message.from_user == 'SERVER' && message.body.toUpperCase() == 'PING') { // reply to heartbeat // this.log.debug('Respond to heartbeat'); - let message = sendToMrcServer(socket, 'CLIENT', '', 'SERVER', 'ALL', '', `IMALIVE:${siteName}`); - return message; + this.sendToMrcServer(socket, 'CLIENT', '', 'SERVER', 'ALL', '', `IMALIVE:${siteName}`); } else { // if not a heartbeat, and we have clients then we need to send something to them - //console.log(this.connectedSockets); this.sendToClient(message); - - return; } } @@ -197,6 +192,34 @@ exports.getModule = class MrcModule extends ServerModule { }; } + receiveFromClient(username, message) { + try { + message = JSON.parse(message); + } catch (e) { + Log.debug({ server : 'MRC', user : username, message : message }, 'Dodgy message received from client'); + } + + this.sendToMrcServer(mrcCentralConnection, message.from_user, message.from_room, message.to_user, message.to_site, message.to_room, message.body); + } + + // send a message back to the mrc central server + sendToMrcServer(socket, fromUser, fromRoom, toUser, toSite, toRoom, messageBody) { + const config = Config(); + const siteName = slugify(config.general.boardName); + + const line = [ + fromUser, + siteName, + sanitiseRoomName(fromRoom), + sanitiseName(toUser || ''), + sanitiseName(toSite || ''), + sanitiseRoomName(toRoom || ''), + sanitiseMessage(messageBody) + ].join('~') + '~'; + + Log.debug({ server : 'MRC', data : line }, 'Sending data'); + return socket.write(line + '\n'); + } }; @@ -219,41 +242,11 @@ function sanitiseMessage(message) { return message.replace(/[^\x20-\x7D]/g, ''); } -function receiveFromClient(username, message) { - try { - message = JSON.parse(message) - } catch (e) { - Log.debug({ server : 'MRC', user : username, message : message }, 'Dodgy message received from client'); - } - - sendToMrcServer(mrcCentralConnection, message.from_user, message.from_room, message.to_user, message.to_site, message.to_room, message.body) -} - -// send a message back to the mrc central server -function sendToMrcServer(socket, fromUser, fromRoom, toUser, toSite, toRoom, messageBody) { - const config = Config(); - const siteName = slugify(config.general.boardName) - - const line = [ - fromUser, - siteName, - sanitiseRoomName(fromRoom), - sanitiseName(toUser || ''), - sanitiseName(toSite || ''), - sanitiseRoomName(toRoom || ''), - sanitiseMessage(messageBody) - ].join('~') + '~'; - - Log.debug({ server : 'MRC', data : line }, 'Sending data'); - return socket.write(line + '\n'); -} - -function slugify(text) -{ - return text.toString() - .replace(/\s+/g, '_') // Replace spaces with _ - .replace(/[^\w\-]+/g, '') // Remove all non-word chars - .replace(/\-\-+/g, '_') // Replace multiple - with single - - .replace(/^-+/, '') // Trim - from start of text - .replace(/-+$/, ''); // Trim - from end of text +function slugify(text) { + return text.toString() + .replace(/\s+/g, '_') // Replace spaces with _ + .replace(/[^\w\-]+/g, '') // Remove all non-word chars + .replace(/\-\-+/g, '_') // Replace multiple - with single - + .replace(/^-+/, '') // Trim - from start of text + .replace(/-+$/, ''); // Trim - from end of text } \ No newline at end of file From 9018bc363cb11ef0f59d658c9f91a4720dcb00be Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 24 May 2019 22:26:27 -0600 Subject: [PATCH 036/140] show_art module can now take Buffer to display --- core/show_art.js | 9 +++++++++ core/theme.js | 21 +++++++++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/core/show_art.js b/core/show_art.js index a480fa05..7e53ca60 100644 --- a/core/show_art.js +++ b/core/show_art.js @@ -68,6 +68,15 @@ exports.getModule = class ShowArtModule extends MenuModule { } showByExtraArgs(cb) { + const artData = _.get(this.config, 'extraArgs.artData'); + if(Buffer.isBuffer(artData)) { + const options = { + pause : this.shouldPause(), + desc : 'extraArgs', + }; + return this.displaySingleArtWithOptions(artData, options, cb); + } + this.getArtKeyValue(this.config.key, (err, artSpec) => { if(err) { return cb(err); diff --git a/core/theme.js b/core/theme.js index 8460cf24..a2d31df1 100644 --- a/core/theme.js +++ b/core/theme.js @@ -27,6 +27,7 @@ exports.getAvailableThemes = getAvailableThemes; exports.getRandomTheme = getRandomTheme; exports.setClientTheme = setClientTheme; exports.initAvailableThemes = initAvailableThemes; +exports.displayPreparedArt = displayPreparedArt; exports.displayThemeArt = displayThemeArt; exports.displayThemedPause = displayThemedPause; exports.displayThemedPrompt = displayThemedPrompt; @@ -510,6 +511,17 @@ function getThemeArt(options, cb) { ); } +function displayPreparedArt(options, artInfo, cb) { + const displayOpts = { + sauce : artInfo.sauce, + font : options.font, + trailingLF : options.trailingLF, + }; + art.display(options.client, artInfo.data, displayOpts, (err, mciMap, extraInfo) => { + return cb(err, { mciMap : mciMap, artInfo : artInfo, extraInfo : extraInfo } ); + }); +} + function displayThemeArt(options, cb) { assert(_.isObject(options)); assert(_.isObject(options.client)); @@ -537,14 +549,7 @@ function displayThemeArt(options, cb) { } }, function disp(artInfo, callback) { - const displayOpts = { - sauce : artInfo.sauce, - font : options.font, - trailingLF : options.trailingLF, - }; - art.display(options.client, artInfo.data, displayOpts, (err, mciMap, extraInfo) => { - return callback(err, { mciMap : mciMap, artInfo : artInfo, extraInfo : extraInfo } ); - }); + return displayPreparedArt(options, artInfo, callback); } ], (err, artData) => { From 565f0ea7c2c8bed080b34270001bbadde1503a5a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 24 May 2019 22:26:43 -0600 Subject: [PATCH 037/140] Note on oputil --- WHATSNEW.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index 376344fc..38df6cf4 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -8,7 +8,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For + 2-Factor (2FA) authentication is now available using [RFC-4266 - HOTP: HMAC-Based One-Time Password Algorithm)](https://tools.ietf.org/html/rfc4226), [RFC-6238 - TOTP: Time-Based One-Time Password Algorithm](https://tools.ietf.org/html/rfc6238), or [Google Authenticator](http://google-authenticator.com/). QR codes for activation are available as well. One-time backup aka recovery codes can also be used. + `oputil.js user 2fa USERNAME TYPE` enables 2-factor authentication for a user. * `oputil.js user info USERNAME --security` can now display additional security information such as 2FA/OTP. - +* `oputil.js fb scan --quick` is now the default. Override with `--full-scan`. ## 0.0.9-alpha * Development is now against Node.js 10.x LTS. While other Node.js series may continue to work, you're own your own and YMMV! From 4ef32d1c52f5ca777bda0fd5529b7920495a6d6f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 24 May 2019 22:26:56 -0600 Subject: [PATCH 038/140] Formatting + refs --- core/client.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/core/client.js b/core/client.js index d340981b..8820a3ac 100644 --- a/core/client.js +++ b/core/client.js @@ -136,14 +136,8 @@ function Client(/*input, output*/) { // this.getTermClient = function(deviceAttr) { let termClient = { - // - // See http://www.fbl.cz/arctel/download/techman.pdf - // - // Known clients: - // * Irssi ConnectBot (Android) - // - '63;1;2' : 'arctel', - '50;86;84;88' : 'vtx', + '63;1;2' : 'arctel', // http://www.fbl.cz/arctel/download/techman.pdf - Irssi ConnectBot (Android) + '50;86;84;88' : 'vtx', // https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt }[deviceAttr]; if(!termClient) { From b62f55961f20061d5bc6ccf07230cca7edcca249 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 24 May 2019 22:27:28 -0600 Subject: [PATCH 039/140] Better formatting --- core/oputil/oputil_help.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index 3e9cb60a..fe76f4b7 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -11,11 +11,11 @@ const usageHelp = exports.USAGE_HELP = { `usage: oputil.js [--version] [--help] [] -global arguments: - -c, --config PATH Specify config path (${getDefaultConfigPath()}) +Global arguments: + -c, --config PATH Specify config path (default is ${getDefaultConfigPath()}) -n, --no-prompt Assume defaults (don't prompt for input where possible) -commands: +Commands: user User management config Configuration management fb File base management @@ -36,13 +36,17 @@ Actions: rename USERNAME NEWNAME Rename a user (mv) - 2fa-otp USERNAME SPEC Enable Two Factor Authentication (2FA) + 2fa-otp USERNAME SPEC Enable 2FA/OTP for the user (otp) + The system supports various implementations of Two Factor Authentication (2FA) + One Time Password (OTP) authentication. + Valid specs: - totp : Time-Based One-Time Password Algorithm (RFC-6238) - hotp : HMAC-Based One-Time Password Algorithm (RFC-4266) - google : Google Authenticator + disable : Removes 2FA/OTP from the user + google : Google Authenticator + hotp : HMAC-Based One-Time Password Algorithm (RFC-4266) + totp : Time-Based One-Time Password Algorithm (RFC-6238) activate USERNAME Set a user's status to "active" From 8802ae24bafd456f7e8ec40d0ab7b86b00558dff Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 24 May 2019 22:27:50 -0600 Subject: [PATCH 040/140] Good progress on 2FA/OTP config --- core/menu_module.js | 29 +++++- core/oputil/oputil_user.js | 20 ++++- core/toggle_menu_view.js | 9 +- core/user_2fa_otp.js | 3 +- core/user_2fa_otp_config.js | 171 ++++++++++++++++++++++++++++++++++++ core/user_property.js | 2 +- core/view.js | 6 ++ 7 files changed, 230 insertions(+), 10 deletions(-) create mode 100644 core/user_2fa_otp_config.js diff --git a/core/menu_module.js b/core/menu_module.js index 06c0f6d7..526482da 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -16,6 +16,7 @@ const { getPredefinedMCIValue } = require('../core/predefined_mci.js'); const async = require('async'); const assert = require('assert'); const _ = require('lodash'); +const iconvDecode = require('iconv-lite').decode; exports.MenuModule = class MenuModule extends PluginModule { @@ -379,7 +380,7 @@ exports.MenuModule = class MenuModule extends PluginModule { ); } - displayAsset(name, options, cb) { + displayAsset(nameOrData, options, cb) { if(_.isFunction(options)) { cb = options; options = {}; @@ -389,10 +390,25 @@ exports.MenuModule = class MenuModule extends PluginModule { this.client.term.rawWrite(ansi.resetScreen()); } + options = Object.assign( { client : this.client, font : this.menuConfig.config.font }, options ); + + if(Buffer.isBuffer(nameOrData)) { + const data = iconvDecode(nameOrData, options.encoding || 'cp437'); + return theme.displayPreparedArt( + options, + { data }, + (err, artData) => { + if(cb) { + return cb(err, artData); + } + } + ); + } + return theme.displayThemedAsset( - name, + nameOrData, this.client, - Object.assign( { font : this.menuConfig.config.font }, options ), + options, (err, artData) => { if(cb) { return cb(err, artData); @@ -513,7 +529,7 @@ exports.MenuModule = class MenuModule extends PluginModule { } setViewText(formName, mciId, text, appendMultiLine) { - const view = this.viewControllers[formName].getView(mciId); + const view = this.getView(formName, mciId); if(!view) { return; } @@ -525,6 +541,11 @@ exports.MenuModule = class MenuModule extends PluginModule { } } + getView(formName, id) { + const form = this.viewControllers[formName]; + return form && form.getView(id); + } + updateCustomViewTextsWithFilter(formName, startId, fmtObj, options) { options = options || {}; diff --git a/core/oputil/oputil_user.js b/core/oputil/oputil_user.js index 92705bff..7b441875 100644 --- a/core/oputil/oputil_user.js +++ b/core/oputil/oputil_user.js @@ -372,12 +372,28 @@ function twoFactorAuthOTP(user) { prepareOTP, } = require('../../core/user_2fa_otp.js'); + let otpType = argv._[argv._.length - 1]; + + // shortcut for removal + if('disable' === otpType) { + const props = [ + UserProps.AuthFactor2OTP, + UserProps.AuthFactor2OTPSecret, + UserProps.AuthFactor2OTPBackupCodes, + ]; + return user.removeProperties(props, err => { + if(err) { + console.error(err.message); + } else { + console.info(`2FA OTP disabled for ${user.username}`); + } + }); + } + async.waterfall( [ function validate(callback) { // :TODO: Prompt for if not supplied - let otpType = argv._[argv._.length - 1]; - // allow aliases for OTP types otpType = { google : OTPTypes.GoogleAuthenticator, diff --git a/core/toggle_menu_view.js b/core/toggle_menu_view.js index aadfe9c3..c06297b2 100644 --- a/core/toggle_menu_view.js +++ b/core/toggle_menu_view.js @@ -35,11 +35,16 @@ util.inherits(ToggleMenuView, MenuView); ToggleMenuView.prototype.redraw = function() { ToggleMenuView.super_.prototype.redraw.call(this); + if(0 === this.items.length) { + return; + } + //this.cachePositions(); this.client.term.write(this.hasFocus ? this.getFocusSGR() : this.getSGR()); - assert(this.items.length === 2); + assert(this.items.length === 2, 'ToggleMenuView must contain exactly (2) items'); + for(var i = 0; i < 2; i++) { var item = this.items[i]; var text = strUtil.stylizeString( @@ -102,7 +107,7 @@ ToggleMenuView.prototype.onKeyPress = function(ch, key) { if(key) { if(this.isKeyMapped('right', key.name) || this.isKeyMapped('down', key.name)) { this.focusNext(); - } else if(this.isKeyMapped('left', key.name) || this.isKeyMapped('up', key.nam4e)) { + } else if(this.isKeyMapped('left', key.name) || this.isKeyMapped('up', key.name)) { this.focusPrevious(); } } diff --git a/core/user_2fa_otp.js b/core/user_2fa_otp.js index 2f51f105..0b53ad8b 100644 --- a/core/user_2fa_otp.js +++ b/core/user_2fa_otp.js @@ -17,10 +17,11 @@ const Config = require('./config.js').get; // deps const _ = require('lodash'); const crypto = require('crypto'); -const async = require('async'); const qrGen = require('qrcode-generator'); exports.prepareOTP = prepareOTP; +exports.createQRCode = createQRCode; +exports.otpFromType = otpFromType; exports.loginFactor2_OTP = loginFactor2_OTP; const OTPTypes = exports.OTPTypes = { diff --git a/core/user_2fa_otp_config.js b/core/user_2fa_otp_config.js new file mode 100644 index 00000000..a4bbb3df --- /dev/null +++ b/core/user_2fa_otp_config.js @@ -0,0 +1,171 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const { MenuModule } = require('./menu_module.js'); +const UserProps = require('./user_property.js'); +const { + OTPTypes, + otpFromType, + createQRCode, +} = require('./user_2fa_otp.js'); + +// deps +const async = require('async'); +const _ = require('lodash'); +const iconv = require('iconv-lite'); + +exports.moduleInfo = { + name : 'User 2FA/OTP Configuration', + desc : 'Module for user 2FA/OTP configuration', + author : 'NuSkooler', +}; + +const FormIds = { + menu : 0, +}; + +const MciViewIds = { + enableToggle : 1, + typeSelection : 2, + submission : 3, + infoText : 4, + + customRangeStart : 10, // 10+ = customs +}; + +exports.getModule = class User2FA_OTPConfigModule extends MenuModule { + constructor(options) { + super(options); + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); + + this.menuMethods = { + showQRCode : (formData, extraArgs, cb) => { + return this.showQRCode(cb); + } + }; + } + + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } + + async.series( + [ + (callback) => { + return this.prepViewController('menu', FormIds.menu, mciData.menu, callback); + }, + (callback) => { + const requiredCodes = [ + MciViewIds.enableToggle, + MciViewIds.typeSelection, + MciViewIds.submission, + ]; + return this.validateMCIByViewIds('menu', requiredCodes, callback); + }, + (callback) => { + const enableToggleView = this.getView('menu', MciViewIds.enableToggle); + let initialIndex = this.isOTPEnabledForUser() ? 1 : 0; + enableToggleView.setFocusItemIndex(initialIndex); + this.enableToggleUpdate(initialIndex); + + enableToggleView.on('index update', idx => { + return this.enableToggleUpdate(idx); + }); + + const typeSelectionView = this.getView('menu', MciViewIds.typeSelection); + initialIndex = this.typeSelectionIndexFromUserOTPType(); + typeSelectionView.setFocusItemIndex(initialIndex); + + typeSelectionView.on('index update', idx => { + return this.typeSelectionUpdate(idx); + }); + + this.viewControllers.menu.on('return', view => { + if(view === enableToggleView) { + return this.enableToggleUpdate(enableToggleView.focusedItemIndex); + } else if (view === typeSelectionView) { + return this.typeSelectionUpdate(typeSelectionView.focusedItemIndex); + } + }); + + return callback(null); + } + ], + err => { + return cb(err); + } + ); + }); + } + + showQRCode(cb) { + const otp = otpFromType(this.client.user.getProperty(UserProps.AuthFactor2OTP)); + let qrCodeAscii = ''; + if(!otp) { + qrCodeAscii = '2FA/OTP is not currently enabled for this account'; + } + + const qrOptions = { + username : this.client.user.username, + qrType : 'ascii', + }; + qrCodeAscii = createQRCode( + otp, + qrOptions, + this.client.user.getProperty(UserProps.AuthFactor2OTPSecret) + ).replace(/\n/g, '\r\n'); + + const modOpts = { + extraArgs : { + artData : iconv.encode(`${qrCodeAscii}\r\n`, 'cp437'), + } + }; + this.gotoMenu( + this.menuConfig.config.mainMenuUser2FAOTP_ShowQR || 'mainMenuUser2FAOTP_ShowQR', + modOpts, + cb + ); + } + + isOTPEnabledForUser() { + return this.typeSelectionIndexFromUserOTPType(-1) != -1; + } + + getInfoText(key) { + return _.get(this.config, [ 'infoText', key ], ''); + } + + enableToggleUpdate(idx) { + const key = { + 0 : '2faDisabled', + 1 : '2faEnabled', + }[idx]; + this.updateCustomViewTextsWithFilter('menu', MciViewIds.customRangeStart, { infoText : this.getInfoText(key) } ); + } + + typeSelectionIndexFromUserOTPType(defaultIndex = 0) { + const type = this.client.user.getProperty(UserProps.AuthFactor2OTP); + return { + [ OTPTypes.RFC6238_TOTP ] : 0, + [ OTPTypes.RFC4266_HOTP ] : 1, + [ OTPTypes.GoogleAuthenticator ] : 2, + }[type] || defaultIndex; + } + + otpTypeFromTypeSelectionIndex(idx) { + return { + 0 : OTPTypes.RFC6238_TOTP, + 1 : OTPTypes.RFC4266_HOTP, + 2 : OTPTypes.GoogleAuthenticator, + }[idx]; + } + + typeSelectionUpdate(idx) { + const key = '2faType_' + this.otpTypeFromTypeSelectionIndex(idx); + this.updateCustomViewTextsWithFilter('menu', MciViewIds.customRangeStart, { infoText : this.getInfoText(key) } ); + } +}; + diff --git a/core/user_property.js b/core/user_property.js index 00f640fb..88ac11b1 100644 --- a/core/user_property.js +++ b/core/user_property.js @@ -60,7 +60,7 @@ module.exports = { SSHPubKey : 'ssh_public_key', // OpenSSH format (ssh-keygen, etc.) AuthFactor1Types : 'auth_factor1_types', // List of User.AuthFactor1Types value(s) - AuthFactor2OTP : 'auth_factor2_otp', // If present, OTP type for 2FA + 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/core/view.js b/core/view.js index 7d3c5693..b675b508 100644 --- a/core/view.js +++ b/core/view.js @@ -178,6 +178,12 @@ View.prototype.setSpecialKeyMapOverride = function(specialKeyMapOverride) { View.prototype.setPropertyValue = function(propName, value) { switch(propName) { + case 'acceptsFocus' : + if (_.isBoolean(value)) { + this.acceptsFocus = value; + } + break; + case 'height' : this.setHeight(value); break; case 'width' : this.setWidth(value); break; case 'focus' : this.setFocus(value); break; From 109b157c02846fe57cbed752389ad5baa4a21423 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 26 May 2019 00:14:36 +0100 Subject: [PATCH 041/140] more MRC bug squashing and tidy up --- core/mrc.js | 84 +++++++++++++++++----------- core/servers/chat/mrc_multiplexer.js | 55 +++++++++++------- 2 files changed, 88 insertions(+), 51 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index 5320c999..54aa70f7 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -5,16 +5,17 @@ const Log = require('./logger.js').log; const { MenuModule } = require('./menu_module.js'); const { - pipeToAnsi + pipeToAnsi, + stripMciColorCodes } = require('./color_codes.js'); -const stringFormat = require('./string_format.js'); -const StringUtil = require('./string_util.js'); +const stringFormat = require('./string_format.js'); +const StringUtil = require('./string_util.js'); // deps const _ = require('lodash'); const async = require('async'); -const net = require('net'); -const moment = require('moment'); +const net = require('net'); +const moment = require('moment'); exports.moduleInfo = { name : 'MRC Client', @@ -107,6 +108,12 @@ exports.getModule = class mrcModule extends MenuModule { this.viewControllers.mrcChat.switchFocus(MciViewIds.mrcChat.inputArea); return cb(null); + }, + + quit : (formData, extraArgs, cb) => { + this.sendServerMessage('LOGOFF'); + this.state.socket.destroy(); + return this.prevMenu(cb); } }; } @@ -167,12 +174,23 @@ exports.getModule = class mrcModule extends MenuModule { */ addMessageToChatLog(message) { const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); - chatLogView.addText(pipeToAnsi(message)); + const messageLength = stripMciColorCodes(message).length; + const chatWidth = chatLogView.dimens.width; + let padAmount = 0; + + if (messageLength > chatWidth) { + padAmount = chatWidth - (messageLength % chatWidth); + } else { + padAmount = chatWidth - messageLength; + } + + const padding = ' |00' + ' '.repeat(padAmount - 2); + chatLogView.addText(pipeToAnsi(message + padding)); } /** - * Processes data received back from the MRC multiplexer - */ + * Processes data received from the MRC multiplexer + */ processReceivedMessage(blob) { blob.split('\n').forEach( message => { @@ -200,13 +218,13 @@ exports.getModule = class mrcModule extends MenuModule { this.state.nicks = params[1].split(','); break; - case 'STATS': + case 'STATS': { const stats = params[1].split(' '); this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.mrcUsers).setText(stats[2]); this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.mrcBbses).setText(stats[0]); this.state.last_ping = stats[1]; - break; + } default: this.addMessageToChatLog(message.body); @@ -214,20 +232,10 @@ exports.getModule = class mrcModule extends MenuModule { } } else { - if (message.from_user == this.state.alias && message.to_user == 'NOTME') { - // don't deliver NOTME messages - return; - } else { + if (message.to_room == this.state.room) { // if we're here then we want to show it to the user const currentTime = moment().format(this.client.currentTheme.helpers.getTimeFormat()); - - if (message.to_user == this.state.alias) { - // it's a pm - this.addMessageToChatLog('|08' + currentTime + ' |14PM|00 ' + message.body + '|00'); - } else { - // it's not a pm - this.addMessageToChatLog('|08' + currentTime + '|00 ' + message.body + '|00'); - } + this.addMessageToChatLog('|08' + currentTime + '|00 ' + message.body + '|00'); } } @@ -240,27 +248,39 @@ exports.getModule = class mrcModule extends MenuModule { */ processOutgoingMessage(message, to_user) { if (message.startsWith('/')) { - this.processSlashCommand(message); - } else { if (message == '') { + // don't do anything if message is blank, just update stats this.sendServerMessage('STATS'); return; } - // just format and send + // else just format and send const textFormatObj = { fromUserName : this.state.alias, + toUserName : to_user, message : message }; const messageFormat = this.config.messageFormat || '|00|10<|02{fromUserName}|10>|00 |03{message}|00'; + + const privateMessageFormat = + this.config.outgoingPrivateMessageFormat || + '|00|10<|02{fromUserName}|10|14->|02{toUserName}>|00 |03{message}|00'; + let formattedMessage = ''; + if (to_user == undefined) { + // normal message + formattedMessage = stringFormat(messageFormat, textFormatObj); + } else { + // pm + formattedMessage = stringFormat(privateMessageFormat, textFormatObj); + } + try { - const formattedMessage = stringFormat(messageFormat, textFormatObj); this.sendMessageToMultiplexer(to_user || '', '', this.state.room, formattedMessage); } catch(e) { this.client.log.warn( { error : e.message }, 'MRC error'); @@ -283,7 +303,7 @@ exports.getModule = class mrcModule extends MenuModule { case 'pm': this.processOutgoingMessage(cmd[2], cmd[1]); break; - case 'rainbow': + case 'rainbow': { // this is brutal, but i love it const line = message.replace(/^\/rainbow\s/, '').split(' ').reduce(function (a, c) { const cc = Math.floor((Math.random() * 31) + 1).toString().padStart(2, '0'); @@ -293,17 +313,17 @@ exports.getModule = class mrcModule extends MenuModule { this.processOutgoingMessage(line); break; - + } case 'l33t': - this.processOutgoingMessage(StringUtil.stylizeString(message.substr(5), 'l33t')); + this.processOutgoingMessage(StringUtil.stylizeString(message.substr(6), 'l33t')); break; - case 'kewl': + case 'kewl': { const text_modes = Array('f','v','V','i','M'); const mode = text_modes[Math.floor(Math.random() * text_modes.length)]; - this.processOutgoingMessage(StringUtil.stylizeString(message.substr(5), mode)); + this.processOutgoingMessage(StringUtil.stylizeString(message.substr(6), mode)); break; - + } case 'whoon': this.sendServerMessage('WHOON'); break; diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js index 9410a237..7eaac22f 100644 --- a/core/servers/chat/mrc_multiplexer.js +++ b/core/servers/chat/mrc_multiplexer.js @@ -2,10 +2,12 @@ 'use strict'; // ENiGMA½ -const Log = require('../../logger.js').log; -const { ServerModule } = require('../../server_module.js'); -const Config = require('../../config.js').get; -const { Errors } = require('../../enig_error.js'); +const Log = require('../../logger.js').log; +const { ServerModule } = require('../../server_module.js'); +const Config = require('../../config.js').get; +const { Errors } = require('../../enig_error.js'); +const SysProps = require('../../system_property.js'); +const StatLog = require('../../stat_log.js'); // deps const net = require('net'); @@ -30,9 +32,7 @@ let mrcCentralConnection = ''; exports.getModule = class MrcModule extends ServerModule { constructor() { super(); - this.log = Log.child( { server : 'MRC' } ); - } createServer(cb) { @@ -43,12 +43,12 @@ exports.getModule = class MrcModule extends ServerModule { const self = this; const config = Config(); - const boardName = config.general.boardName; + const boardName = config.general.prettyBoardName || config.general.boardName; const enigmaVersion = 'ENiGMA-BBS_' + require('../../../package.json').version; const mrcConnectOpts = { - port : 50000, - host : 'mrc.bottomlessabyss.net' + host : config.chatServers.mrc.serverHostname || 'mrc.bottomlessabyss.net', + port : config.chatServers.mrc.serverPort || 5000 }; const handshake = `${boardName}~${enigmaVersion}/${os.platform()}-${os.arch()}/${PROTOCOL_VERSION}`; @@ -91,7 +91,6 @@ exports.getModule = class MrcModule extends ServerModule { socket.on('data', data => { // split on \n to deal with getting messages in batches data.toString().split('\n').forEach( item => { - if (item == '') return; // save username with socket @@ -103,7 +102,6 @@ exports.getModule = class MrcModule extends ServerModule { self.receiveFromClient(socket.username, item); } }); - }); socket.on('end', function() { @@ -117,7 +115,6 @@ exports.getModule = class MrcModule extends ServerModule { }); }); - return cb(null); } @@ -146,22 +143,31 @@ exports.getModule = class MrcModule extends ServerModule { return _.isNumber(_.get(config, 'chatServers.mrc.multiplexerPort')); } + /** + * Sends received messages to local clients + */ sendToClient(message) { connectedSockets.forEach( (client) => { - if (message.to_user == '' || message.to_user == client.username || message.to_user == 'CLIENT') { - // this.log.debug({ server : 'MRC', username : client.username, message : message }, 'Forwarding message to connected user') + if (message.to_user == '' || message.to_user == client.username || message.to_user == 'CLIENT' || message.from_user == client.username || message.to_user == 'NOTME' ) { + this.log.debug({ server : 'MRC', username : client.username, message : message }, 'Forwarding message to connected user'); client.write(JSON.stringify(message) + '\n'); + } else { + this.log.debug({ server : 'MRC', username : client.username, message : message }, 'Not forwarding message'); } }); } + /** + * Processes messages received // split raw data received into an object we can work withfrom the central MRC server + */ receiveFromMRC(socket, message) { const config = Config(); const siteName = slugify(config.general.boardName); if (message.from_user == 'SERVER' && message.body == 'HELLO') { - // initial server hello, can ignore + // reply with extra bbs info + this.sendToMrcServer(socket, 'CLIENT', '', 'SERVER', 'ALL', '', `INFOSYS:${StatLog.getSystemStat(SysProps.SysOpUsername)}`); } else if (message.from_user == 'SERVER' && message.body.toUpperCase() == 'PING') { // reply to heartbeat @@ -174,7 +180,9 @@ exports.getModule = class MrcModule extends ServerModule { } } - // split raw data received into an object we can work with + /** + * Takes an MRC message and parses it into something usable + */ parseMessage(line) { const msg = line.split('~'); if (msg.length < 7) { @@ -192,6 +200,9 @@ exports.getModule = class MrcModule extends ServerModule { }; } + /** + * Receives a message from a local client and sanity checks before sending on to the central MRC server + */ receiveFromClient(username, message) { try { message = JSON.parse(message); @@ -202,7 +213,9 @@ exports.getModule = class MrcModule extends ServerModule { this.sendToMrcServer(mrcCentralConnection, message.from_user, message.from_room, message.to_user, message.to_site, message.to_room, message.body); } - // send a message back to the mrc central server + /** + * Converts a message back into the MRC format and sends it to the central MRC server + */ sendToMrcServer(socket, fromUser, fromRoom, toUser, toSite, toRoom, messageBody) { const config = Config(); const siteName = slugify(config.general.boardName); @@ -222,8 +235,9 @@ exports.getModule = class MrcModule extends ServerModule { } }; - -// User / site name must be ASCII 33-125, no MCI, 30 chars max, underscores +/** + * User / site name must be ASCII 33-125, no MCI, 30 chars max, underscores + */ function sanitiseName(str) { return str.replace( /\s/g, '_' @@ -242,6 +256,9 @@ function sanitiseMessage(message) { return message.replace(/[^\x20-\x7D]/g, ''); } +/** + * SLugifies the BBS name for use as an MRC "site name" + */ function slugify(text) { return text.toString() .replace(/\s+/g, '_') // Replace spaces with _ From b30d0e189d64fe1f9f804eb1df367ebabb9832fd Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 26 May 2019 00:15:34 +0100 Subject: [PATCH 042/140] Add MRC config to luciano_blocktronics theme --- art/themes/luciano_blocktronics/theme.hjson | 33 +++++++++++++++++---- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 998d4c34..49d459f6 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -1078,14 +1078,37 @@ } } - - //////////////////////////////// ERC /////////////////////////////// - - ercClient: { + mrc: { config: { - //chatEntryFormat: "|00|08[|03{bbsTag}|08] |10{userName}|08: |02{message}" + messageFormat: "|00|10<|02{fromUserName}|10>|00 |03{message}|00" + privateMessageFormat: "|00|10<|02{fromUserName}|15->{toUserName}|10>|00 |03{message}|00" + } + 0: { + mci: { + MT1: { + width: 72 + height: 18 + } + ET2: { + width: 69 // fnarr! + maxLength: 140 + } + TL3: { + width: 20 + } + TL4: { + width: 20 + } + TL5: { + width: 2 + } + TL6: { + width: 2 + } + } } } + } prompts: { From 0b9599755485534cfbacb77b4a0ecb8c5b0a6993 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 26 May 2019 00:16:49 +0100 Subject: [PATCH 043/140] MRC general config --- core/config.js | 12 ++++++++-- misc/menu_template.in.hjson | 46 +++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/core/config.js b/core/config.js index 854e65c8..ea309fc6 100644 --- a/core/config.js +++ b/core/config.js @@ -452,9 +452,17 @@ function getDefaultConfig() { chatServers : { mrc: { enabled : true, + serverHostname : 'mrc.bottomlessabyss.net', + serverPort : 5000, multiplexerPort : 5000, - serverHostname : "mrc.bottomlessabyss.com", - serverPort : 5000 + bbsInfo : { + sysop : '', + telnet : '', + website : '', + ssh : '', + description : '', + + } } }, diff --git a/misc/menu_template.in.hjson b/misc/menu_template.in.hjson index 9aac86f3..9ae54d17 100644 --- a/misc/menu_template.in.hjson +++ b/misc/menu_template.in.hjson @@ -1066,6 +1066,10 @@ value: { command: "UA" } action: @menu:mainMenuUserAchievementsEarned } + { + value: { command: "MRC" } + action: @menu:mrc + } { value: 1 action: @menu:mainMenu @@ -1094,6 +1098,48 @@ } } + mrc: { + desc: MRC Chat + module: mrc + art: MRC + config: { + cls: true + } + form: { + 0: { + mci: { + MT1: { + mode: preview + autoScroll: true + } + ET2: { + argName: inputArea + submit: true + focus: true + } + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + { + keys: [ "down arrow", "up arrow", "page up", "page down" ] + action: @method:movementKeyPressed + } + ] + submit: { + *: [ + { + value: { inputArea: null } + action: @method:sendChatMessage + } + ] + } + } + } + } + nodeMessage: { desc: Node Messaging module: node_msg From 27e3d50c5d9edc8e57dd0ce60e53aa665951331f Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 26 May 2019 23:32:22 +0100 Subject: [PATCH 044/140] Fix mrc debug menu config --- config/menu.hjson | 4237 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 4237 insertions(+) create mode 100644 config/menu.hjson diff --git a/config/menu.hjson b/config/menu.hjson new file mode 100644 index 00000000..99585ade --- /dev/null +++ b/config/menu.hjson @@ -0,0 +1,4237 @@ +{ + /* + ./\/\.' ENiGMA½ Menu Configuration -/--/-------- - -- - + + _____________________ _____ ____________________ __________\_ / + \__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp! + // __|___// | \// |// | \// | | \// \ /___ /_____ + /____ _____| __________ ___|__| ____| \ / _____ \ + ---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/ + /__ _\ + <*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/ + + *-----------------------------------------------------------------------------* + + General Information + ------------------------------- - - + This configuration is in HJSON (http://hjson.org/) format. Strict to-spec + JSON is also perfectly valid. Use 'hjson' from npm to convert to/from JSON. + + See http://hjson.org/ for more information and syntax. + + Various editors and IDEs such as Sublime Text 3, Visual Studio Code, and so + on have syntax highlighting for the HJSON format which are highly recommended. + + ------------------------------- -- - - + Menu Configuration + ------------------------------- - - + ENiGMA½ makes no assumptions about specific menu types (main, doors, etc.), + but instead allows full customization of all menus throughout the system. + Some menus such as a main menu are considered "standard" while others are + backed by a specific module. SysOps can tweak various settings about these + modules (look & feel, keyboard interation, and so on) or even fully replace + the module with something else. + + This file starts out as an example setup. Look at the examples, change + settings, menu ordering/flow, add/remove menus, implement ACS control, + etc.! + + Remember you can *live edit* this file. That is, make a change and save + while you're logged into the system and it will take effect on the next + menu change or screen refresh. + + Please see RTFM ...er, uh... see the documentation for more information, and + don't be shy to ask for help: + + BBS : Xibalba @ xibalba.l33t.codes + FTN : BBS Discussion on fsxNet + IRC : #enigma-bbs / FreeNode + Email : bryan@l33t.codes + */ + menus: { + // + // Send telnet connections to matrix where users can login, apply, etc. + // + telnetConnected: { + art: CONNECT + next: matrix + config: { nextTimeout: 1500 } + } + + // + // SSH connections are pre-authenticated via the SSH server itself. + // Jump directly to the login sequence + // + sshConnected: { + art: CONNECT + next: fullLoginSequenceLoginArt + config: { nextTimeout: 1500 } + } + + // + // Another SSH specialization: If the user logs in with a new user + // name (e.g. "new", "apply", ...) they will be directed to the + // application process. + // + sshConnectedNewUser: { + art: CONNECT + next: newUserApplicationPreSsh + config: { nextTimeout: 1500 } + } + + // Ye ol' standard matrix + matrix: { + art: matrix + form: { + 0: { + VM: { + mci: { + VM1: { + submit: true + focus: true + argName: navSelect + // + // To enable forgot password, you will need to have the web server + // enabled and mail/SMTP configured. Once that is in place, swap out + // the commented lines below as well as in the submit block + // + items: [ + { + text: login + data: login + } + { + text: apply + data: apply + } + { + text: forgot pass + data: forgot + } + { + text: log off + data: logoff + } + ] + } + } + submit: { + *: [ + { + value: { navSelect: "login" } + action: @menu:login + } + { + value: { navSelect: "apply" } + action: @menu:newUserApplicationPre + } + { + value: { navSelect: "forgot" } + action: @menu:forgotPassword + } + { + value: { navSelect: "logoff" } + action: @menu:logoff + } + ] + } + } + } + } + } + + login: { + art: USERLOG + next: fullLoginSequenceLoginArt + config: { + tooNodeMenu: loginAttemptTooNode + inactive: loginAttemptAccountInactive + disabled: loginAttemptAccountDisabled + locked: loginAttemptAccountLocked + } + form: { + 0: { + mci: { + ET1: { + maxLength: @config:users.usernameMax + argName: username + focus: true + } + ET2: { + password: true + maxLength: @config:users.passwordMax + argName: password + submit: true + } + } + submit: { + *: [ + { + value: { password: null } + action: @systemMethod:login + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + loginAttemptTooNode: { + art: TOONODE + config: { + cls: true + nextTimeout: 2000 + } + next: logoff + } + + loginAttemptAccountLocked: { + art: ACCOUNTLOCKED + config: { + cls: true + nextTimeout: 2000 + } + next: logoff + } + + loginAttemptAccountDisabled: { + art: ACCOUNTDISABLED + config: { + cls: true + nextTimeout: 2000 + } + next: logoff + } + + loginAttemptAccountInactive: { + art: ACCOUNTINACTIVE + config: { + cls: true + nextTimeout: 2000 + } + next: logoff + } + + forgotPassword: { + desc: Forgot password + prompt: forgotPasswordPrompt + submit: [ + { + value: { username: null } + action: @systemMethod:sendForgotPasswordEmail + extraArgs: { next: "forgotPasswordSubmitted" } + } + ] + } + + forgotPasswordSubmitted: { + desc: Forgot password + art: FORGOTPWSENT + config: { + cls: true + pause: true + } + next: @systemMethod:logoff + } + + // :TODO: Prompt Yes/No for logoff confirm + fullLogoffSequence: { + desc: Logging Off + prompt: logoffConfirmation + submit: [ + { + value: { promptValue: 0 } + action: @menu:fullLogoffSequencePreAd + } + { + value: { promptValue: 1 } + action: @systemMethod:prevMenu + } + ] + } + + fullLogoffSequencePreAd: { + art: PRELOGAD + desc: Logging Off + next: fullLogoffSequenceRandomBoardAd + config: { + cls: true + nextTimeout: 1500 + } + } + + fullLogoffSequenceRandomBoardAd: { + art: OTHRBBS + desc: Logging Off + next: logoff + config: { + baudRate: 57600 + pause: true + cls: true + } + } + + logoff: { + art: LOGOFF + desc: Logging Off + next: @systemMethod:logoff + } + + // A quick preamble - defaults to warning about broken terminals + newUserApplicationPre: { + art: NEWUSER1 + next: newUserApplication + desc: Applying + config: { + pause: true + cls: true + menuFlags: [ "noHistory" ] + } + } + + newUserApplication: { + module: nua + art: NUA + next: [ + { + // Initial SysOp does not send feedback to themselves + acs: ID1 + next: fullLoginSequenceLoginArt + } + { + // ...everyone else does + next: newUserFeedbackToSysOpPreamble + } + ] + form: { + 0: { + mci: { + ET1: { + focus: true + argName: username + maxLength: @config:users.usernameMax + validate: @systemMethod:validateUserNameAvail + } + ET2: { + argName: realName + maxLength: @config:users.realNameMax + validate: @systemMethod:validateNonEmpty + } + MET3: { + argName: birthdate + maskPattern: "####/##/##" + validate: @systemMethod:validateBirthdate + } + ME4: { + argName: sex + maskPattern: A + textStyle: upper + validate: @systemMethod:validateNonEmpty + } + ET5: { + argName: location + maxLength: @config:users.locationMax + validate: @systemMethod:validateNonEmpty + } + ET6: { + argName: affils + maxLength: @config:users.affilsMax + } + ET7: { + argName: email + maxLength: @config:users.emailMax + validate: @systemMethod:validateEmailAvail + } + ET8: { + argName: web + maxLength: @config:users.webMax + } + ET9: { + argName: password + password: true + maxLength: @config:users.passwordMax + validate: @systemMethod:validatePasswordSpec + } + ET10: { + argName: passwordConfirm + password: true + maxLength: @config:users.passwordMax + validate: @method:validatePassConfirmMatch + } + TM12: { + argName: submission + items: [ "apply", "cancel" ] + submit: true + } + } + + submit: { + *: [ + { + value: { "submission" : 0 } + action: @method:submitApplication + extraArgs: { + inactive: userNeedsActivated + error: newUserCreateError + } + } + { + value: { "submission" : 1 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + // A quick preamble - defaults to warning about broken terminals (SSH version) + newUserApplicationPreSsh: { + art: NEWUSER1 + next: newUserApplicationSsh + desc: Applying + config: { + pause: true + cls: true + menuFlags: [ "noHistory" ] + } + } + + // + // SSH specialization of NUA + // Canceling this form logs off vs falling back to matrix + // + newUserApplicationSsh: { + module: nua + art: NUA + fallback: logoff + next: newUserFeedbackToSysOpPreamble + form: { + 0: { + mci: { + ET1: { + focus: true + argName: username + maxLength: @config:users.usernameMax + validate: @systemMethod:validateUserNameAvail + } + ET2: { + argName: realName + maxLength: @config:users.realNameMax + validate: @systemMethod:validateNonEmpty + } + MET3: { + argName: birthdate + maskPattern: "####/##/##" + validate: @systemMethod:validateBirthdate + } + ME4: { + argName: sex + maskPattern: A + textStyle: upper + validate: @systemMethod:validateNonEmpty + } + ET5: { + argName: location + maxLength: @config:users.locationMax + validate: @systemMethod:validateNonEmpty + } + ET6: { + argName: affils + maxLength: @config:users.affilsMax + } + ET7: { + argName: email + maxLength: @config:users.emailMax + validate: @systemMethod:validateEmailAvail + } + ET8: { + argName: web + maxLength: @config:users.webMax + } + ET9: { + argName: password + password: true + maxLength: @config:users.passwordMax + validate: @systemMethod:validatePasswordSpec + } + ET10: { + argName: passwordConfirm + password: true + maxLength: @config:users.passwordMax + validate: @method:validatePassConfirmMatch + } + TM12: { + argName: submission + items: [ "apply", "cancel" ] + submit: true + } + } + + submit: { + *: [ + { + value: { "submission" : 0 } + action: @method:submitApplication + extraArgs: { + inactive: userNeedsActivated + error: newUserCreateError + } + } + { + value: { "submission" : 1 } + action: @systemMethod:logoff + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:logoff + } + ] + } + } + } + + newUserFeedbackToSysOpPreamble: { + art: LETTER + config: { pause: true } + next: newUserFeedbackToSysOp + } + + newUserFeedbackToSysOp: { + desc: Feedback to SysOp + module: msg_area_post_fse + next: [ + { + acs: AS2 + next: fullLoginSequenceLoginArt + } + { + next: newUserInactiveDone + } + ] + config: { + art: { + header: MSGEHDR + body: MSGBODY + footerEditor: MSGEFTR + footerEditorMenu: MSGEMFT + help: MSGEHLP + }, + editorMode: edit + editorType: email + messageAreaTag: private_mail + toUserId: 1 /* always to +op */ + } + form: { + 0: { + mci: { + TL1: { + argName: from + } + ET2: { + argName: to + focus: true + text: @sysStat:sysop_username + // :TODO: readOnly: true + } + ET3: { + argName: subject + maxLength: 72 + submit: true + text: New user feedback + validate: @systemMethod:validateMessageSubject + } + } + submit: { + 3: [ + { + value: { subject: null } + action: @method:headerSubmit + } + ] + } + } + 1: { + mci: { + MT1: { + width: 79 + argName: message + mode: edit + } + } + + submit: { + *: [ { value: "message", action: "@method:editModeEscPressed" } ] + } + actionKeys: [ + { + keys: [ "escape" ] + viewId: 1 + } + ] + }, + 2: { + TLTL: { + mci: { + TL1: { + width: 5 + } + TL2: { + width: 4 + } + } + } + } + 3: { + HM: { + mci: { + HM1: { + // :TODO: clear + items: [ "save", "help" ] + } + } + submit: { + *: [ + { + value: { 1: 0 } + action: @method:editModeMenuSave + } + { + value: { 1: 1 } + action: @method:editModeMenuHelp + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:editModeEscPressed + } + { + keys: [ "?" ] + action: @method:editModeMenuHelp + } + ] + } + } + } + } + + newUserInactiveDone: { + desc: Finished with NUA + art: DONE + config: { pause: true } + next: @menu:logoff + } + + fullLoginSequenceLoginArt: { + desc: Logging In + art: WELCOME + config: { pause: true } + next: fullLoginSequenceLastCallers + } + + fullLoginSequenceLastCallers: { + desc: Last Callers + module: last_callers + art: LASTCALL + config: { + pause: true + font: cp437 + } + next: fullLoginSequenceWhosOnline + } + fullLoginSequenceWhosOnline: { + desc: Who's Online + module: whos_online + art: WHOSON + config: { pause: true } + next: fullLoginSequenceOnelinerz + } + + fullLoginSequenceOnelinerz: { + desc: Viewing Onelinerz + module: onelinerz + next: [ + { + // calls >= 2 + acs: NC2 + next: fullLoginSequenceNewScanConfirm + } + { + // new users - skip new scan + next: fullLoginSequenceUserStats + } + ] + config: { + cls: true + art: { + view: ONELINER + add: ONEADD + } + } + form: { + 0: { + mci: { + VM1: { + focus: false + height: 10 + } + TM2: { + argName: addOrExit + items: [ "yeah!", "nah" ] + "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } + submit: true + focus: true + } + } + submit: { + *: [ + { + value: { addOrExit: 0 } + action: @method:viewAddScreen + } + { + value: { addOrExit: null } + action: @systemMethod:nextMenu + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:nextMenu + } + ] + }, + 1: { + mci: { + ET1: { + focus: true + maxLength: 70 + argName: oneliner + } + TL2: { + width: 60 + } + TM3: { + argName: addOrCancel + items: [ "add", "cancel" ] + "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } + submit: true + } + } + + submit: { + *: [ + { + value: { addOrCancel: 0 } + action: @method:addEntry + } + { + value: { addOrCancel: 1 } + action: @method:cancelAdd + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:cancelAdd + } + ] + } + } + } + + fullLoginSequenceNewScanConfirm: { + desc: Logging In + prompt: loginGlobalNewScan + submit: [ + { + value: { promptValue: 0 } + action: @menu:fullLoginSequenceNewScan + } + { + value: { promptValue: 1 } + action: @menu:fullLoginSequenceUserStats + } + ] + } + + fullLoginSequenceNewScan: { + desc: Performing New Scan + module: new_scan + art: NEWSCAN + next: fullLoginSequenceSysStats + config: { + messageListMenu: newScanMessageList + } + } + + fullLoginSequenceSysStats: { + desc: System Stats + art: SYSSTAT + config: { pause: true } + next: fullLoginSequenceUserStats + } + fullLoginSequenceUserStats: { + desc: User Stats + art: STATUS + config: { pause: true } + next: mainMenu + } + + newScanMessageList: { + desc: New Messages + module: msg_list + art: NEWMSGS + config: { + menuViewPost: messageAreaViewPost + } + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: message + } + TL6: { + // theme me! + } + } + submit: { + *: [ + { + value: { message: null } + action: @method:selectMessage + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + { + keys: [ "x", "shift + x" ] + action: @method:fullExit + } + { + keys: [ "m", "shift + m" ] + action: @method:markAllRead + } + ] + } + } + } + + newScanFileBaseList: { + module: file_area_list + desc: New Files + config: { + art: { + browse: FNEWBRWSE + details: FDETAIL + detailsGeneral: FDETGEN + detailsNfo: FDETNFO + detailsFileList: FDETLST + help: FBHELP + } + } + form: { + 0: { + mci: { + MT1: { + mode: preview + ansiView: true + } + + HM2: { + focus: true + submit: true + argName: navSelect + items: [ + "prev", "next", "details", "toggle queue", "rate", "help", "quit" + ] + focusItemIndex: 1 + } + } + + submit: { + *: [ + { + value: { navSelect: 0 } + action: @method:prevFile + } + { + value: { navSelect: 1 } + action: @method:nextFile + } + { + value: { navSelect: 2 } + action: @method:viewDetails + } + { + value: { navSelect: 3 } + action: @method:toggleQueue + } + { + value: { navSelect: 4 } + action: @menu:fileBaseGetRatingForSelectedEntry + } + { + value: { navSelect: 5 } + action: @method:displayHelp + } + { + value: { navSelect: 6 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: [ + { + keys: [ "w", "shift + w" ] + action: @method:showWebDownloadLink + } + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + { + keys: [ "t", "shift + t" ] + action: @method:toggleQueue + } + { + keys: [ "v", "shift + v" ] + action: @method:viewDetails + } + { + keys: [ "r", "shift + r" ] + action: @menu:fileBaseGetRatingForSelectedEntry + } + { + keys: [ "?" ] + action: @method:displayHelp + } + ] + } + + 1: { + mci: { + HM1: { + focus: true + submit: true + argName: navSelect + items: [ + "general", "nfo/readme", "file listing" + ] + } + } + + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @method:detailsQuit + } + ] + } + + 2: { + // details - general + mci: {} + } + + 3: { + // details - nfo/readme + mci: { + MT1: { + mode: preview + } + } + } + + 4: { + // details - file listing + mci: { + VM1: { + + } + } + } + } + } + + /////////////////////////////////////////////////////////////////////// + // Main Menu + /////////////////////////////////////////////////////////////////////// + mainMenu: { + art: MMENU + desc: Main Menu + prompt: menuCommand + config: { + font: cp437 + interrupt: realtime + } + submit: [ + { + value: { command: "MSG" } + action: @menu:nodeMessage + } + { + value: { command: "MRC" } + action: @menu:mrc + } + { + value: { command: "G" } + action: @menu:fullLogoffSequence + } + { + value: { command: "D" } + action: @menu:doorMenu + } + { + value: { command: "F" } + action: @menu:fileBase + } + { + value: { command: "U" } + action: @menu:mainMenuUserList + } + { + value: { command: "L" } + action: @menu:mainMenuLastCallers + } + { + value: { command: "W" } + action: @menu:mainMenuWhosOnline + } + { + value: { command: "Y" } + action: @menu:mainMenuUserStats + } + { + value: { command: "M" } + action: @menu:messageArea + } + { + value: { command: "E" } + action: @menu:mailMenu + } + { + value: { command: "C" } + action: @menu:mainMenuUserConfig + } + { + value: { command: "S" } + action: @menu:mainMenuSystemStats + } + { + value: { command: "!" } + action: @menu:mainMenuGlobalNewScan + } + { + value: { command: "K" } + action: @menu:mainMenuFeedbackToSysOp + } + { + value: { command: "O" } + action: @menu:mainMenuOnelinerz + } + { + value: { command: "R" } + action: @menu:mainMenuRumorz + } + { + value: { command: "BBS"} + action: @menu:bbsList + } + { + value: { command: "UA" } + action: @menu:mainMenuUserAchievementsEarned + } + { + value: 1 + action: @menu:mainMenu + } + ] + } + + mainMenuUserAchievementsEarned: { + desc: Achievements + module: user_achievements_earned + art: USERACHIEV + form: { + 0: { + mci: { + VM1: { + focus: true + } + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + ercClient: { + art: erc + module: erc_client + config: { + host: localhost + port: 5001 + bbsTag: CHANGEME + } + + form: { + 0: { + mci: { + MT1: { + width: 79 + height: 21 + mode: preview + autoScroll: true + } + ET3: { + autoScale: false + width: 77 + argName: inputArea + focus: true + submit: true + } + } + + submit: { + *: [ + { + value: { inputArea: null } + action: @method:inputAreaSubmit + } + ] + } + actionKeys: [ + { + keys: [ "tab" ] + } + { + keys: [ "up arrow" ] + action: @method:scrollDown + } + { + keys: [ "down arrow" ] + action: @method:scrollUp + } + ] + } + } + } + + mrc: { + desc: MRC Chat + module: mrc + art: MRC + config: { + cls: true + } + form: { + 0: { + mci: { + MT1: { + mode: preview + autoScroll: true + } + ET2: { + argName: inputArea + submit: true + focus: true + } + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:quit + } + { + keys: [ "down arrow", "up arrow", "page up", "page down" ] + action: @method:movementKeyPressed + } + ] + submit: { + *: [ + { + value: { inputArea: null } + action: @method:sendChatMessage + } + ] + } + } + } + } + + nodeMessage: { + desc: Node Messaging + module: node_msg + art: NODEMSG + config: { + cls: true + art: { + header: NODEMSGHDR + footer: NODEMSGFTR + } + } + form: { + 0: { + mci: { + SM1: { + argName: node + } + ET2: { + argName: message + submit: true + } + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + submit: { + *: [ + { + value: { message: null } + action: @method:sendMessage + } + ] + } + } + } + } + + mainMenuLastCallers: { + desc: Last Callers + module: last_callers + art: LASTCALL + config: { pause: true } + } + + mainMenuWhosOnline: { + desc: Who's Online + module: whos_online + art: WHOSON + config: { pause: true } + } + + mainMenuUserStats: { + desc: User Stats + art: STATUS + config: { pause: true } + } + + mainMenuSystemStats: { + desc: System Stats + art: SYSSTAT + config: { pause: true } + } + + mainMenuUserList: { + desc: User Listing + module: user_list + art: USERLST + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + } + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + mainMenuUserConfig: { + module: user_config + art: CONFSCR + form: { + 0: { + mci: { + ET1: { + argName: realName + maxLength: @config:users.realNameMax + validate: @systemMethod:validateNonEmpty + focus: true + } + ME2: { + argName: birthdate + maskPattern: "####/##/##" + } + ME3: { + argName: sex + maskPattern: A + textStyle: upper + validate: @systemMethod:validateNonEmpty + } + ET4: { + argName: location + maxLength: @config:users.locationMax + validate: @systemMethod:validateNonEmpty + } + ET5: { + argName: affils + maxLength: @config:users.affilsMax + } + ET6: { + argName: email + maxLength: @config:users.emailMax + validate: @method:validateEmailAvail + } + ET7: { + argName: web + maxLength: @config:users.webMax + } + ME8: { + maskPattern: "##" + argName: termHeight + validate: @systemMethod:validateNonEmpty + } + SM9: { + argName: theme + } + ET10: { + argName: password + maxLength: @config:users.passwordMax + password: true + validate: @method:validatePassword + } + ET11: { + argName: passwordConfirm + maxLength: @config:users.passwordMax + password: true + validate: @method:validatePassConfirmMatch + } + TM25: { + argName: submission + items: [ "save", "cancel" ] + submit: true + } + } + + submit: { + *: [ + { + value: { submission: 0 } + action: @method:saveChanges + } + { + value: { submission: 1 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + mainMenuGlobalNewScan: { + desc: Performing New Scan + module: new_scan + art: NEWSCAN + config: { + messageListMenu: newScanMessageList + } + } + + mainMenuFeedbackToSysOp: { + desc: Feedback to SysOp + module: msg_area_post_fse + config: { + art: { + header: MSGEHDR + body: MSGBODY + footerEditor: MSGEFTR + footerEditorMenu: MSGEMFT + help: MSGEHLP + }, + editorMode: edit + editorType: email + messageAreaTag: private_mail + toUserId: 1 /* always to +op */ + } + form: { + 0: { + mci: { + TL1: { + argName: from + } + ET2: { + argName: to + focus: true + text: @sysStat:sysop_username + // :TODO: readOnly: true + } + ET3: { + argName: subject + maxLength: 72 + submit: true + validate: @systemMethod:validateMessageSubject + } + } + submit: { + 3: [ + { + value: { subject: null } + action: @method:headerSubmit + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + 1: { + mci: { + MT1: { + width: 79 + argName: message + mode: edit + } + } + + submit: { + *: [ { value: "message", action: "@method:editModeEscPressed" } ] + } + actionKeys: [ + { + keys: [ "escape" ] + viewId: 1 + } + ] + }, + 2: { + TLTL: { + mci: { + TL1: { + width: 5 + } + TL2: { + width: 4 + } + } + } + } + 3: { + HM: { + mci: { + HM1: { + // :TODO: clear + items: [ "save", "discard", "help" ] + } + } + submit: { + *: [ + { + value: { 1: 0 } + action: @method:editModeMenuSave + } + { + value: { 1: 1 } + action: @systemMethod:prevMenu + } + { + value: { 1: 2 } + action: @method:editModeMenuHelp + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:editModeEscPressed + } + { + keys: [ "?" ] + action: @method:editModeMenuHelp + } + ] + } + } + } + } + + mainMenuOnelinerz: { + desc: Viewing Onelinerz + module: onelinerz + config: { + cls: true + art: { + view: ONELINER + add: ONEADD + } + } + form: { + 0: { + mci: { + VM1: { + focus: false + height: 10 + } + TM2: { + argName: addOrExit + items: [ "yeah!", "nah" ] + "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } + submit: true + focus: true + } + } + submit: { + *: [ + { + value: { addOrExit: 0 } + action: @method:viewAddScreen + } + { + value: { addOrExit: null } + action: @systemMethod:nextMenu + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:nextMenu + } + ] + }, + 1: { + mci: { + ET1: { + focus: true + maxLength: 70 + argName: oneliner + } + TL2: { + width: 60 + } + TM3: { + argName: addOrCancel + items: [ "add", "cancel" ] + "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } + submit: true + } + } + + submit: { + *: [ + { + value: { addOrCancel: 0 } + action: @method:addEntry + } + { + value: { addOrCancel: 1 } + action: @method:cancelAdd + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:cancelAdd + } + ] + } + } + } + + mainMenuRumorz: { + desc: Rumorz + module: rumorz + config: { + cls: true + art: { + entries: RUMORS + add: RUMORADD + } + } + form: { + 0: { + mci: { + VM1: { + focus: false + height: 10 + } + TM2: { + argName: addOrExit + items: [ "yeah!", "nah" ] + "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } + submit: true + focus: true + } + } + submit: { + *: [ + { + value: { addOrExit: 0 } + action: @method:viewAddScreen + } + { + value: { addOrExit: null } + action: @systemMethod:nextMenu + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:nextMenu + } + ] + }, + 1: { + mci: { + ET1: { + focus: true + maxLength: 70 + argName: rumor + } + TL2: { + width: 60 + } + TM3: { + argName: addOrCancel + items: [ "add", "cancel" ] + "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } + submit: true + } + } + + submit: { + *: [ + { + value: { addOrCancel: 0 } + action: @method:addEntry + } + { + value: { addOrCancel: 1 } + action: @method:cancelAdd + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:cancelAdd + } + ] + } + } + } + + bbsList: { + desc: Viewing BBS List + module: bbs_list + config: { + cls: true + art: { + entries: BBSLIST + add: BBSADD + } + } + + form: { + 0: { + mci: { + VM1: { maxLength: 32 } + TL2: { maxLength: 32 } + TL3: { maxLength: 32 } + TL4: { maxLength: 32 } + TL5: { maxLength: 32 } + TL6: { maxLength: 32 } + TL7: { maxLength: 32 } + TL8: { maxLength: 32 } + TL9: { maxLength: 32 } + } + actionKeys: [ + { + keys: [ "a" ] + action: @method:addBBS + } + { + // :TODO: add delete key + keys: [ "d" ] + action: @method:deleteBBS + } + { + keys: [ "q", "escape" ] + action: @systemMethod:prevMenu + } + ] + } + 1: { + mci: { + ET1: { + argName: name + maxLength: 32 + validate: @systemMethod:validateNonEmpty + } + ET2: { + argName: sysop + maxLength: 32 + validate: @systemMethod:validateNonEmpty + } + ET3: { + argName: telnet + maxLength: 32 + validate: @systemMethod:validateNonEmpty + } + ET4: { + argName: www + maxLength: 32 + } + ET5: { + argName: location + maxLength: 32 + } + ET6: { + argName: software + maxLength: 32 + } + ET7: { + argName: notes + maxLength: 32 + } + TM17: { + argName: submission + items: [ "save", "cancel" ] + submit: true + } + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @method:cancelSubmit + } + ] + + submit: { + *: [ + { + value: { "submission" : 0 } + action: @method:submitBBS + } + { + value: { "submission" : 1 } + action: @method:cancelSubmit + } + ] + } + } + } + } + + /////////////////////////////////////////////////////////////////////// + // Doors Menu + /////////////////////////////////////////////////////////////////////// + doorMenu: { + desc: Doors Menu + art: DOORMNU + prompt: menuCommand + config: { + interrupt: realtime + } + submit: [ + { + value: { command: "G" } + action: @menu:logoff + } + { + value: { command: "Q" } + action: @systemMethod:prevMenu + } + // + // The system supports many ways of launching doors including + // modules for DoorParty!, BBSLink, etc. + // + // Below are some examples. See the documentation for more info. + // + { + value: { command: "ABRACADABRA" } + action: @menu:doorAbracadabraExample + } + { + value: { command: "TWBBSLINK" } + action: @menu:doorTradeWars2002BBSLinkExample + } + { + value: { command: "DP" } + action: @menu:doorPartyExample + } + { + value: { command: "CN" } + action: @menu:doorCombatNetExample + } + { + value: { command: "EXODUS" } + action: @menu:doorExodusCataclysm + } + ] + } + + // + // Local Door Example via abracadabra module + // + // This example assumes launch_door.sh (which is passed args) + // launches the door. + // + doorAbracadabraExample: { + desc: Abracadabra Example + module: abracadabra + config: { + name: Example Door + dropFileType: DORINFO + cmd: /home/enigma/DOS/scripts/launch_door.sh + args: [ + "{node}", + "{dropFile}", + "{srvPort}", + ], + nodeMax: 1 + tooManyArt: DOORMANY + io: socket + } + } + + // + // BBSLink Example (TradeWars 2000) + // + // Register @ https://bbslink.net/ + // + doorTradeWars2002BBSLinkExample: { + desc: Playing TW 2002 (BBSLink) + module: bbs_link + config: { + sysCode: XXXXXXXX + authCode: XXXXXXXX + schemeCode: XXXXXXXX + door: tw + } + } + + // + // DoorParty! Example + // + // Register @ http://throwbackbbs.com/ + // + doorPartyExample: { + desc: Using DoorParty! + module: door_party + config: { + username: XXXXXXXX + password: XXXXXXXX + bbsTag: XX + } + } + + // + // CombatNet Example + // + // Register @ http://combatnet.us/ + // + doorCombatNetExample: { + desc: Using CombatNet + module: combatnet + config: { + bbsTag: CBNxxx + password: XXXXXXXXX + } + } + + // + // Exodus Example (cataclysm) + // Register @ https://oddnetwork.org/exodus/ + // + doorExodusCataclysm: { + desc: Cataclysm + module: exodus + config: { + rejectUnauthorized: false + board: XXX + key: XXXXXXXX + door: cataclysm + } + } + + /////////////////////////////////////////////////////////////////////// + // Message Area Menu + /////////////////////////////////////////////////////////////////////// + messageArea: { + art: MSGMNU + desc: Message Area + prompt: messageMenuCommand + config: { + interrupt: realtime + } + submit: [ + { + value: { command: "P" } + action: @menu:messageAreaNewPost + } + { + value: { command: "J" } + action: @menu:messageAreaChangeCurrentConference + } + { + value: { command: "C" } + action: @menu:messageAreaChangeCurrentArea + } + { + value: { command: "L" } + action: @menu:messageAreaMessageList + } + { + value: { command: "Q" } + action: @systemMethod:prevMenu + } + { + value: { command: "G" } + action: @menu:fullLogoffSequence + } + { + value: { command: "<" } + action: @systemMethod:prevConf + } + { + value: { command: ">" } + action: @systemMethod:nextConf + } + { + value: { command: "[" } + action: @systemMethod:prevArea + } + { + value: { command: "]" } + action: @systemMethod:nextArea + } + { + value: { command: "D" } + action: @menu:messageAreaSetNewScanDate + } + { + value: { command: "S" } + action: @menu:messageSearch + } + { + value: 1 + action: @menu:messageArea + } + ] + } + + messageSearch: { + desc: Message Search + module: message_base_search + art: MSEARCH + config: { + messageListMenu: messageAreaSearchMessageList + } + form: { + 0: { + mci: { + ET1: { + focus: true + argName: searchTerms + } + BT2: { + argName: search + text: search + submit: true + } + SM3: { + argName: confTag + } + SM4: { + argName: areaTag + } + ET5: { + argName: toUserName + maxLength: @config:users.usernameMax + } + ET6: { + argName: fromUserName + maxLength: @config:users.usernameMax + } + BT7: { + argName: advancedSearch + text: advanced search + submit: true + } + } + + submit: { + *: [ + { + value: { search: null } + action: @method:search + } + { + value: { advancedSearch: null } + action: @method:search + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + messageAreaSearchMessageList: { + desc: Message Search + module: msg_list + art: MSRCHLST + config: { + menuViewPost: messageAreaViewPost + } + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: message + } + TL6: { + // theme me! + } + } + submit: { + *: [ + { + value: { message: null } + action: @method:selectMessage + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + messageAreaMyMessagesList: { + desc: Personal Messages + module: msg_list + art: MYMSGLST + config: { + menuViewPost: messageAreaViewPost + } + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: message + } + } + submit: { + *: [ + { + value: { message: null } + action: @method:selectMessage + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + messageSearchNoResults: { + desc: Message Search + art: MSRCNORES + config: { + pause: true + } + } + + messageAreaChangeCurrentConference: { + art: CCHANGE + module: msg_conf_list + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: conf + } + } + submit: { + *: [ + { + value: { conf: null } + action: @method:changeConference + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + messageAreaSetNewScanDate: { + module: set_newscan_date + desc: Message Base + art: SETMNSDATE + config: { + target: message + scanDateFormat: YYYYMMDD + } + form: { + 0: { + mci: { + ME1: { + focus: true + submit: true + argName: scanDate + maskPattern: "####/##/##" + } + SM2: { + argName: targetSelection + submit: false + } + } + submit: { + *: [ + { + value: { scanDate: null } + action: @method:scanDateSubmit + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + changeMessageConfPreArt: { + module: show_art + config: { + method: messageConf + key: confTag + pause: true + cls: true + menuFlags: [ "popParent", "noHistory" ] + } + } + + messageAreaChangeCurrentArea: { + // :TODO: rename this art to ACHANGE + art: CHANGE + module: msg_area_list + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: area + } + } + submit: { + *: [ + { + value: { area: null } + action: @method:changeArea + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + changeMessageAreaPreArt: { + module: show_art + config: { + method: messageArea + key: areaTag + pause: true + cls: true + menuFlags: [ "popParent", "noHistory" ] + } + } + + messageAreaMessageList: { + module: msg_list + art: MSGLIST + config: { + menuViewPost: messageAreaViewPost + } + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: message + } + } + submit: { + *: [ + { + value: { message: null } + action: @method:selectMessage + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + messageAreaViewPost: { + module: msg_area_view_fse + config: { + art: { + header: MSGVHDR + body: MSGBODY + footerView: MSGVFTR + help: MSGVHLP + }, + editorMode: view + editorType: area + } + form: { + 0: { + mci: { + // :TODO: ensure this block isn't even req. for theme to apply... + } + } + 1: { + mci: { + MT1: { + width: 79 + mode: preview + } + } + submit: { + *: [ + { + value: message + action: @method:editModeEscPressed + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + viewId: 1 + } + ] + } + 2: { + TLTL: { + mci: { + TL1: { width: 5 } + TL2: { width: 4 } + } + } + } + 4: { + mci: { + HM1: { + // :TODO: (#)Jump/(L)Index (msg list)/Last + items: [ "prev", "next", "reply", "quit", "help" ] + focusItemIndex: 1 + } + } + submit: { + *: [ + { + value: { 1: 0 } + action: @method:prevMessage + } + { + value: { 1: 1 } + action: @method:nextMessage + } + { + value: { 1: 2 } + action: @method:replyMessage + extraArgs: { + menu: messageAreaReplyPost + } + } + { + value: { 1: 3 } + action: @systemMethod:prevMenu + } + { + value: { 1: 4 } + action: @method:viewModeMenuHelp + } + ] + } + actionKeys: [ + { + keys: [ "p", "shift + p" ] + action: @method:prevMessage + } + { + keys: [ "n", "shift + n" ] + action: @method:nextMessage + } + { + keys: [ "r", "shift + r" ] + action: @method:replyMessage + extraArgs: { + menu: messageAreaReplyPost + } + } + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + { + keys: [ "?" ] + action: @method:viewModeMenuHelp + } + { + keys: [ "down arrow", "up arrow", "page up", "page down" ] + action: @method:movementKeyPressed + } + ] + } + } + } + + messageAreaReplyPost: { + module: msg_area_post_fse + config: { + art: { + header: MSGEHDR + body: MSGBODY + quote: MSGQUOT + footerEditor: MSGEFTR + footerEditorMenu: MSGEMFT + help: MSGEHLP + } + editorMode: edit + editorType: area + } + form: { + 0: { + mci: { + // :TODO: use appropriate system properties for max lengths + TL1: { + argName: from + } + ET2: { + argName: to + focus: true + validate: @systemMethod:validateNonEmpty + } + ET3: { + argName: subject + maxLength: 72 + submit: true + validate: @systemMethod:validateNonEmpty + } + TL4: { + // :TODO: this is for RE: line (NYI) + //width: 27 + //textOverflow: ... + } + } + submit: { + 3: [ + { + value: { subject: null } + action: @method:headerSubmit + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + 1: { + mci: { + MT1: { + width: 79 + height: 14 + argName: message + mode: edit + } + } + submit: { + *: [ { "value": "message", "action": "@method:editModeEscPressed" } ] + } + actionKeys: [ + { + keys: [ "escape" ], + viewId: 1 + } + ] + } + + 3: { + mci: { + HM1: { + items: [ "save", "discard", "quote", "help" ] + } + } + + submit: { + *: [ + { + value: { 1: 0 } + action: @method:editModeMenuSave + } + { + value: { 1: 1 } + action: @systemMethod:prevMenu + } + { + value: { 1: 2 }, + action: @method:editModeMenuQuote + } + { + value: { 1: 3 } + action: @method:editModeMenuHelp + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @method:editModeEscPressed + } + { + keys: [ "s", "shift + s" ] + action: @method:editModeMenuSave + } + { + keys: [ "d", "shift + d" ] + action: @systemMethod:prevMenu + } + { + keys: [ "q", "shift + q" ] + action: @method:editModeMenuQuote + } + { + keys: [ "?" ] + action: @method:editModeMenuHelp + } + ] + } + + // Quote builder + 5: { + mci: { + MT1: { + width: 79 + height: 7 + } + VM3: { + width: 79 + height: 4 + argName: quote + } + } + + submit: { + *: [ + { + value: { quote: null } + action: @method:appendQuoteEntry + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @method:quoteBuilderEscPressed + } + ] + } + } + } + // :TODO: messageAreaSelect (change msg areas -> call @systemMethod -> fallback to menu + messageAreaNewPost: { + desc: Posting message, + module: msg_area_post_fse + config: { + art: { + header: MSGEHDR + body: MSGBODY + footerEditor: MSGEFTR + footerEditorMenu: MSGEMFT + help: MSGEHLP + } + editorMode: edit + editorType: area + } + form: { + 0: { + mci: { + TL1: { + argName: from + } + ET2: { + argName: to + focus: true + text: All + validate: @systemMethod:validateNonEmpty + } + ET3: { + argName: subject + maxLength: 72 + submit: true + validate: @systemMethod:validateNonEmpty + // :TODO: Validate -> close/cancel if empty + } + } + submit: { + 3: [ + { + value: { subject: null } + action: @method:headerSubmit + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + + 1: { + "mci" : { + MT1: { + width: 79 + argName: message + mode: edit + } + } + + submit: { + *: [ { "value": "message", "action": "@method:editModeEscPressed" } ] + } + actionKeys: [ + { + keys: [ "escape" ] + viewId: 1 + } + ] + } + 2: { + TLTL: { + mci: { + TL1: { width: 5 } + TL2: { width: 4 } + } + } + } + 3: { + HM: { + mci: { + HM1: { + // :TODO: clear + "items" : [ "save", "discard", "help" ] + } + } + submit: { + *: [ + { + value: { 1: 0 } + action: @method:editModeMenuSave + } + { + value: { 1: 1 } + action: @systemMethod:prevMenu + } + { + value: { 1: 2 } + action: @method:editModeMenuHelp + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:editModeEscPressed + } + { + keys: [ "?" ] + action: @method:editModeMenuHelp + } + ] + // :TODO: something like the following for overriding keymap + // this should only override specified entries. others will default + /* + "keyMap" : { + "accept" : [ "return" ] + } + */ + } + } + } + } + + + // + // User to User mail aka Email Menu + // + mailMenu: { + art: MAILMNU + desc: Mail Menu + prompt: menuCommand + config: { + interrupt: realtime + } + submit: [ + { + value: { command: "C" } + action: @menu:mailMenuCreateMessage + } + { + value: { command: "I" } + action: @menu:mailMenuInbox + } + { + value: { command: "Q" } + action: @systemMethod:prevMenu + } + { + value: { command: "G" } + action: @menu:fullLogoffSequence + } + { + value: 1 + action: @menu:mailMenu + } + ] + } + + mailMenuCreateMessage: { + desc: Mailing Someone + module: msg_area_post_fse + config: { + art: { + header: MSGEHDR + body: MSGBODY + footerEditor: MSGEFTR + footerEditorMenu: MSGEMFT + help: MSGEHLP + }, + editorMode: edit + editorType: email + messageAreaTag: private_mail + } + form: { + 0: { + mci: { + TL1: { + argName: from + } + ET2: { + argName: to + focus: true + validate: @systemMethod:validateGeneralMailAddressedTo + } + ET3: { + argName: subject + maxLength: 72 + submit: true + validate: @systemMethod:validateMessageSubject + } + } + submit: { + 3: [ + { + value: { subject: null } + action: @method:headerSubmit + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + 1: { + mci: { + MT1: { + width: 79 + argName: message + mode: edit + } + } + + submit: { + *: [ { value: "message", action: "@method:editModeEscPressed" } ] + } + actionKeys: [ + { + keys: [ "escape" ] + viewId: 1 + } + ] + }, + 2: { + TLTL: { + mci: { + TL1: { + width: 5 + } + TL2: { + width: 4 + } + } + } + } + 3: { + HM: { + mci: { + HM1: { + // :TODO: clear + items: [ "save", "discard", "help" ] + } + } + submit: { + *: [ + { + value: { 1: 0 } + action: @method:editModeMenuSave + } + { + value: { 1: 1 } + action: @systemMethod:prevMenu + } + { + value: { 1: 2 } + action: @method:editModeMenuHelp + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:editModeEscPressed + } + { + keys: [ "?" ] + action: @method:editModeMenuHelp + } + ] + } + } + } + } + + mailMenuInbox: { + module: msg_list + art: PRVMSGLIST + config: { + menuViewPost: messageAreaViewPost + messageAreaTag: private_mail + } + form: { + 0: { // main list + mci: { + VM1: { + focus: true + submit: true + argName: message + } + } + submit: { + *: [ + { + value: { message: null } + action: @method:selectMessage + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + { + keys: [ "delete", "d", "shift + d" ] + action: @method:deleteSelected + } + ] + } + 1: { // delete prompt form + submit: { + *: [ + { + value: { promptValue: 0 } + action: @method:deleteMessageYes + } + { + value: { promptValue: 1 } + action: @method:deleteMessageNo + } + ] + } + } + } + } + + //////////////////////////////////////////////////////////////////////// + // File Base + //////////////////////////////////////////////////////////////////////// + + fileBase: { + desc: File Base + art: FMENU + prompt: fileMenuCommand + config: { + interrupt: realtime + } + submit: [ + { + value: { menuOption: "L" } + action: @menu:fileBaseListEntries + } + { + value: { menuOption: "B" } + action: @menu:fileBaseBrowseByAreaSelect + } + { + value: { menuOption: "F" } + action: @menu:fileAreaFilterEditor + } + { + value: { menuOption: "Q" } + action: @systemMethod:prevMenu + } + { + value: { menuOption: "G" } + action: @menu:fullLogoffSequence + } + { + value: { menuOption: "D" } + action: @menu:fileBaseDownloadManager + } + { + value: { menuOption: "W" } + action: @menu:fileBaseWebDownloadManager + } + { + value: { menuOption: "U" } + action: @menu:fileBaseUploadFiles + } + { + value: { menuOption: "S" } + action: @menu:fileBaseSearch + } + { + value: { menuOption: "P" } + action: @menu:fileBaseSetNewScanDate + } + { + value: { menuOption: "E" } + action: @menu:fileBaseExportListFilter + } + ] + } + + fileBaseExportListFilter: { + module: file_base_search + art: FBLISTEXPSEARCH + config: { + fileBaseListEntriesMenu: fileBaseExportList + } + form: { + 0: { + mci: { + ET1: { + focus: true + argName: searchTerms + } + BT2: { + argName: search + text: search + submit: true + } + ET3: { + maxLength: 64 + argName: tags + } + SM4: { + maxLength: 64 + argName: areaIndex + } + SM5: { + items: [ + "upload date", + "uploaded by", + "downloads", + "rating", + "estimated year", + "size", + "filename" + ] + argName: sortByIndex + } + SM6: { + items: [ + "decending", + "ascending" + ] + argName: orderByIndex + } + BT7: { + argName: advancedSearch + text: advanced search + submit: true + } + } + + submit: { + *: [ + { + value: { search: null } + action: @method:search + } + { + value: { advancedSearch: null } + action: @method:search + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseExportList: { + module: file_base_user_list_export + art: FBLISTEXP + config: { + pause: true + templates: { + entry: file_list_entry.asc + } + } + form: { + 0: { + mci: { + TL1: { } + TL2: { } + } + } + } + } + + fileBaseExportListNoResults: { + desc: Browsing Files + art: FBNORES + config: { + pause: true + menuFlags: [ "noHistory", "popParent" ] + } + } + + fileBaseSetNewScanDate: { + module: set_newscan_date + desc: File Base + art: SETFNSDATE + config: { + target: file + scanDateFormat: YYYYMMDD + } + form: { + 0: { + mci: { + ME1: { + focus: true + submit: true + argName: scanDate + maskPattern: "####/##/##" + } + } + submit: { + *: [ + { + value: { scanDate: null } + action: @method:scanDateSubmit + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseListEntries: { + module: file_area_list + desc: Browsing Files + config: { + art: { + browse: FBRWSE + details: FDETAIL + detailsGeneral: FDETGEN + detailsNfo: FDETNFO + detailsFileList: FDETLST + help: FBHELP + } + } + form: { + 0: { + mci: { + MT1: { + mode: preview + } + + HM2: { + focus: true + submit: true + argName: navSelect + items: [ + "prev", "next", "details", "toggle queue", "rate", "change filter", "help", "quit" + ] + focusItemIndex: 1 + } + } + + submit: { + *: [ + { + value: { navSelect: 0 } + action: @method:prevFile + } + { + value: { navSelect: 1 } + action: @method:nextFile + } + { + value: { navSelect: 2 } + action: @method:viewDetails + } + { + value: { navSelect: 3 } + action: @method:toggleQueue + } + { + value: { navSelect: 4 } + action: @menu:fileBaseGetRatingForSelectedEntry + } + { + value: { navSelect: 5 } + action: @menu:fileAreaFilterEditor + } + { + value: { navSelect: 6 } + action: @method:displayHelp + } + { + value: { navSelect: 7 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: [ + { + keys: [ "w", "shift + w" ] + action: @method:showWebDownloadLink + } + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + { + keys: [ "t", "shift + t" ] + action: @method:toggleQueue + } + { + keys: [ "f", "shift + f" ] + action: @menu:fileAreaFilterEditor + } + { + keys: [ "v", "shift + v" ] + action: @method:viewDetails + } + { + keys: [ "r", "shift + r" ] + action: @menu:fileBaseGetRatingForSelectedEntry + } + { + keys: [ "?" ] + action: @method:displayHelp + } + ] + } + + 1: { + mci: { + HM1: { + focus: true + submit: true + argName: navSelect + items: [ + "general", "nfo/readme", "file listing" + ] + } + } + + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @method:detailsQuit + } + ] + } + + 2: { + // details - general + mci: {} + } + + 3: { + // details - nfo/readme + mci: { + MT1: { + mode: preview + } + } + } + + 4: { + // details - file listing + mci: { + VM1: { + + } + } + } + } + } + + fileBaseBrowseByAreaSelect: { + desc: Browsing File Areas + module: file_base_area_select + art: FAREASEL + form: { + 0: { + mci: { + VM1: { + focus: true + argName: areaTag + } + } + + submit: { + *: [ + { + value: { areaTag: null } + action: @method:selectArea + } + ] + } + + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseGetRatingForSelectedEntry: { + desc: Rating a File + prompt: fileBaseRateEntryPrompt + config: { + cls: true + } + submit: [ + // :TODO: handle esc/q + { + // pass data back to caller + value: { rating: null } + action: @systemMethod:prevMenu + } + ] + } + + fileBaseListEntriesNoResults: { + desc: Browsing Files + art: FBNORES + config: { + pause: true + menuFlags: [ "noHistory", "popParent" ] + } + } + + fileBaseSearch: { + module: file_base_search + desc: Searching Files + art: FSEARCH + form: { + 0: { + mci: { + ET1: { + focus: true + argName: searchTerms + } + BT2: { + argName: search + text: search + submit: true + } + ET3: { + maxLength: 64 + argName: tags + } + SM4: { + maxLength: 64 + argName: areaIndex + } + SM5: { + items: [ + "upload date", + "uploaded by", + "downloads", + "rating", + "estimated year", + "size", + "filename", + ] + argName: sortByIndex + } + SM6: { + items: [ + "decending", + "ascending" + ] + argName: orderByIndex + } + BT7: { + argName: advancedSearch + text: advanced search + submit: true + } + } + + submit: { + *: [ + { + value: { search: null } + action: @method:search + } + { + value: { advancedSearch: null } + action: @method:search + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileAreaFilterEditor: { + desc: File Filter Editor + module: file_area_filter_edit + art: FFILEDT + form: { + 0: { + mci: { + ET1: { + argName: searchTerms + } + ET2: { + maxLength: 64 + argName: tags + } + SM3: { + maxLength: 64 + argName: areaIndex + } + SM4: { + items: [ + "upload date", + "uploaded by", + "downloads", + "rating", + "estimated year", + "size", + ] + argName: sortByIndex + } + SM5: { + items: [ + "decending", + "ascending" + ] + argName: orderByIndex + } + ET6: { + maxLength: 64 + argName: name + validate: @systemMethod:validateNonEmpty + } + HM7: { + focus: true + items: [ + "prev", "next", "make active", "save", "new", "delete" + ] + argName: navSelect + focusItemIndex: 1 + } + } + + submit: { + *: [ + { + value: { navSelect: 0 } + action: @method:prevFilter + } + { + value: { navSelect: 1 } + action: @method:nextFilter + } + { + value: { navSelect: 2 } + action: @method:makeFilterActive + } + { + value: { navSelect: 3 } + action: @method:saveFilter + } + { + value: { navSelect: 4 } + action: @method:newFilter + } + { + value: { navSelect: 5 } + action: @method:deleteFilter + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseDownloadManager: { + desc: Download Manager + module: file_base_download_manager + config: { + art: { + queueManager: FDLMGR + /* + NYI + details: FDLDET + */ + } + emptyQueueMenu: fileBaseDownloadManagerEmptyQueue + } + form: { + 0: { + mci: { + VM1: { + argName: queueItem + } + HM2: { + focus: true + items: [ "download all", "quit" ] + argName: navSelect + } + } + + submit: { + *: [ + { + value: { navSelect: 0 } + action: @method:downloadAll + } + { + value: { navSelect: 1 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: [ + { + keys: [ "a", "shift + a" ] + action: @method:downloadAll + } + { + keys: [ "delete", "r", "shift + r" ] + action: @method:removeItem + } + { + keys: [ "c", "shift + c" ] + action: @method:clearQueue + } + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseWebDownloadManager: { + desc: Web D/L Manager + module: file_base_web_download_manager + config: { + art: { + queueManager: FWDLMGR + batchList: BATDLINF + } + emptyQueueMenu: fileBaseDownloadManagerEmptyQueue + } + form: { + 0: { + mci: { + VM1: { + argName: queueItem + } + HM2: { + focus: true + items: [ "get batch link", "quit", "help" ] + argName: navSelect + } + } + + submit: { + *: [ + { + value: { navSelect: 0 } + action: @method:getBatchLink + } + { + value: { navSelect: 1 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: [ + { + keys: [ "b", "shift + b" ] + action: @method:getBatchLink + } + { + keys: [ "delete", "r", "shift + r" ] + action: @method:removeItem + } + { + keys: [ "c", "shift + c" ] + action: @method:clearQueue + } + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseDownloadManagerEmptyQueue: { + desc: Empty Download Queue + art: FEMPTYQ + config: { + pause: true + menuFlags: [ "noHistory", "popParent" ] + } + } + + fileTransferProtocolSelection: { + desc: Protocol selection + module: file_transfer_protocol_select + art: FPROSEL + form: { + 0: { + mci: { + VM1: { + focus: true + argName: protocol + } + } + + submit: { + *: [ + { + value: { protocol: null } + action: @method:selectProtocol + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseUploadFiles: { + desc: Uploading + module: upload + config: { + interrupt: never + art: { + options: ULOPTS + fileDetails: ULDETAIL + processing: ULCHECK + dupes: ULDUPES + } + } + + form: { + // options + 0: { + mci: { + SM1: { + argName: areaSelect + focus: true + } + TM2: { + argName: uploadType + items: [ "blind", "supply filename" ] + } + ET3: { + argName: fileName + maxLength: 255 + validate: @method:validateNonBlindFileName + } + HM4: { + argName: navSelect + items: [ "continue", "cancel" ] + submit: true + } + } + + submit: { + *: [ + { + value: { navSelect: 0 } + action: @method:optionsNavContinue + } + { + value: { navSelect: 1 } + action: @systemMethod:prevMenu + } + ] + } + + "actionKeys" : [ + { + "keys" : [ "escape" ], + action: @systemMethod:prevMenu + } + ] + } + + 1: { + mci: { } + } + + // file details entry + 2: { + mci: { + MT1: { + argName: shortDesc + tabSwitchesView: true + focus: true + } + + ET2: { + argName: tags + } + + ME3: { + argName: estYear + maskPattern: "####" + } + + BT4: { + argName: continue + text: continue + submit: true + } + } + + submit: { + *: [ + { + value: { continue: null } + action: @method:fileDetailsContinue + } + ] + } + } + + // dupes + 3: { + mci: { + VM1: { + /* + Use 'dupeInfoFormat' to custom format: + + areaDesc + areaName + areaTag + desc + descLong + fileId + fileName + fileSha256 + storageTag + uploadTimestamp + + */ + + mode: preview + } + } + } + } + } + + fileBaseNoUploadAreasAvail: { + desc: File Base + art: ULNOAREA + config: { + pause: true + menuFlags: [ "noHistory", "popParent" ] + } + } + + sendFilesToUser: { + desc: Downloading + module: file_transfer + config: { + // defaults - generally use extraArgs + protocol: zmodem8kSexyz + direction: send + } + } + + recvFilesFromUser: { + desc: Uploading + module: file_transfer + config: { + // defaults - generally use extraArgs + protocol: zmodem8kSexyz + direction: recv + } + } + + + //////////////////////////////////////////////////////////////////////// + // Required entries + //////////////////////////////////////////////////////////////////////// + idleLogoff: { + art: IDLELOG + next: @systemMethod:logoff + } + //////////////////////////////////////////////////////////////////////// + // Demo Section + // :TODO: This entire section needs updated!!! + //////////////////////////////////////////////////////////////////////// + "demoMain" : { + "art" : "demo_selection_vm.ans", + "form" : { + "0" : { + "VM" : { + "mci" : { + "VM1" : { + "items" : [ + "Single Line Text Editing Views", + "Spinner & Toggle Views", + "Mask Edit Views", + "Multi Line Text Editor", + "Vertical Menu Views", + "Horizontal Menu Views", + "Art Display", + "Full Screen Editor" + ], + "height" : 10, + "itemSpacing" : 1, + "justify" : "center", + "focusTextStyle" : "small i" + } + }, + "submit" : { + "*" : [ + { + "value" : { "1" : 0 }, + "action" : "@menu:demoEditTextView" + }, + { + "value" : { "1" : 1 }, + "action" : "@menu:demoSpinAndToggleView" + }, + { + "value" : { "1" : 2 }, + "action" : "@menu:demoMaskEditView" + }, + { + "value" : { "1" : 3 }, + "action" : "@menu:demoMultiLineEditTextView" + }, + { + "value" : { "1" : 4 }, + "action" : "@menu:demoVerticalMenuView" + }, + { + "value" : { "1" : 5 }, + "action" : "@menu:demoHorizontalMenuView" + }, + { + "value" : { "1" : 6 }, + "action" : "@menu:demoArtDisplay" + }, + { + "value" : { "1" : 7 }, + "action" : "@menu:demoFullScreenEditor" + } + ] + } + } + } + } + }, + "demoEditTextView" : { + "art" : "demo_edit_text_view1.ans", + "form" : { + "0" : { + "BTETETETET" : { + "mci" : { + "ET1" : { + "width" : 20, + "maxLength" : 20 + }, + "ET2" : { + "width" : 20, + "maxLength" : 40, + "textOverflow" : "..." + }, + "ET3" : { + "width" : 20, + "fillChar" : "-", + "styleSGR1" : "|00|36", + "maxLength" : 20 + }, + "ET4" : { + "width" : 20, + "maxLength" : 20, + "password" : true + }, + "BT5" : { + "width" : 8, + "text" : "< Back" + } + }, + "submit" : { + "*" : [ + { + "value" : 5, + "action" : "@menu:demoMain" + } + ] + }, + "actionKeys" : [ + { + "keys" : [ "escape" ], + "viewId" : 5 + } + ] + } + } + } + }, + "demoSpinAndToggleView" : { + "art" : "demo_spin_and_toggle.ans", + "form" : { + "0" : { + "BTSMSMTM" : { + "mci" : { + "SM1" : { + "items" : [ "Henry Morgan", "François l'Ollonais", "Roche Braziliano", "Black Bart", "Blackbeard" ] + }, + "SM2" : { + "items" : [ "Razor 1911", "DrinkOrDie", "TRSI" ] + }, + "TM3" : { + "items" : [ "Yarly", "Nowaii" ], + "styleSGR1" : "|00|30|01", + "hotKeys" : { "Y" : 0, "N" : 1 } + }, + "BT8" : { + "text" : "< Back" + } + }, + "submit" : { + "*" : [ + { + "value" : 8, + "action" : "@menu:demoMain" + } + ] + }, + "actionKeys" : [ + { + "keys" : [ "escape" ], + "viewId" : 8 + } + ] + } + } + } + }, + "demoMaskEditView" : { + "art" : "demo_mask_edit_text_view1.ans", + "form" : { + "0" : { + "BTMEME" : { + "mci" : { + "ME1" : { + "maskPattern" : "##/##/##", + "styleSGR1" : "|00|30|01", + //"styleSGR2" : "|00|45|01", + "styleSGR3" : "|00|30|35", + "fillChar" : "#" + }, + "BT5" : { + "text" : "< Back" + } + }, + "submit" : { + "*" : [ + { + "value" : 5, + "action" : "@menu:demoMain" + } + ] + }, + "actionKeys" : [ + { + "keys" : [ "escape" ], + "viewId" : 5 + } + ] + } + } + } + }, + "demoMultiLineEditTextView" : { + "art" : "demo_multi_line_edit_text_view1.ans", + "form" : { + "0" : { + "BTMT" : { + "mci" : { + "MT1" : { + "width" : 70, + "height" : 17, + //"text" : "@art:demo_multi_line_edit_text_view_text.txt", + // "text" : "@systemMethod:textFromFile" + text: "Hints:\n\t* Insert / CTRL-V toggles overtype mode\n\t* CTRL-Y deletes the current line\n\t* Try Page Up / Page Down\n\t* Home goes to the start of line text\n\t* End goes to the end of a line\n\n\nTab handling:\n-------------------------------------------------\n\tA\tB\tC\tD\tE\tF\nA\tB\tC\tD\tE\tF\tG\tH\n\tA\tB\tC\tD\tE\tF\nA\tB\tC\tD\tE\tF\tG\tH\nA0\tBB\t1\tCCC\t2\tDDD\t3EEEE\nW\t\tX\t\tY\t\tZ\n\nAn excerpt from A Clockwork Orange:\n\"What sloochatted then, of course, was that my cellmates woke up and started joining in, tolchocking a bit wild in the near-dark, and the shoom seemed to wake up the whole tier, so that you could slooshy a lot of creeching and banging about with tin mugs on the wall, as though all the plennies in all the cells thought a big break was about to commence, O my brothers.\n", + "focus" : true + }, + "BT5" : { + "text" : "< Back" + } + }, + "submit" : { + "*" : [ + { + "value" : 5, + "action" : "@menu:demoMain" + } + ] + }, + "actionKeys" : [ + { + "keys" : [ "escape" ], + "viewId" : 5 + } + ] + } + } + } + }, + "demoHorizontalMenuView" : { + "art" : "demo_horizontal_menu_view1.ans", + "form" : { + "0" : { + "BTHMHM" : { + "mci" : { + "HM1" : { + "items" : [ "One", "Two", "Three" ], + "hotKeys" : { "1" : 0, "2" : 1, "3" : 2 } + }, + "HM2" : { + "items" : [ "Uno", "Dos", "Tres" ], + "hotKeys" : { "U" : 0, "D" : 1, "T" : 2 } + }, + "BT5" : { + "text" : "< Back" + } + }, + "submit" : { + "*" : [ + { + "value" : 5, + "action" : "@menu:demoMain" + } + ] + }, + "actionKeys" : [ + { + "keys" : [ "escape" ], + "viewId" : 5 + } + ] + } + } + } + }, + "demoVerticalMenuView" : { + "art" : "demo_vertical_menu_view1.ans", + "form" : { + "0" : { + "BTVM" : { + "mci" : { + "VM1" : { + "items" : [ + "|33Oblivion/2", + "|33iNiQUiTY", + "|33ViSiON/X" + ], + "focusItems" : [ + "|33Oblivion|01/|00|332", + "|01|33i|00|33N|01i|00|33QU|01i|00|33TY", + "|33ViSiON/X" + ] + // + // :TODO: how to do the following: + // 1) Supply a view a string for a standard vs focused item + // "items" : [...], "focusItems" : [ ... ] ? + // "draw" : "@method:drawItemX", then items: [...] + }, + "BT5" : { + "text" : "< Back" + } + }, + "submit" : { + "*" : [ + { + "value" : 5, + "action" : "@menu:demoMain" + } + ] + }, + "actionKeys" : [ + { + "keys" : [ "escape" ], + "viewId" : 5 + } + ] + } + } + } + + }, + "demoArtDisplay" : { + "art" : "demo_selection_vm.ans", + "form" : { + "0" : { + "VM" : { + "mci" : { + "VM1" : { + "items" : [ + "Defaults - DOS ANSI", + "bw_mindgames.ans - DOS", + "test.ans - DOS", + "Defaults - Amiga", + "Pause at Term Height" + ], + // :TODO: justify not working?? + "focusTextStyle" : "small i" + } + }, + "submit" : { + "*" : [ + { + "value" : { "1" : 0 }, + "action" : "@menu:demoDefaultsDosAnsi" + }, + { + "value" : { "1" : 1 }, + "action" : "@menu:demoDefaultsDosAnsi_bw_mindgames" + }, + { + "value" : { "1" : 2 }, + "action" : "@menu:demoDefaultsDosAnsi_test" + } + ] + } + } + } + } + }, + "demoDefaultsDosAnsi" : { + "art" : "DM-ENIG2.ANS" + }, + "demoDefaultsDosAnsi_bw_mindgames" : { + "art" : "bw_mindgames.ans" + }, + "demoDefaultsDosAnsi_test" : { + "art" : "test.ans" + }, + "demoFullScreenEditor" : { + "module" : "fse", + "config" : { + "editorType" : "netMail", + "art" : { + "header" : "demo_fse_netmail_header.ans", + "body" : "demo_fse_netmail_body.ans", + "footerEditor" : "demo_fse_netmail_footer_edit.ans", + "footerEditorMenu" : "demo_fse_netmail_footer_edit_menu.ans", + "footerView" : "demo_fse_netmail_footer_view.ans", + "help" : "demo_fse_netmail_help.ans" + } + }, + "form" : { + "0" : { + "ETETET" : { + "mci" : { + "ET1" : { + // :TODO: from/to may be set by args + // :TODO: focus may change dep on view vs edit + "width" : 36, + "focus" : true, + "argName" : "to" + }, + "ET2" : { + "width" : 36, + "argName" : "from" + }, + "ET3" : { + "width" : 65, + "maxLength" : 72, + "submit" : [ "enter" ], + "argName" : "subject" + } + }, + "submit" : { + "3" : [ + { + "value" : { "subject" : null }, + "action" : "@method:headerSubmit" + } + ] + } + } + }, + "1" : { + "MT" : { + "mci" : { + "MT1" : { + "width" : 79, + "height" : 17, + "text" : "", // :TODO: should not be req. + "argName" : "message" + } + }, + "submit" : { + "*" : [ + { + "value" : "message", + "action" : "@method:editModeEscPressed" + } + ] + }, + "actionKeys" : [ + { + "keys" : [ "escape" ], + "viewId" : 1 + } + ] + } + }, + "2" : { + "TLTL" : { + "mci" : { + "TL1" : { + "width" : 5 + }, + "TL2" : { + "width" : 4 + } + } + } + }, + "3" : { + "HM" : { + "mci" : { + "HM1" : { + // :TODO: Continue, Save, Discard, Clear, Quote, Help + "items" : [ "Save", "Discard", "Quote", "Help" ] + } + }, + "submit" : { + "*" : [ + { + "value" : { "1" : 0 }, + "action" : "@method:editModeMenuSave" + }, + { + "value" : { "1" : 1 }, + "action" : "@menu:demoMain" + }, + { + "value" : { "1" : 2 }, + "action" : "@method:editModeMenuQuote" + }, + { + "value" : { "1" : 3 }, + "action" : "@method:editModeMenuHelp" + }, + { + "value" : 1, + "action" : "@method:editModeEscPressed" + } + ] + }, + "actionKeys" : [ // :TODO: Need better name + { + "keys" : [ "escape" ], + "action" : "@method:editModeEscPressed" + } + ] + } + } + } + } + } +} From 2c3219fc67279d0aa16711138726d81d7559e823 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 26 May 2019 23:44:38 +0100 Subject: [PATCH 045/140] Remove heartbeat when client exits mrc --- core/mrc.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/mrc.js b/core/mrc.js index 54aa70f7..d696c9e7 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -112,6 +112,7 @@ exports.getModule = class mrcModule extends MenuModule { quit : (formData, extraArgs, cb) => { this.sendServerMessage('LOGOFF'); + clearInterval(this.heartbeat); this.state.socket.destroy(); return this.prevMenu(cb); } @@ -147,7 +148,7 @@ exports.getModule = class mrcModule extends MenuModule { self.clientConnect(); // send register to central MRC and get stats every 60s - setInterval(function () { + self.heartbeat = setInterval(function () { self.sendHeartbeat(); self.sendServerMessage('STATS'); }, 60000); From 072246ae1b125ba6b4c5d1eebc7b07eca72c759b Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 26 May 2019 23:45:00 +0100 Subject: [PATCH 046/140] Main menu ansi to include MRC --- art/themes/luciano_blocktronics/MMENU.ANS | Bin 3610 -> 3653 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/art/themes/luciano_blocktronics/MMENU.ANS b/art/themes/luciano_blocktronics/MMENU.ANS index bd8a483f56406abf49f17d727730aab0a4a157ff..9db39ca9e7afc13910caf7d3549d931dc59f72fd 100644 GIT binary patch delta 63 zcmbOwb5v%76|b(5fwOe9VXi`MQL=QjL9T*yw6R%kZfQl delta 20 ccmX>qGfQTJ6)%&C<>YkU9gOQIEAXiT07rlZQvd(} From ca8add36b52587d67e4fa55b6dd53821374d3af3 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Mon, 27 May 2019 15:50:07 +0100 Subject: [PATCH 047/140] reenable disabled jobs --- core/config.js | 56 +++++++++++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/core/config.js b/core/config.js index ea309fc6..37dfbd90 100644 --- a/core/config.js +++ b/core/config.js @@ -984,38 +984,38 @@ function getDefaultConfig() { eventScheduler : { events : { - // dailyMaintenance : { - // schedule : 'at 11:59pm', - // action : '@method:core/misc_scheduled_events.js:dailyMaintenanceScheduledEvent', - // }, - // trimMessageAreas : { - // // may optionally use [or ]@watch:/path/to/file - // schedule : 'every 24 hours', + dailyMaintenance : { + schedule : 'at 11:59pm', + action : '@method:core/misc_scheduled_events.js:dailyMaintenanceScheduledEvent', + }, + trimMessageAreas : { + // may optionally use [or ]@watch:/path/to/file + schedule : 'every 24 hours', - // // action: - // // - @method:path/to/module.js:theMethodName - // // (path is relative to ENiGMA base dir) - // // - // // - @execute:/path/to/something/executable.sh - // // - // action : '@method:core/message_area.js:trimMessageAreasScheduledEvent', - // }, + // action: + // - @method:path/to/module.js:theMethodName + // (path is relative to ENiGMA base dir) + // + // - @execute:/path/to/something/executable.sh + // + action : '@method:core/message_area.js:trimMessageAreasScheduledEvent', + }, - // nntpMaintenance : { - // schedule : 'every 12 hours', // should generally be < trimMessageAreas interval - // action : '@method:core/servers/content/nntp.js:performMaintenanceTask', - // }, + nntpMaintenance : { + schedule : 'every 12 hours', // should generally be < trimMessageAreas interval + action : '@method:core/servers/content/nntp.js:performMaintenanceTask', + }, - // updateFileAreaStats : { - // schedule : 'every 1 hours', - // action : '@method:core/file_base_area.js:updateAreaStatsScheduledEvent', - // }, + updateFileAreaStats : { + schedule : 'every 1 hours', + action : '@method:core/file_base_area.js:updateAreaStatsScheduledEvent', + }, - // forgotPasswordMaintenance : { - // schedule : 'every 24 hours', - // action : '@method:core/web_password_reset.js:performMaintenanceTask', - // args : [ '24 hours' ] // items older than this will be removed - // }, + forgotPasswordMaintenance : { + schedule : 'every 24 hours', + action : '@method:core/web_password_reset.js:performMaintenanceTask', + args : [ '24 hours' ] // items older than this will be removed + }, // // Enable the following entry in your config.hjson to periodically create/update From b4e37118dd1b3849870cfe9e94deab1189ec355d Mon Sep 17 00:00:00 2001 From: David Stephens Date: Tue, 28 May 2019 23:45:22 +0100 Subject: [PATCH 048/140] Fixed user achievements ansi --- .../{USERACHIEV.ANS => USERACHIEV.ans} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename art/themes/luciano_blocktronics/{USERACHIEV.ANS => USERACHIEV.ans} (100%) diff --git a/art/themes/luciano_blocktronics/USERACHIEV.ANS b/art/themes/luciano_blocktronics/USERACHIEV.ans similarity index 100% rename from art/themes/luciano_blocktronics/USERACHIEV.ANS rename to art/themes/luciano_blocktronics/USERACHIEV.ans From 29e0c8f7905a0ba7746c71405b856d19ff817ca3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 29 May 2019 22:03:29 -0600 Subject: [PATCH 049/140] Ability to show secret or backup codes --- core/user_2fa_otp_config.js | 83 +++++++++++++++++++++++++++---------- 1 file changed, 61 insertions(+), 22 deletions(-) diff --git a/core/user_2fa_otp_config.js b/core/user_2fa_otp_config.js index a4bbb3df..a47e0e6d 100644 --- a/core/user_2fa_otp_config.js +++ b/core/user_2fa_otp_config.js @@ -34,6 +34,11 @@ const MciViewIds = { customRangeStart : 10, // 10+ = customs }; +const DefaultMsg = { + otpNotEnabled : '2FA/OTP is not currently enabled for this account.', + noBackupCodes : 'No backup codes remaining or set.', +}; + exports.getModule = class User2FA_OTPConfigModule extends MenuModule { constructor(options) { super(options); @@ -42,6 +47,12 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { this.menuMethods = { showQRCode : (formData, extraArgs, cb) => { return this.showQRCode(cb); + }, + showSecret : (formData, extraArgs, cb) => { + return this.showSecret(cb); + }, + showBackupCodes : (formData, extraArgs, cb) => { + return this.showBackupCodes(cb); } }; } @@ -101,35 +112,63 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { }); } - showQRCode(cb) { - const otp = otpFromType(this.client.user.getProperty(UserProps.AuthFactor2OTP)); - let qrCodeAscii = ''; - if(!otp) { - qrCodeAscii = '2FA/OTP is not currently enabled for this account'; - } - - const qrOptions = { - username : this.client.user.username, - qrType : 'ascii', - }; - qrCodeAscii = createQRCode( - otp, - qrOptions, - this.client.user.getProperty(UserProps.AuthFactor2OTPSecret) - ).replace(/\n/g, '\r\n'); - + displayDetails(details, cb) { const modOpts = { extraArgs : { - artData : iconv.encode(`${qrCodeAscii}\r\n`, 'cp437'), + artData : iconv.encode(`${details}\r\n`, 'cp437'), } }; this.gotoMenu( - this.menuConfig.config.mainMenuUser2FAOTP_ShowQR || 'mainMenuUser2FAOTP_ShowQR', + this.menuConfig.config.user2FAOTP_ShowDetails || 'user2FAOTP_ShowDetails', modOpts, cb ); } + showQRCode(cb) { + const otp = otpFromType(this.client.user.getProperty(UserProps.AuthFactor2OTP)); + + let qrCode; + if(!otp) { + qrCode = this.config.otpNotEnabled || DefaultMsg.otpNotEnabled; + } else { + const qrOptions = { + username : this.client.user.username, + qrType : 'ascii', + }; + qrCode = createQRCode( + otp, + qrOptions, + this.client.user.getProperty(UserProps.AuthFactor2OTPSecret) + ).replace(/\n/g, '\r\n'); + } + + return this.displayDetails(qrCode, cb); + } + + showSecret(cb) { + const info = + this.client.user.getProperty(UserProps.AuthFactor2OTPSecret) || + this.config.otpNotEnabled || DefaultMsg.otpNotEnabled; + return this.displayDetails(info, cb); + } + + showBackupCodes(cb) { + let info; + const noBackupCodes = this.config.noBackupCodes || DefaultMsg.noBackupCodes; + if(!this.isOTPEnabledForUser()) { + info = this.config.otpNotEnabled || DefaultMsg.otpNotEnabled; + } else { + try { + info = JSON.parse(this.client.user.getProperty(UserProps.AuthFactor2OTPBackupCodes) || '[]').join(', '); + info = info || noBackupCodes; + } catch(e) { + info = noBackupCodes; + } + } + return this.displayDetails(info, cb); + } + isOTPEnabledForUser() { return this.typeSelectionIndexFromUserOTPType(-1) != -1; } @@ -140,8 +179,8 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { enableToggleUpdate(idx) { const key = { - 0 : '2faDisabled', - 1 : '2faEnabled', + 0 : 'disabled', + 1 : 'enabled', }[idx]; this.updateCustomViewTextsWithFilter('menu', MciViewIds.customRangeStart, { infoText : this.getInfoText(key) } ); } @@ -164,7 +203,7 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { } typeSelectionUpdate(idx) { - const key = '2faType_' + this.otpTypeFromTypeSelectionIndex(idx); + const key = this.otpTypeFromTypeSelectionIndex(idx); this.updateCustomViewTextsWithFilter('menu', MciViewIds.customRangeStart, { infoText : this.getInfoText(key) } ); } }; From 593bf67b45a7bf45bd2db19b850d3e69a79a17ef Mon Sep 17 00:00:00 2001 From: David Stephens Date: Fri, 31 May 2019 01:16:32 +0100 Subject: [PATCH 050/140] Stop passing sockets all over the place. General tidy up and refactor. Add reconnection to MRC logic. --- core/servers/chat/mrc_multiplexer.js | 170 ++++++++++++++++++--------- 1 file changed, 113 insertions(+), 57 deletions(-) diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js index 7eaac22f..e0ef4a54 100644 --- a/core/servers/chat/mrc_multiplexer.js +++ b/core/servers/chat/mrc_multiplexer.js @@ -16,7 +16,8 @@ const os = require('os'); // MRC -const PROTOCOL_VERSION = '1.2.9'; +const protocolVersion = '1.2.9'; +const lineDelimiter = new RegExp('\r\n|\r|\n'); const ModuleInfo = exports.moduleInfo = { name : 'MRC', @@ -27,62 +28,130 @@ const ModuleInfo = exports.moduleInfo = { }; const connectedSockets = new Set(); -let mrcCentralConnection = ''; exports.getModule = class MrcModule extends ServerModule { constructor() { super(); - this.log = Log.child( { server : 'MRC' } ); + + this.log = Log.child( { server : 'MRC' } ); + this.config = Config(); + this.mrcConnectOpts = { + host : this.config.chatServers.mrc.serverHostname || 'mrc.bottomlessabyss.net', + port : this.config.chatServers.mrc.serverPort || 5000, + retryDelay : this.config.chatServers.mrc.retryDelay || 10000 + }; } - createServer(cb) { - if (!this.enabled) { - return cb(null); - } - - const self = this; - + _connectionHandler() { const config = Config(); const boardName = config.general.prettyBoardName || config.general.boardName; const enigmaVersion = 'ENiGMA-BBS_' + require('../../../package.json').version; - const mrcConnectOpts = { - host : config.chatServers.mrc.serverHostname || 'mrc.bottomlessabyss.net', - port : config.chatServers.mrc.serverPort || 5000 - }; - - const handshake = `${boardName}~${enigmaVersion}/${os.platform()}-${os.arch()}/${PROTOCOL_VERSION}`; + const handshake = `${boardName}~${enigmaVersion}/${os.platform()}-${os.arch()}/${protocolVersion}`; this.log.debug({ handshake : handshake }, 'Handshaking with MRC server'); + this.mrcClient.write(handshake); + this.log.info(this.mrcConnectOpts, 'Connected to MRC server'); + } + + createServer(cb) { + + if (!this.enabled) { + return cb(null); + } + + this.connectToMrc(); + this.createLocalListener(); + + return cb(null); + } + + listen(cb) { + if (!this.enabled) { + return cb(null); + } + + const config = Config(); + + const port = parseInt(config.chatServers.mrc.multiplexerPort); + if(isNaN(port)) { + this.log.warn( { port : config.chatServers.mrc.multiplexerPort, server : ModuleInfo.name }, 'Invalid port' ); + return cb(Errors.Invalid(`Invalid port: ${config.chatServers.mrc.multiplexerPort}`)); + } + Log.info( { server : ModuleInfo.name, port : config.chatServers.mrc.multiplexerPort }, 'MRC multiplexer local listener starting up'); + return this.server.listen(port, cb); + } + + /** + * Handles connecting to to the MRC server + */ + connectToMrc() { + const self = this; + // create connection to MRC server - this.mrcClient = net.createConnection(mrcConnectOpts, () => { - this.mrcClient.write(handshake); - this.log.info(mrcConnectOpts, 'Connected to MRC server'); - mrcCentralConnection = this.mrcClient; - }); + this.mrcClient = net.createConnection(this.mrcConnectOpts, self._connectionHandler.bind(self)); + + this.mrcClient.requestedDisconnect = false; // do things when we get data from MRC central - this.mrcClient.on('data', (data) => { - // split on \n to deal with getting messages in batches - data.toString().split('\n').forEach( item => { - if (item == '') return; + var buffer = new Buffer.from(''); - this.log.debug( { data : item } , 'Received data'); - let message = this.parseMessage(item); - this.log.debug(message, 'Parsed data'); - if (message) { - this.receiveFromMRC(this.mrcClient, message); + function handleData(chunk) { + if (typeof (chunk) === 'string') { + buffer += chunk; + } else { + buffer = Buffer.concat([buffer, chunk]); + } + + var lines = buffer.toString().split(lineDelimiter); + + if (lines.pop()) { + // if buffer is not ended with \r\n, there's more chunks. + return; + } else { + // else, initialize the buffer. + buffer = new Buffer.from(''); + } + + lines.forEach(function iterator(line) { + if (line.length) { + let message = self.parseMessage(line); + if (message) { + self.receiveFromMRC(message); + } } }); + } + + this.mrcClient.on('data', (data) => { + handleData(data); }); this.mrcClient.on('end', () => { - this.log.info(mrcConnectOpts, 'Disconnected from MRC server'); + this.log.info(this.mrcConnectOpts, 'Disconnected from MRC server'); + }); + + this.mrcClient.on('close', () => { + + if (this.mrcClient && this.mrcClient.requestedDisconnect) + return; + this.log.info(this.mrcConnectOpts, 'Disconnected from MRC server, reconnecting'); + + this.log.debug('Waiting ' + this.mrcConnectOpts.retryDelay + 'ms before retrying'); + + + setTimeout(function() { + self.connectToMrc(); + }, this.mrcConnectOpts.retryDelay); }); this.mrcClient.on('error', err => { this.log.info( { error : err.message }, 'MRC server error'); }); + } + + createLocalListener() { + const self = this; // start a local server for clients to connect to this.server = net.createServer( function(socket) { @@ -90,7 +159,7 @@ exports.getModule = class MrcModule extends ServerModule { socket.on('data', data => { // split on \n to deal with getting messages in batches - data.toString().split('\n').forEach( item => { + data.toString().split(lineDelimiter).forEach( item => { if (item == '') return; // save username with socket @@ -114,24 +183,6 @@ exports.getModule = class MrcModule extends ServerModule { } }); }); - - return cb(null); - } - - listen(cb) { - if (!this.enabled) { - return cb(null); - } - - const config = Config(); - - const port = parseInt(config.chatServers.mrc.multiplexerPort); - if(isNaN(port)) { - this.log.warn( { port : config.chatServers.mrc.multiplexerPort, server : ModuleInfo.name }, 'Invalid port' ); - return cb(Errors.Invalid(`Invalid port: ${config.chatServers.mrc.multiplexerPort}`)); - } - Log.info( { server : ModuleInfo.name, port : config.chatServers.mrc.multiplexerPort }, 'MRC multiplexer local listener starting up'); - return this.server.listen(port, cb); } get enabled() { @@ -158,21 +209,21 @@ exports.getModule = class MrcModule extends ServerModule { } /** - * Processes messages received // split raw data received into an object we can work withfrom the central MRC server + * Processes messages received from the central MRC server */ - receiveFromMRC(socket, message) { + receiveFromMRC(message) { const config = Config(); const siteName = slugify(config.general.boardName); if (message.from_user == 'SERVER' && message.body == 'HELLO') { // reply with extra bbs info - this.sendToMrcServer(socket, 'CLIENT', '', 'SERVER', 'ALL', '', `INFOSYS:${StatLog.getSystemStat(SysProps.SysOpUsername)}`); + this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `INFOSYS:${StatLog.getSystemStat(SysProps.SysOpUsername)}`); } else if (message.from_user == 'SERVER' && message.body.toUpperCase() == 'PING') { // reply to heartbeat // this.log.debug('Respond to heartbeat'); - this.sendToMrcServer(socket, 'CLIENT', '', 'SERVER', 'ALL', '', `IMALIVE:${siteName}`); + this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `IMALIVE:${siteName}`); } else { // if not a heartbeat, and we have clients then we need to send something to them @@ -210,13 +261,13 @@ exports.getModule = class MrcModule extends ServerModule { Log.debug({ server : 'MRC', user : username, message : message }, 'Dodgy message received from client'); } - this.sendToMrcServer(mrcCentralConnection, message.from_user, message.from_room, message.to_user, message.to_site, message.to_room, message.body); + this.sendToMrcServer(message.from_user, message.from_room, message.to_user, message.to_site, message.to_room, message.body); } /** * Converts a message back into the MRC format and sends it to the central MRC server */ - sendToMrcServer(socket, fromUser, fromRoom, toUser, toSite, toRoom, messageBody) { + sendToMrcServer(fromUser, fromRoom, toUser, toSite, toRoom, messageBody) { const config = Config(); const siteName = slugify(config.general.boardName); @@ -231,7 +282,12 @@ exports.getModule = class MrcModule extends ServerModule { ].join('~') + '~'; Log.debug({ server : 'MRC', data : line }, 'Sending data'); - return socket.write(line + '\n'); + this.sendRaw(line); + } + + sendRaw(message) { + // optionally log messages here + this.mrcClient.write(message + '\r\n'); } }; From 136854017a26582a0684c0b23f8840f510e8d401 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Fri, 31 May 2019 20:32:16 +0100 Subject: [PATCH 051/140] Lots of tidy-up from PR #235 feedback --- core/servers/chat/mrc_multiplexer.js | 44 +++++++++++++--------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js index e0ef4a54..641353c2 100644 --- a/core/servers/chat/mrc_multiplexer.js +++ b/core/servers/chat/mrc_multiplexer.js @@ -45,12 +45,12 @@ exports.getModule = class MrcModule extends ServerModule { _connectionHandler() { const config = Config(); const boardName = config.general.prettyBoardName || config.general.boardName; - const enigmaVersion = 'ENiGMA-BBS_' + require('../../../package.json').version; + const enigmaVersion = 'ENiGMA½-BBS_' + require('../../../package.json').version; const handshake = `${boardName}~${enigmaVersion}/${os.platform()}-${os.arch()}/${protocolVersion}`; this.log.debug({ handshake : handshake }, 'Handshaking with MRC server'); - this.mrcClient.write(handshake); + this.sendRaw(handshake); this.log.info(this.mrcConnectOpts, 'Connected to MRC server'); } @@ -94,10 +94,10 @@ exports.getModule = class MrcModule extends ServerModule { this.mrcClient.requestedDisconnect = false; // do things when we get data from MRC central - var buffer = new Buffer.from(''); + let buffer = new Buffer.from(''); function handleData(chunk) { - if (typeof (chunk) === 'string') { + if(_.isString(chunk)) { buffer += chunk; } else { buffer = Buffer.concat([buffer, chunk]); @@ -113,7 +113,7 @@ exports.getModule = class MrcModule extends ServerModule { buffer = new Buffer.from(''); } - lines.forEach(function iterator(line) { + lines.forEach( line => { if (line.length) { let message = self.parseMessage(line); if (message) { @@ -151,10 +151,9 @@ exports.getModule = class MrcModule extends ServerModule { } createLocalListener() { - const self = this; - // start a local server for clients to connect to - this.server = net.createServer( function(socket) { + + this.server = net.createServer( socket => { socket.setEncoding('ascii'); socket.on('data', data => { @@ -168,7 +167,7 @@ exports.getModule = class MrcModule extends ServerModule { socket.username = item.split('|')[1]; Log.debug( { server : 'MRC', user: socket.username } , 'User connected'); } else { - self.receiveFromClient(socket.username, item); + this.receiveFromClient(socket.username, item); } }); }); @@ -219,6 +218,10 @@ exports.getModule = class MrcModule extends ServerModule { if (message.from_user == 'SERVER' && message.body == 'HELLO') { // reply with extra bbs info this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `INFOSYS:${StatLog.getSystemStat(SysProps.SysOpUsername)}`); + this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `INFOWEB:${config.general.website}`); + this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `INFOTEL:${config.general.telnetHostname}`); + this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `INFOSSH:${config.general.sshHostname}`); + this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `INFODSC:${config.general.description}`); } else if (message.from_user == 'SERVER' && message.body.toUpperCase() == 'PING') { // reply to heartbeat @@ -235,20 +238,15 @@ exports.getModule = class MrcModule extends ServerModule { * Takes an MRC message and parses it into something usable */ parseMessage(line) { - const msg = line.split('~'); - if (msg.length < 7) { - return; - } - return { - from_user: msg[0], - from_site: msg[1], - from_room: msg[2], - to_user: msg[3], - to_site: msg[4], - to_room: msg[5], - body: msg[6] - }; + const [from_user, from_site, from_room, to_user, to_site, to_room, body ] = line.split('~'); + + // const msg = line.split('~'); + // if (msg.length < 7) { + // return; + // } + + return { from_user, from_site, from_room, to_user, to_site, to_room, body }; } /** @@ -287,7 +285,7 @@ exports.getModule = class MrcModule extends ServerModule { sendRaw(message) { // optionally log messages here - this.mrcClient.write(message + '\r\n'); + this.mrcClient.write(message + '\n'); } }; From ce16f17081e34f2588775410cf2272dab5450b80 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Fri, 31 May 2019 20:34:07 +0100 Subject: [PATCH 052/140] Fix MRC MLTEV padding bug --- core/mrc.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index d696c9e7..172466a9 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -178,14 +178,17 @@ exports.getModule = class mrcModule extends MenuModule { const messageLength = stripMciColorCodes(message).length; const chatWidth = chatLogView.dimens.width; let padAmount = 0; + let spaces = 2; if (messageLength > chatWidth) { - padAmount = chatWidth - (messageLength % chatWidth); + padAmount = chatWidth - (messageLength % chatWidth) - spaces; } else { - padAmount = chatWidth - messageLength; + padAmount = chatWidth - messageLength - spaces ; } - const padding = ' |00' + ' '.repeat(padAmount - 2); + if (padAmount < 0) padAmount = 0; + + const padding = ' |00' + ' '.repeat(padAmount); chatLogView.addText(pipeToAnsi(message + padding)); } From 793c05ee82f70e296dfdd115c53dbcef1e84b1ec Mon Sep 17 00:00:00 2001 From: David Stephens Date: Fri, 31 May 2019 21:08:22 +0100 Subject: [PATCH 053/140] Add config options to template --- misc/config_template.in.hjson | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index 5e523e72..960a9896 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -50,6 +50,21 @@ general: { // Your BBS Name! boardName: XXXXX + + // Your BBS name, with pipe codes for styling + prettyBoardName : '|08XXXXX' + + // Telnet hostname and port for your board + telnetHostname : 'xibalba.l33t.codes:44510' + + // SSH hostname and port for your board + sshHostname : 'xibalba.l33t.codes:44511' + + // Your board's website + website : 'https://enigma-bbs.github.io' + + // Short board description + description : 'Yet another awesome ENiGMA½ BBS' } paths: { @@ -274,6 +289,16 @@ } } + chatServers: { + // multi relay chat settings. No need to sign up, just enable it. + // More info: https://bbswiki.bottomlessabyss.net/index.php?title=MRC_Chat_platform + mrc: { + enabled : false + serverHostname : 'mrc.bottomlessabyss.net' + serverPort : 5000 + } + } + // // Currently, ENiGMA½ can use external email to mail // users for password resets. Additional functionality will From 19e10bb09656983c267578a2fd6c7d1780c025f3 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Fri, 31 May 2019 21:19:29 +0100 Subject: [PATCH 054/140] Tweak to MRC handshake --- core/servers/chat/mrc_multiplexer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js index 641353c2..2b7c54a3 100644 --- a/core/servers/chat/mrc_multiplexer.js +++ b/core/servers/chat/mrc_multiplexer.js @@ -47,7 +47,7 @@ exports.getModule = class MrcModule extends ServerModule { const boardName = config.general.prettyBoardName || config.general.boardName; const enigmaVersion = 'ENiGMA½-BBS_' + require('../../../package.json').version; - const handshake = `${boardName}~${enigmaVersion}/${os.platform()}-${os.arch()}/${protocolVersion}`; + const handshake = `${boardName}~${enigmaVersion}/${os.platform()}.${os.arch()}/${protocolVersion}`; this.log.debug({ handshake : handshake }, 'Handshaking with MRC server'); this.sendRaw(handshake); From b6d0d0d95e53972f7947272423f8fb17ed627efe Mon Sep 17 00:00:00 2001 From: David Stephens Date: Fri, 31 May 2019 21:20:18 +0100 Subject: [PATCH 055/140] Move generic config options to general config section --- core/config.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/core/config.js b/core/config.js index 37dfbd90..a4762e91 100644 --- a/core/config.js +++ b/core/config.js @@ -169,6 +169,11 @@ function getDefaultConfig() { return { general : { boardName : 'Another Fine ENiGMA½ BBS', + prettyBoardName : '|08A|07nother |07F|08ine |07E|08NiGMA|07½ B|08BS', + telnetHostname : '', + sshHostname : '', + website : 'https://enigma-bbs.github.io', + description : 'An ENiGMA½ BBS', // :TODO: closedSystem prob belongs under users{}? closedSystem : false, // is the system closed to new users? @@ -454,15 +459,8 @@ function getDefaultConfig() { enabled : true, serverHostname : 'mrc.bottomlessabyss.net', serverPort : 5000, + retryDelay : 10000, multiplexerPort : 5000, - bbsInfo : { - sysop : '', - telnet : '', - website : '', - ssh : '', - description : '', - - } } }, From 8153473b89f4dfab160bf7b74c35e4cee6315be8 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Fri, 31 May 2019 22:00:48 +0100 Subject: [PATCH 056/140] Dial down MRC logging --- core/mrc.js | 2 -- core/servers/chat/mrc_multiplexer.js | 19 ++++++++----------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index 172466a9..bca11656 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -396,9 +396,7 @@ exports.getModule = class mrcModule extends MenuModule { body: body }; - this.log.debug({ message: message }, 'Sending message to MRC multiplexer'); // TODO: check socket still exists here - this.state.socket.write(JSON.stringify(message) + '\n'); } diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js index 2b7c54a3..69b3b248 100644 --- a/core/servers/chat/mrc_multiplexer.js +++ b/core/servers/chat/mrc_multiplexer.js @@ -34,11 +34,12 @@ exports.getModule = class MrcModule extends ServerModule { super(); this.log = Log.child( { server : 'MRC' } ); - this.config = Config(); + + const config = Config(); this.mrcConnectOpts = { - host : this.config.chatServers.mrc.serverHostname || 'mrc.bottomlessabyss.net', - port : this.config.chatServers.mrc.serverPort || 5000, - retryDelay : this.config.chatServers.mrc.retryDelay || 10000 + host : config.chatServers.mrc.serverHostname || 'mrc.bottomlessabyss.net', + port : config.chatServers.mrc.serverPort || 5000, + retryDelay : config.chatServers.mrc.retryDelay || 10000 }; } @@ -78,7 +79,7 @@ exports.getModule = class MrcModule extends ServerModule { this.log.warn( { port : config.chatServers.mrc.multiplexerPort, server : ModuleInfo.name }, 'Invalid port' ); return cb(Errors.Invalid(`Invalid port: ${config.chatServers.mrc.multiplexerPort}`)); } - Log.info( { server : ModuleInfo.name, port : config.chatServers.mrc.multiplexerPort }, 'MRC multiplexer local listener starting up'); + Log.info( { server : ModuleInfo.name, port : config.chatServers.mrc.multiplexerPort }, 'MRC multiplexer starting up'); return this.server.listen(port, cb); } @@ -135,11 +136,10 @@ exports.getModule = class MrcModule extends ServerModule { if (this.mrcClient && this.mrcClient.requestedDisconnect) return; + this.log.info(this.mrcConnectOpts, 'Disconnected from MRC server, reconnecting'); - this.log.debug('Waiting ' + this.mrcConnectOpts.retryDelay + 'ms before retrying'); - setTimeout(function() { self.connectToMrc(); }, this.mrcConnectOpts.retryDelay); @@ -199,10 +199,8 @@ exports.getModule = class MrcModule extends ServerModule { sendToClient(message) { connectedSockets.forEach( (client) => { if (message.to_user == '' || message.to_user == client.username || message.to_user == 'CLIENT' || message.from_user == client.username || message.to_user == 'NOTME' ) { - this.log.debug({ server : 'MRC', username : client.username, message : message }, 'Forwarding message to connected user'); + // this.log.debug({ server : 'MRC', username : client.username, message : message }, 'Forwarding message to connected user'); client.write(JSON.stringify(message) + '\n'); - } else { - this.log.debug({ server : 'MRC', username : client.username, message : message }, 'Not forwarding message'); } }); } @@ -225,7 +223,6 @@ exports.getModule = class MrcModule extends ServerModule { } else if (message.from_user == 'SERVER' && message.body.toUpperCase() == 'PING') { // reply to heartbeat - // this.log.debug('Respond to heartbeat'); this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `IMALIVE:${siteName}`); } else { From a97c5bbe822f3ab3f4055c40a88a446a2f14c978 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Fri, 31 May 2019 22:18:35 +0100 Subject: [PATCH 057/140] Add extra line to MRC chat area --- art/themes/luciano_blocktronics/mrc.ans | Bin 881 -> 721 bytes art/themes/luciano_blocktronics/theme.hjson | 32 +++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/art/themes/luciano_blocktronics/mrc.ans b/art/themes/luciano_blocktronics/mrc.ans index a1bbd030f30251860bafab4e8e98625373ec9843..a3079f3c879ba6ccb6f8aac3571711279406892e 100644 GIT binary patch delta 163 zcmey!c9C^MFrx>TbhLrBu|ckMw7I#nbhKga{X1OufgCgE`&{?$02!vvcerlfQLt42 zD$G@N4Kb3AHnRY!1nIhSpX>H*u%gNGOsc{_HBjL@_Z9AdbXZSLV^U*{x~04QW)0u#D_hwDB_B}mlF z2q+5H3e{<709VF!{|;18aa73qpA|n z^|>I=806l$f9F0(50D4;5ZFyX&g9FCYV1&n$^RHtd5{%NR$*#a<6vfBU}OwnkYE4< QVNWMtg)nzV2oFvI0HiZg?EnA( diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 49d459f6..0e21329c 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -1087,7 +1087,7 @@ mci: { MT1: { width: 72 - height: 18 + height: 19 } ET2: { width: 69 // fnarr! @@ -1109,6 +1109,36 @@ } } + irc: { + config: { + messageFormat: "|00|10<|02{fromUserName}|10>|00 |03{message}|00" + privateMessageFormat: "|00|10<|02{fromUserName}|15->{toUserName}|10>|00 |03{message}|00" + } + 0: { + mci: { + MT1: { + width: 72 + height: 17 + } + ET2: { + width: 69 // fnarr! + maxLength: 140 + } + TL3: { + width: 20 + } + TL4: { + width: 20 + } + TL5: { + width: 2 + } + TL6: { + width: 2 + } + } + } + } } prompts: { From 9cc59f8ff8829db4a73cdcfdd00749992c5c90f6 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 1 Jun 2019 14:38:48 -0600 Subject: [PATCH 058/140] Hopefully a little better handling of external doors/processes --- core/door.js | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/core/door.js b/core/door.js index b07c89bc..c8dd3796 100644 --- a/core/door.js +++ b/core/door.js @@ -70,14 +70,13 @@ module.exports = class Door { const args = exeInfo.args.map( arg => stringFormat(arg, formatObj) ); - this.client.log.debug( + this.client.log.info( { cmd : exeInfo.cmd, args, io : this.io }, - 'Executing door' + 'Executing external door process' ); - let door; try { - door = pty.spawn(exeInfo.cmd, args, { + this.doorPty = pty.spawn(exeInfo.cmd, args, { cols : this.client.term.termWidth, rows : this.client.term.termHeight, cwd : cwd, @@ -88,15 +87,19 @@ module.exports = class Door { return cb(e); } + this.client.log.debug( + { processId : this.doorPty.pid }, 'External door process spawned' + ); + if('stdio' === this.io) { this.client.log.debug('Using stdio for door I/O'); - this.client.term.output.pipe(door); + this.client.term.output.pipe(this.doorPty); - door.on('data', this.doorDataHandler.bind(this)); + this.doorPty.on('data', this.doorDataHandler.bind(this)); - door.once('close', () => { - return this.restoreIo(door); + this.doorPty.once('close', () => { + return this.restoreIo(this.doorPty); }); } else if('socket' === this.io) { this.client.log.debug( @@ -105,7 +108,7 @@ module.exports = class Door { ); } - door.once('exit', exitCode => { + this.doorPty.once('exit', exitCode => { this.client.log.info( { exitCode : exitCode }, 'Door exited'); if(this.sockServer) { @@ -114,10 +117,11 @@ module.exports = class Door { // we may not get a close if('stdio' === this.io) { - this.restoreIo(door); + this.restoreIo(this.doorPty); } - door.removeAllListeners(); + this.doorPty.removeAllListeners(); + delete this.doorPty; return cb(null); }); @@ -128,9 +132,15 @@ module.exports = class Door { } restoreIo(piped) { - if(!this.restored && this.client.term.output) { - this.client.term.output.unpipe(piped); - this.client.term.output.resume(); + if(!this.restored) { + if(this.doorPty) { + this.doorPty.kill(); + } + + if(this.client.term.output) { + this.client.term.output.unpipe(piped); + this.client.term.output.resume(); + } this.restored = true; } } From 5d73a224be55ce011294b2d6305c1c1e57063c4c Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 2 Jun 2019 15:24:08 +0100 Subject: [PATCH 059/140] Compress MRC help text --- core/mrc.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index bca11656..6fd8ac35 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -49,17 +49,14 @@ var MciViewIds = { // TODO: this is a bit shit, could maybe do it with an ansi instead const helpText = ` General Chat: -/rooms - List of current rooms -/join - Join a room +/rooms & /join - Send a private message -/clear - Clear the chat log ---- /whoon - Who's on what BBS /chatters - Who's in what room -/topic - Set the topic -/meetups - MRC MeetUps -/bbses - BBS's connected -/info - Info about specific BBS +/topic - Set the room topic +/bbses & /info - Info about BBS's connected +/meetups - Info about MRC MeetUps --- /l33t - l337 5p34k /kewl - BBS KeWL SPeaK From 8726128cda8e96ba8128f2d0457f490af0d7f933 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 2 Jun 2019 15:47:15 +0100 Subject: [PATCH 060/140] Turn off MRC by default --- core/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config.js b/core/config.js index a4762e91..c8a4c46a 100644 --- a/core/config.js +++ b/core/config.js @@ -456,7 +456,7 @@ function getDefaultConfig() { chatServers : { mrc: { - enabled : true, + enabled : false, serverHostname : 'mrc.bottomlessabyss.net', serverPort : 5000, retryDelay : 10000, From 43e22cfc742d0629b1b1ea89cfad21642aeeb5ff Mon Sep 17 00:00:00 2001 From: David Stephens Date: Wed, 5 Jun 2019 23:41:28 +0100 Subject: [PATCH 061/140] Turn off debug logging of MRC message sending --- core/servers/chat/mrc_multiplexer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js index 69b3b248..9b976eff 100644 --- a/core/servers/chat/mrc_multiplexer.js +++ b/core/servers/chat/mrc_multiplexer.js @@ -276,7 +276,7 @@ exports.getModule = class MrcModule extends ServerModule { sanitiseMessage(messageBody) ].join('~') + '~'; - Log.debug({ server : 'MRC', data : line }, 'Sending data'); + // Log.debug({ server : 'MRC', data : line }, 'Sending data'); this.sendRaw(line); } From c79e2bc97118d41e7c665a4b3730fa07841e5eba Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 5 Jun 2019 22:42:20 -0600 Subject: [PATCH 062/140] Remove menu.hjson - use config templates --- config/menu.hjson | 4237 --------------------------------------------- 1 file changed, 4237 deletions(-) delete mode 100644 config/menu.hjson diff --git a/config/menu.hjson b/config/menu.hjson deleted file mode 100644 index 99585ade..00000000 --- a/config/menu.hjson +++ /dev/null @@ -1,4237 +0,0 @@ -{ - /* - ./\/\.' ENiGMA½ Menu Configuration -/--/-------- - -- - - - _____________________ _____ ____________________ __________\_ / - \__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp! - // __|___// | \// |// | \// | | \// \ /___ /_____ - /____ _____| __________ ___|__| ____| \ / _____ \ - ---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/ - /__ _\ - <*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/ - - *-----------------------------------------------------------------------------* - - General Information - ------------------------------- - - - This configuration is in HJSON (http://hjson.org/) format. Strict to-spec - JSON is also perfectly valid. Use 'hjson' from npm to convert to/from JSON. - - See http://hjson.org/ for more information and syntax. - - Various editors and IDEs such as Sublime Text 3, Visual Studio Code, and so - on have syntax highlighting for the HJSON format which are highly recommended. - - ------------------------------- -- - - - Menu Configuration - ------------------------------- - - - ENiGMA½ makes no assumptions about specific menu types (main, doors, etc.), - but instead allows full customization of all menus throughout the system. - Some menus such as a main menu are considered "standard" while others are - backed by a specific module. SysOps can tweak various settings about these - modules (look & feel, keyboard interation, and so on) or even fully replace - the module with something else. - - This file starts out as an example setup. Look at the examples, change - settings, menu ordering/flow, add/remove menus, implement ACS control, - etc.! - - Remember you can *live edit* this file. That is, make a change and save - while you're logged into the system and it will take effect on the next - menu change or screen refresh. - - Please see RTFM ...er, uh... see the documentation for more information, and - don't be shy to ask for help: - - BBS : Xibalba @ xibalba.l33t.codes - FTN : BBS Discussion on fsxNet - IRC : #enigma-bbs / FreeNode - Email : bryan@l33t.codes - */ - menus: { - // - // Send telnet connections to matrix where users can login, apply, etc. - // - telnetConnected: { - art: CONNECT - next: matrix - config: { nextTimeout: 1500 } - } - - // - // SSH connections are pre-authenticated via the SSH server itself. - // Jump directly to the login sequence - // - sshConnected: { - art: CONNECT - next: fullLoginSequenceLoginArt - config: { nextTimeout: 1500 } - } - - // - // Another SSH specialization: If the user logs in with a new user - // name (e.g. "new", "apply", ...) they will be directed to the - // application process. - // - sshConnectedNewUser: { - art: CONNECT - next: newUserApplicationPreSsh - config: { nextTimeout: 1500 } - } - - // Ye ol' standard matrix - matrix: { - art: matrix - form: { - 0: { - VM: { - mci: { - VM1: { - submit: true - focus: true - argName: navSelect - // - // To enable forgot password, you will need to have the web server - // enabled and mail/SMTP configured. Once that is in place, swap out - // the commented lines below as well as in the submit block - // - items: [ - { - text: login - data: login - } - { - text: apply - data: apply - } - { - text: forgot pass - data: forgot - } - { - text: log off - data: logoff - } - ] - } - } - submit: { - *: [ - { - value: { navSelect: "login" } - action: @menu:login - } - { - value: { navSelect: "apply" } - action: @menu:newUserApplicationPre - } - { - value: { navSelect: "forgot" } - action: @menu:forgotPassword - } - { - value: { navSelect: "logoff" } - action: @menu:logoff - } - ] - } - } - } - } - } - - login: { - art: USERLOG - next: fullLoginSequenceLoginArt - config: { - tooNodeMenu: loginAttemptTooNode - inactive: loginAttemptAccountInactive - disabled: loginAttemptAccountDisabled - locked: loginAttemptAccountLocked - } - form: { - 0: { - mci: { - ET1: { - maxLength: @config:users.usernameMax - argName: username - focus: true - } - ET2: { - password: true - maxLength: @config:users.passwordMax - argName: password - submit: true - } - } - submit: { - *: [ - { - value: { password: null } - action: @systemMethod:login - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - loginAttemptTooNode: { - art: TOONODE - config: { - cls: true - nextTimeout: 2000 - } - next: logoff - } - - loginAttemptAccountLocked: { - art: ACCOUNTLOCKED - config: { - cls: true - nextTimeout: 2000 - } - next: logoff - } - - loginAttemptAccountDisabled: { - art: ACCOUNTDISABLED - config: { - cls: true - nextTimeout: 2000 - } - next: logoff - } - - loginAttemptAccountInactive: { - art: ACCOUNTINACTIVE - config: { - cls: true - nextTimeout: 2000 - } - next: logoff - } - - forgotPassword: { - desc: Forgot password - prompt: forgotPasswordPrompt - submit: [ - { - value: { username: null } - action: @systemMethod:sendForgotPasswordEmail - extraArgs: { next: "forgotPasswordSubmitted" } - } - ] - } - - forgotPasswordSubmitted: { - desc: Forgot password - art: FORGOTPWSENT - config: { - cls: true - pause: true - } - next: @systemMethod:logoff - } - - // :TODO: Prompt Yes/No for logoff confirm - fullLogoffSequence: { - desc: Logging Off - prompt: logoffConfirmation - submit: [ - { - value: { promptValue: 0 } - action: @menu:fullLogoffSequencePreAd - } - { - value: { promptValue: 1 } - action: @systemMethod:prevMenu - } - ] - } - - fullLogoffSequencePreAd: { - art: PRELOGAD - desc: Logging Off - next: fullLogoffSequenceRandomBoardAd - config: { - cls: true - nextTimeout: 1500 - } - } - - fullLogoffSequenceRandomBoardAd: { - art: OTHRBBS - desc: Logging Off - next: logoff - config: { - baudRate: 57600 - pause: true - cls: true - } - } - - logoff: { - art: LOGOFF - desc: Logging Off - next: @systemMethod:logoff - } - - // A quick preamble - defaults to warning about broken terminals - newUserApplicationPre: { - art: NEWUSER1 - next: newUserApplication - desc: Applying - config: { - pause: true - cls: true - menuFlags: [ "noHistory" ] - } - } - - newUserApplication: { - module: nua - art: NUA - next: [ - { - // Initial SysOp does not send feedback to themselves - acs: ID1 - next: fullLoginSequenceLoginArt - } - { - // ...everyone else does - next: newUserFeedbackToSysOpPreamble - } - ] - form: { - 0: { - mci: { - ET1: { - focus: true - argName: username - maxLength: @config:users.usernameMax - validate: @systemMethod:validateUserNameAvail - } - ET2: { - argName: realName - maxLength: @config:users.realNameMax - validate: @systemMethod:validateNonEmpty - } - MET3: { - argName: birthdate - maskPattern: "####/##/##" - validate: @systemMethod:validateBirthdate - } - ME4: { - argName: sex - maskPattern: A - textStyle: upper - validate: @systemMethod:validateNonEmpty - } - ET5: { - argName: location - maxLength: @config:users.locationMax - validate: @systemMethod:validateNonEmpty - } - ET6: { - argName: affils - maxLength: @config:users.affilsMax - } - ET7: { - argName: email - maxLength: @config:users.emailMax - validate: @systemMethod:validateEmailAvail - } - ET8: { - argName: web - maxLength: @config:users.webMax - } - ET9: { - argName: password - password: true - maxLength: @config:users.passwordMax - validate: @systemMethod:validatePasswordSpec - } - ET10: { - argName: passwordConfirm - password: true - maxLength: @config:users.passwordMax - validate: @method:validatePassConfirmMatch - } - TM12: { - argName: submission - items: [ "apply", "cancel" ] - submit: true - } - } - - submit: { - *: [ - { - value: { "submission" : 0 } - action: @method:submitApplication - extraArgs: { - inactive: userNeedsActivated - error: newUserCreateError - } - } - { - value: { "submission" : 1 } - action: @systemMethod:prevMenu - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - // A quick preamble - defaults to warning about broken terminals (SSH version) - newUserApplicationPreSsh: { - art: NEWUSER1 - next: newUserApplicationSsh - desc: Applying - config: { - pause: true - cls: true - menuFlags: [ "noHistory" ] - } - } - - // - // SSH specialization of NUA - // Canceling this form logs off vs falling back to matrix - // - newUserApplicationSsh: { - module: nua - art: NUA - fallback: logoff - next: newUserFeedbackToSysOpPreamble - form: { - 0: { - mci: { - ET1: { - focus: true - argName: username - maxLength: @config:users.usernameMax - validate: @systemMethod:validateUserNameAvail - } - ET2: { - argName: realName - maxLength: @config:users.realNameMax - validate: @systemMethod:validateNonEmpty - } - MET3: { - argName: birthdate - maskPattern: "####/##/##" - validate: @systemMethod:validateBirthdate - } - ME4: { - argName: sex - maskPattern: A - textStyle: upper - validate: @systemMethod:validateNonEmpty - } - ET5: { - argName: location - maxLength: @config:users.locationMax - validate: @systemMethod:validateNonEmpty - } - ET6: { - argName: affils - maxLength: @config:users.affilsMax - } - ET7: { - argName: email - maxLength: @config:users.emailMax - validate: @systemMethod:validateEmailAvail - } - ET8: { - argName: web - maxLength: @config:users.webMax - } - ET9: { - argName: password - password: true - maxLength: @config:users.passwordMax - validate: @systemMethod:validatePasswordSpec - } - ET10: { - argName: passwordConfirm - password: true - maxLength: @config:users.passwordMax - validate: @method:validatePassConfirmMatch - } - TM12: { - argName: submission - items: [ "apply", "cancel" ] - submit: true - } - } - - submit: { - *: [ - { - value: { "submission" : 0 } - action: @method:submitApplication - extraArgs: { - inactive: userNeedsActivated - error: newUserCreateError - } - } - { - value: { "submission" : 1 } - action: @systemMethod:logoff - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:logoff - } - ] - } - } - } - - newUserFeedbackToSysOpPreamble: { - art: LETTER - config: { pause: true } - next: newUserFeedbackToSysOp - } - - newUserFeedbackToSysOp: { - desc: Feedback to SysOp - module: msg_area_post_fse - next: [ - { - acs: AS2 - next: fullLoginSequenceLoginArt - } - { - next: newUserInactiveDone - } - ] - config: { - art: { - header: MSGEHDR - body: MSGBODY - footerEditor: MSGEFTR - footerEditorMenu: MSGEMFT - help: MSGEHLP - }, - editorMode: edit - editorType: email - messageAreaTag: private_mail - toUserId: 1 /* always to +op */ - } - form: { - 0: { - mci: { - TL1: { - argName: from - } - ET2: { - argName: to - focus: true - text: @sysStat:sysop_username - // :TODO: readOnly: true - } - ET3: { - argName: subject - maxLength: 72 - submit: true - text: New user feedback - validate: @systemMethod:validateMessageSubject - } - } - submit: { - 3: [ - { - value: { subject: null } - action: @method:headerSubmit - } - ] - } - } - 1: { - mci: { - MT1: { - width: 79 - argName: message - mode: edit - } - } - - submit: { - *: [ { value: "message", action: "@method:editModeEscPressed" } ] - } - actionKeys: [ - { - keys: [ "escape" ] - viewId: 1 - } - ] - }, - 2: { - TLTL: { - mci: { - TL1: { - width: 5 - } - TL2: { - width: 4 - } - } - } - } - 3: { - HM: { - mci: { - HM1: { - // :TODO: clear - items: [ "save", "help" ] - } - } - submit: { - *: [ - { - value: { 1: 0 } - action: @method:editModeMenuSave - } - { - value: { 1: 1 } - action: @method:editModeMenuHelp - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:editModeEscPressed - } - { - keys: [ "?" ] - action: @method:editModeMenuHelp - } - ] - } - } - } - } - - newUserInactiveDone: { - desc: Finished with NUA - art: DONE - config: { pause: true } - next: @menu:logoff - } - - fullLoginSequenceLoginArt: { - desc: Logging In - art: WELCOME - config: { pause: true } - next: fullLoginSequenceLastCallers - } - - fullLoginSequenceLastCallers: { - desc: Last Callers - module: last_callers - art: LASTCALL - config: { - pause: true - font: cp437 - } - next: fullLoginSequenceWhosOnline - } - fullLoginSequenceWhosOnline: { - desc: Who's Online - module: whos_online - art: WHOSON - config: { pause: true } - next: fullLoginSequenceOnelinerz - } - - fullLoginSequenceOnelinerz: { - desc: Viewing Onelinerz - module: onelinerz - next: [ - { - // calls >= 2 - acs: NC2 - next: fullLoginSequenceNewScanConfirm - } - { - // new users - skip new scan - next: fullLoginSequenceUserStats - } - ] - config: { - cls: true - art: { - view: ONELINER - add: ONEADD - } - } - form: { - 0: { - mci: { - VM1: { - focus: false - height: 10 - } - TM2: { - argName: addOrExit - items: [ "yeah!", "nah" ] - "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } - submit: true - focus: true - } - } - submit: { - *: [ - { - value: { addOrExit: 0 } - action: @method:viewAddScreen - } - { - value: { addOrExit: null } - action: @systemMethod:nextMenu - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:nextMenu - } - ] - }, - 1: { - mci: { - ET1: { - focus: true - maxLength: 70 - argName: oneliner - } - TL2: { - width: 60 - } - TM3: { - argName: addOrCancel - items: [ "add", "cancel" ] - "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } - submit: true - } - } - - submit: { - *: [ - { - value: { addOrCancel: 0 } - action: @method:addEntry - } - { - value: { addOrCancel: 1 } - action: @method:cancelAdd - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:cancelAdd - } - ] - } - } - } - - fullLoginSequenceNewScanConfirm: { - desc: Logging In - prompt: loginGlobalNewScan - submit: [ - { - value: { promptValue: 0 } - action: @menu:fullLoginSequenceNewScan - } - { - value: { promptValue: 1 } - action: @menu:fullLoginSequenceUserStats - } - ] - } - - fullLoginSequenceNewScan: { - desc: Performing New Scan - module: new_scan - art: NEWSCAN - next: fullLoginSequenceSysStats - config: { - messageListMenu: newScanMessageList - } - } - - fullLoginSequenceSysStats: { - desc: System Stats - art: SYSSTAT - config: { pause: true } - next: fullLoginSequenceUserStats - } - fullLoginSequenceUserStats: { - desc: User Stats - art: STATUS - config: { pause: true } - next: mainMenu - } - - newScanMessageList: { - desc: New Messages - module: msg_list - art: NEWMSGS - config: { - menuViewPost: messageAreaViewPost - } - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - argName: message - } - TL6: { - // theme me! - } - } - submit: { - *: [ - { - value: { message: null } - action: @method:selectMessage - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - { - keys: [ "x", "shift + x" ] - action: @method:fullExit - } - { - keys: [ "m", "shift + m" ] - action: @method:markAllRead - } - ] - } - } - } - - newScanFileBaseList: { - module: file_area_list - desc: New Files - config: { - art: { - browse: FNEWBRWSE - details: FDETAIL - detailsGeneral: FDETGEN - detailsNfo: FDETNFO - detailsFileList: FDETLST - help: FBHELP - } - } - form: { - 0: { - mci: { - MT1: { - mode: preview - ansiView: true - } - - HM2: { - focus: true - submit: true - argName: navSelect - items: [ - "prev", "next", "details", "toggle queue", "rate", "help", "quit" - ] - focusItemIndex: 1 - } - } - - submit: { - *: [ - { - value: { navSelect: 0 } - action: @method:prevFile - } - { - value: { navSelect: 1 } - action: @method:nextFile - } - { - value: { navSelect: 2 } - action: @method:viewDetails - } - { - value: { navSelect: 3 } - action: @method:toggleQueue - } - { - value: { navSelect: 4 } - action: @menu:fileBaseGetRatingForSelectedEntry - } - { - value: { navSelect: 5 } - action: @method:displayHelp - } - { - value: { navSelect: 6 } - action: @systemMethod:prevMenu - } - ] - } - - actionKeys: [ - { - keys: [ "w", "shift + w" ] - action: @method:showWebDownloadLink - } - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - { - keys: [ "t", "shift + t" ] - action: @method:toggleQueue - } - { - keys: [ "v", "shift + v" ] - action: @method:viewDetails - } - { - keys: [ "r", "shift + r" ] - action: @menu:fileBaseGetRatingForSelectedEntry - } - { - keys: [ "?" ] - action: @method:displayHelp - } - ] - } - - 1: { - mci: { - HM1: { - focus: true - submit: true - argName: navSelect - items: [ - "general", "nfo/readme", "file listing" - ] - } - } - - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @method:detailsQuit - } - ] - } - - 2: { - // details - general - mci: {} - } - - 3: { - // details - nfo/readme - mci: { - MT1: { - mode: preview - } - } - } - - 4: { - // details - file listing - mci: { - VM1: { - - } - } - } - } - } - - /////////////////////////////////////////////////////////////////////// - // Main Menu - /////////////////////////////////////////////////////////////////////// - mainMenu: { - art: MMENU - desc: Main Menu - prompt: menuCommand - config: { - font: cp437 - interrupt: realtime - } - submit: [ - { - value: { command: "MSG" } - action: @menu:nodeMessage - } - { - value: { command: "MRC" } - action: @menu:mrc - } - { - value: { command: "G" } - action: @menu:fullLogoffSequence - } - { - value: { command: "D" } - action: @menu:doorMenu - } - { - value: { command: "F" } - action: @menu:fileBase - } - { - value: { command: "U" } - action: @menu:mainMenuUserList - } - { - value: { command: "L" } - action: @menu:mainMenuLastCallers - } - { - value: { command: "W" } - action: @menu:mainMenuWhosOnline - } - { - value: { command: "Y" } - action: @menu:mainMenuUserStats - } - { - value: { command: "M" } - action: @menu:messageArea - } - { - value: { command: "E" } - action: @menu:mailMenu - } - { - value: { command: "C" } - action: @menu:mainMenuUserConfig - } - { - value: { command: "S" } - action: @menu:mainMenuSystemStats - } - { - value: { command: "!" } - action: @menu:mainMenuGlobalNewScan - } - { - value: { command: "K" } - action: @menu:mainMenuFeedbackToSysOp - } - { - value: { command: "O" } - action: @menu:mainMenuOnelinerz - } - { - value: { command: "R" } - action: @menu:mainMenuRumorz - } - { - value: { command: "BBS"} - action: @menu:bbsList - } - { - value: { command: "UA" } - action: @menu:mainMenuUserAchievementsEarned - } - { - value: 1 - action: @menu:mainMenu - } - ] - } - - mainMenuUserAchievementsEarned: { - desc: Achievements - module: user_achievements_earned - art: USERACHIEV - form: { - 0: { - mci: { - VM1: { - focus: true - } - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - ercClient: { - art: erc - module: erc_client - config: { - host: localhost - port: 5001 - bbsTag: CHANGEME - } - - form: { - 0: { - mci: { - MT1: { - width: 79 - height: 21 - mode: preview - autoScroll: true - } - ET3: { - autoScale: false - width: 77 - argName: inputArea - focus: true - submit: true - } - } - - submit: { - *: [ - { - value: { inputArea: null } - action: @method:inputAreaSubmit - } - ] - } - actionKeys: [ - { - keys: [ "tab" ] - } - { - keys: [ "up arrow" ] - action: @method:scrollDown - } - { - keys: [ "down arrow" ] - action: @method:scrollUp - } - ] - } - } - } - - mrc: { - desc: MRC Chat - module: mrc - art: MRC - config: { - cls: true - } - form: { - 0: { - mci: { - MT1: { - mode: preview - autoScroll: true - } - ET2: { - argName: inputArea - submit: true - focus: true - } - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:quit - } - { - keys: [ "down arrow", "up arrow", "page up", "page down" ] - action: @method:movementKeyPressed - } - ] - submit: { - *: [ - { - value: { inputArea: null } - action: @method:sendChatMessage - } - ] - } - } - } - } - - nodeMessage: { - desc: Node Messaging - module: node_msg - art: NODEMSG - config: { - cls: true - art: { - header: NODEMSGHDR - footer: NODEMSGFTR - } - } - form: { - 0: { - mci: { - SM1: { - argName: node - } - ET2: { - argName: message - submit: true - } - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - submit: { - *: [ - { - value: { message: null } - action: @method:sendMessage - } - ] - } - } - } - } - - mainMenuLastCallers: { - desc: Last Callers - module: last_callers - art: LASTCALL - config: { pause: true } - } - - mainMenuWhosOnline: { - desc: Who's Online - module: whos_online - art: WHOSON - config: { pause: true } - } - - mainMenuUserStats: { - desc: User Stats - art: STATUS - config: { pause: true } - } - - mainMenuSystemStats: { - desc: System Stats - art: SYSSTAT - config: { pause: true } - } - - mainMenuUserList: { - desc: User Listing - module: user_list - art: USERLST - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - } - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - mainMenuUserConfig: { - module: user_config - art: CONFSCR - form: { - 0: { - mci: { - ET1: { - argName: realName - maxLength: @config:users.realNameMax - validate: @systemMethod:validateNonEmpty - focus: true - } - ME2: { - argName: birthdate - maskPattern: "####/##/##" - } - ME3: { - argName: sex - maskPattern: A - textStyle: upper - validate: @systemMethod:validateNonEmpty - } - ET4: { - argName: location - maxLength: @config:users.locationMax - validate: @systemMethod:validateNonEmpty - } - ET5: { - argName: affils - maxLength: @config:users.affilsMax - } - ET6: { - argName: email - maxLength: @config:users.emailMax - validate: @method:validateEmailAvail - } - ET7: { - argName: web - maxLength: @config:users.webMax - } - ME8: { - maskPattern: "##" - argName: termHeight - validate: @systemMethod:validateNonEmpty - } - SM9: { - argName: theme - } - ET10: { - argName: password - maxLength: @config:users.passwordMax - password: true - validate: @method:validatePassword - } - ET11: { - argName: passwordConfirm - maxLength: @config:users.passwordMax - password: true - validate: @method:validatePassConfirmMatch - } - TM25: { - argName: submission - items: [ "save", "cancel" ] - submit: true - } - } - - submit: { - *: [ - { - value: { submission: 0 } - action: @method:saveChanges - } - { - value: { submission: 1 } - action: @systemMethod:prevMenu - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - mainMenuGlobalNewScan: { - desc: Performing New Scan - module: new_scan - art: NEWSCAN - config: { - messageListMenu: newScanMessageList - } - } - - mainMenuFeedbackToSysOp: { - desc: Feedback to SysOp - module: msg_area_post_fse - config: { - art: { - header: MSGEHDR - body: MSGBODY - footerEditor: MSGEFTR - footerEditorMenu: MSGEMFT - help: MSGEHLP - }, - editorMode: edit - editorType: email - messageAreaTag: private_mail - toUserId: 1 /* always to +op */ - } - form: { - 0: { - mci: { - TL1: { - argName: from - } - ET2: { - argName: to - focus: true - text: @sysStat:sysop_username - // :TODO: readOnly: true - } - ET3: { - argName: subject - maxLength: 72 - submit: true - validate: @systemMethod:validateMessageSubject - } - } - submit: { - 3: [ - { - value: { subject: null } - action: @method:headerSubmit - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - 1: { - mci: { - MT1: { - width: 79 - argName: message - mode: edit - } - } - - submit: { - *: [ { value: "message", action: "@method:editModeEscPressed" } ] - } - actionKeys: [ - { - keys: [ "escape" ] - viewId: 1 - } - ] - }, - 2: { - TLTL: { - mci: { - TL1: { - width: 5 - } - TL2: { - width: 4 - } - } - } - } - 3: { - HM: { - mci: { - HM1: { - // :TODO: clear - items: [ "save", "discard", "help" ] - } - } - submit: { - *: [ - { - value: { 1: 0 } - action: @method:editModeMenuSave - } - { - value: { 1: 1 } - action: @systemMethod:prevMenu - } - { - value: { 1: 2 } - action: @method:editModeMenuHelp - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:editModeEscPressed - } - { - keys: [ "?" ] - action: @method:editModeMenuHelp - } - ] - } - } - } - } - - mainMenuOnelinerz: { - desc: Viewing Onelinerz - module: onelinerz - config: { - cls: true - art: { - view: ONELINER - add: ONEADD - } - } - form: { - 0: { - mci: { - VM1: { - focus: false - height: 10 - } - TM2: { - argName: addOrExit - items: [ "yeah!", "nah" ] - "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } - submit: true - focus: true - } - } - submit: { - *: [ - { - value: { addOrExit: 0 } - action: @method:viewAddScreen - } - { - value: { addOrExit: null } - action: @systemMethod:nextMenu - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:nextMenu - } - ] - }, - 1: { - mci: { - ET1: { - focus: true - maxLength: 70 - argName: oneliner - } - TL2: { - width: 60 - } - TM3: { - argName: addOrCancel - items: [ "add", "cancel" ] - "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } - submit: true - } - } - - submit: { - *: [ - { - value: { addOrCancel: 0 } - action: @method:addEntry - } - { - value: { addOrCancel: 1 } - action: @method:cancelAdd - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:cancelAdd - } - ] - } - } - } - - mainMenuRumorz: { - desc: Rumorz - module: rumorz - config: { - cls: true - art: { - entries: RUMORS - add: RUMORADD - } - } - form: { - 0: { - mci: { - VM1: { - focus: false - height: 10 - } - TM2: { - argName: addOrExit - items: [ "yeah!", "nah" ] - "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } - submit: true - focus: true - } - } - submit: { - *: [ - { - value: { addOrExit: 0 } - action: @method:viewAddScreen - } - { - value: { addOrExit: null } - action: @systemMethod:nextMenu - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:nextMenu - } - ] - }, - 1: { - mci: { - ET1: { - focus: true - maxLength: 70 - argName: rumor - } - TL2: { - width: 60 - } - TM3: { - argName: addOrCancel - items: [ "add", "cancel" ] - "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } - submit: true - } - } - - submit: { - *: [ - { - value: { addOrCancel: 0 } - action: @method:addEntry - } - { - value: { addOrCancel: 1 } - action: @method:cancelAdd - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:cancelAdd - } - ] - } - } - } - - bbsList: { - desc: Viewing BBS List - module: bbs_list - config: { - cls: true - art: { - entries: BBSLIST - add: BBSADD - } - } - - form: { - 0: { - mci: { - VM1: { maxLength: 32 } - TL2: { maxLength: 32 } - TL3: { maxLength: 32 } - TL4: { maxLength: 32 } - TL5: { maxLength: 32 } - TL6: { maxLength: 32 } - TL7: { maxLength: 32 } - TL8: { maxLength: 32 } - TL9: { maxLength: 32 } - } - actionKeys: [ - { - keys: [ "a" ] - action: @method:addBBS - } - { - // :TODO: add delete key - keys: [ "d" ] - action: @method:deleteBBS - } - { - keys: [ "q", "escape" ] - action: @systemMethod:prevMenu - } - ] - } - 1: { - mci: { - ET1: { - argName: name - maxLength: 32 - validate: @systemMethod:validateNonEmpty - } - ET2: { - argName: sysop - maxLength: 32 - validate: @systemMethod:validateNonEmpty - } - ET3: { - argName: telnet - maxLength: 32 - validate: @systemMethod:validateNonEmpty - } - ET4: { - argName: www - maxLength: 32 - } - ET5: { - argName: location - maxLength: 32 - } - ET6: { - argName: software - maxLength: 32 - } - ET7: { - argName: notes - maxLength: 32 - } - TM17: { - argName: submission - items: [ "save", "cancel" ] - submit: true - } - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @method:cancelSubmit - } - ] - - submit: { - *: [ - { - value: { "submission" : 0 } - action: @method:submitBBS - } - { - value: { "submission" : 1 } - action: @method:cancelSubmit - } - ] - } - } - } - } - - /////////////////////////////////////////////////////////////////////// - // Doors Menu - /////////////////////////////////////////////////////////////////////// - doorMenu: { - desc: Doors Menu - art: DOORMNU - prompt: menuCommand - config: { - interrupt: realtime - } - submit: [ - { - value: { command: "G" } - action: @menu:logoff - } - { - value: { command: "Q" } - action: @systemMethod:prevMenu - } - // - // The system supports many ways of launching doors including - // modules for DoorParty!, BBSLink, etc. - // - // Below are some examples. See the documentation for more info. - // - { - value: { command: "ABRACADABRA" } - action: @menu:doorAbracadabraExample - } - { - value: { command: "TWBBSLINK" } - action: @menu:doorTradeWars2002BBSLinkExample - } - { - value: { command: "DP" } - action: @menu:doorPartyExample - } - { - value: { command: "CN" } - action: @menu:doorCombatNetExample - } - { - value: { command: "EXODUS" } - action: @menu:doorExodusCataclysm - } - ] - } - - // - // Local Door Example via abracadabra module - // - // This example assumes launch_door.sh (which is passed args) - // launches the door. - // - doorAbracadabraExample: { - desc: Abracadabra Example - module: abracadabra - config: { - name: Example Door - dropFileType: DORINFO - cmd: /home/enigma/DOS/scripts/launch_door.sh - args: [ - "{node}", - "{dropFile}", - "{srvPort}", - ], - nodeMax: 1 - tooManyArt: DOORMANY - io: socket - } - } - - // - // BBSLink Example (TradeWars 2000) - // - // Register @ https://bbslink.net/ - // - doorTradeWars2002BBSLinkExample: { - desc: Playing TW 2002 (BBSLink) - module: bbs_link - config: { - sysCode: XXXXXXXX - authCode: XXXXXXXX - schemeCode: XXXXXXXX - door: tw - } - } - - // - // DoorParty! Example - // - // Register @ http://throwbackbbs.com/ - // - doorPartyExample: { - desc: Using DoorParty! - module: door_party - config: { - username: XXXXXXXX - password: XXXXXXXX - bbsTag: XX - } - } - - // - // CombatNet Example - // - // Register @ http://combatnet.us/ - // - doorCombatNetExample: { - desc: Using CombatNet - module: combatnet - config: { - bbsTag: CBNxxx - password: XXXXXXXXX - } - } - - // - // Exodus Example (cataclysm) - // Register @ https://oddnetwork.org/exodus/ - // - doorExodusCataclysm: { - desc: Cataclysm - module: exodus - config: { - rejectUnauthorized: false - board: XXX - key: XXXXXXXX - door: cataclysm - } - } - - /////////////////////////////////////////////////////////////////////// - // Message Area Menu - /////////////////////////////////////////////////////////////////////// - messageArea: { - art: MSGMNU - desc: Message Area - prompt: messageMenuCommand - config: { - interrupt: realtime - } - submit: [ - { - value: { command: "P" } - action: @menu:messageAreaNewPost - } - { - value: { command: "J" } - action: @menu:messageAreaChangeCurrentConference - } - { - value: { command: "C" } - action: @menu:messageAreaChangeCurrentArea - } - { - value: { command: "L" } - action: @menu:messageAreaMessageList - } - { - value: { command: "Q" } - action: @systemMethod:prevMenu - } - { - value: { command: "G" } - action: @menu:fullLogoffSequence - } - { - value: { command: "<" } - action: @systemMethod:prevConf - } - { - value: { command: ">" } - action: @systemMethod:nextConf - } - { - value: { command: "[" } - action: @systemMethod:prevArea - } - { - value: { command: "]" } - action: @systemMethod:nextArea - } - { - value: { command: "D" } - action: @menu:messageAreaSetNewScanDate - } - { - value: { command: "S" } - action: @menu:messageSearch - } - { - value: 1 - action: @menu:messageArea - } - ] - } - - messageSearch: { - desc: Message Search - module: message_base_search - art: MSEARCH - config: { - messageListMenu: messageAreaSearchMessageList - } - form: { - 0: { - mci: { - ET1: { - focus: true - argName: searchTerms - } - BT2: { - argName: search - text: search - submit: true - } - SM3: { - argName: confTag - } - SM4: { - argName: areaTag - } - ET5: { - argName: toUserName - maxLength: @config:users.usernameMax - } - ET6: { - argName: fromUserName - maxLength: @config:users.usernameMax - } - BT7: { - argName: advancedSearch - text: advanced search - submit: true - } - } - - submit: { - *: [ - { - value: { search: null } - action: @method:search - } - { - value: { advancedSearch: null } - action: @method:search - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - messageAreaSearchMessageList: { - desc: Message Search - module: msg_list - art: MSRCHLST - config: { - menuViewPost: messageAreaViewPost - } - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - argName: message - } - TL6: { - // theme me! - } - } - submit: { - *: [ - { - value: { message: null } - action: @method:selectMessage - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - messageAreaMyMessagesList: { - desc: Personal Messages - module: msg_list - art: MYMSGLST - config: { - menuViewPost: messageAreaViewPost - } - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - argName: message - } - } - submit: { - *: [ - { - value: { message: null } - action: @method:selectMessage - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - messageSearchNoResults: { - desc: Message Search - art: MSRCNORES - config: { - pause: true - } - } - - messageAreaChangeCurrentConference: { - art: CCHANGE - module: msg_conf_list - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - argName: conf - } - } - submit: { - *: [ - { - value: { conf: null } - action: @method:changeConference - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - messageAreaSetNewScanDate: { - module: set_newscan_date - desc: Message Base - art: SETMNSDATE - config: { - target: message - scanDateFormat: YYYYMMDD - } - form: { - 0: { - mci: { - ME1: { - focus: true - submit: true - argName: scanDate - maskPattern: "####/##/##" - } - SM2: { - argName: targetSelection - submit: false - } - } - submit: { - *: [ - { - value: { scanDate: null } - action: @method:scanDateSubmit - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - changeMessageConfPreArt: { - module: show_art - config: { - method: messageConf - key: confTag - pause: true - cls: true - menuFlags: [ "popParent", "noHistory" ] - } - } - - messageAreaChangeCurrentArea: { - // :TODO: rename this art to ACHANGE - art: CHANGE - module: msg_area_list - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - argName: area - } - } - submit: { - *: [ - { - value: { area: null } - action: @method:changeArea - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - changeMessageAreaPreArt: { - module: show_art - config: { - method: messageArea - key: areaTag - pause: true - cls: true - menuFlags: [ "popParent", "noHistory" ] - } - } - - messageAreaMessageList: { - module: msg_list - art: MSGLIST - config: { - menuViewPost: messageAreaViewPost - } - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - argName: message - } - } - submit: { - *: [ - { - value: { message: null } - action: @method:selectMessage - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - messageAreaViewPost: { - module: msg_area_view_fse - config: { - art: { - header: MSGVHDR - body: MSGBODY - footerView: MSGVFTR - help: MSGVHLP - }, - editorMode: view - editorType: area - } - form: { - 0: { - mci: { - // :TODO: ensure this block isn't even req. for theme to apply... - } - } - 1: { - mci: { - MT1: { - width: 79 - mode: preview - } - } - submit: { - *: [ - { - value: message - action: @method:editModeEscPressed - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - viewId: 1 - } - ] - } - 2: { - TLTL: { - mci: { - TL1: { width: 5 } - TL2: { width: 4 } - } - } - } - 4: { - mci: { - HM1: { - // :TODO: (#)Jump/(L)Index (msg list)/Last - items: [ "prev", "next", "reply", "quit", "help" ] - focusItemIndex: 1 - } - } - submit: { - *: [ - { - value: { 1: 0 } - action: @method:prevMessage - } - { - value: { 1: 1 } - action: @method:nextMessage - } - { - value: { 1: 2 } - action: @method:replyMessage - extraArgs: { - menu: messageAreaReplyPost - } - } - { - value: { 1: 3 } - action: @systemMethod:prevMenu - } - { - value: { 1: 4 } - action: @method:viewModeMenuHelp - } - ] - } - actionKeys: [ - { - keys: [ "p", "shift + p" ] - action: @method:prevMessage - } - { - keys: [ "n", "shift + n" ] - action: @method:nextMessage - } - { - keys: [ "r", "shift + r" ] - action: @method:replyMessage - extraArgs: { - menu: messageAreaReplyPost - } - } - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - { - keys: [ "?" ] - action: @method:viewModeMenuHelp - } - { - keys: [ "down arrow", "up arrow", "page up", "page down" ] - action: @method:movementKeyPressed - } - ] - } - } - } - - messageAreaReplyPost: { - module: msg_area_post_fse - config: { - art: { - header: MSGEHDR - body: MSGBODY - quote: MSGQUOT - footerEditor: MSGEFTR - footerEditorMenu: MSGEMFT - help: MSGEHLP - } - editorMode: edit - editorType: area - } - form: { - 0: { - mci: { - // :TODO: use appropriate system properties for max lengths - TL1: { - argName: from - } - ET2: { - argName: to - focus: true - validate: @systemMethod:validateNonEmpty - } - ET3: { - argName: subject - maxLength: 72 - submit: true - validate: @systemMethod:validateNonEmpty - } - TL4: { - // :TODO: this is for RE: line (NYI) - //width: 27 - //textOverflow: ... - } - } - submit: { - 3: [ - { - value: { subject: null } - action: @method:headerSubmit - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - 1: { - mci: { - MT1: { - width: 79 - height: 14 - argName: message - mode: edit - } - } - submit: { - *: [ { "value": "message", "action": "@method:editModeEscPressed" } ] - } - actionKeys: [ - { - keys: [ "escape" ], - viewId: 1 - } - ] - } - - 3: { - mci: { - HM1: { - items: [ "save", "discard", "quote", "help" ] - } - } - - submit: { - *: [ - { - value: { 1: 0 } - action: @method:editModeMenuSave - } - { - value: { 1: 1 } - action: @systemMethod:prevMenu - } - { - value: { 1: 2 }, - action: @method:editModeMenuQuote - } - { - value: { 1: 3 } - action: @method:editModeMenuHelp - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @method:editModeEscPressed - } - { - keys: [ "s", "shift + s" ] - action: @method:editModeMenuSave - } - { - keys: [ "d", "shift + d" ] - action: @systemMethod:prevMenu - } - { - keys: [ "q", "shift + q" ] - action: @method:editModeMenuQuote - } - { - keys: [ "?" ] - action: @method:editModeMenuHelp - } - ] - } - - // Quote builder - 5: { - mci: { - MT1: { - width: 79 - height: 7 - } - VM3: { - width: 79 - height: 4 - argName: quote - } - } - - submit: { - *: [ - { - value: { quote: null } - action: @method:appendQuoteEntry - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @method:quoteBuilderEscPressed - } - ] - } - } - } - // :TODO: messageAreaSelect (change msg areas -> call @systemMethod -> fallback to menu - messageAreaNewPost: { - desc: Posting message, - module: msg_area_post_fse - config: { - art: { - header: MSGEHDR - body: MSGBODY - footerEditor: MSGEFTR - footerEditorMenu: MSGEMFT - help: MSGEHLP - } - editorMode: edit - editorType: area - } - form: { - 0: { - mci: { - TL1: { - argName: from - } - ET2: { - argName: to - focus: true - text: All - validate: @systemMethod:validateNonEmpty - } - ET3: { - argName: subject - maxLength: 72 - submit: true - validate: @systemMethod:validateNonEmpty - // :TODO: Validate -> close/cancel if empty - } - } - submit: { - 3: [ - { - value: { subject: null } - action: @method:headerSubmit - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - - 1: { - "mci" : { - MT1: { - width: 79 - argName: message - mode: edit - } - } - - submit: { - *: [ { "value": "message", "action": "@method:editModeEscPressed" } ] - } - actionKeys: [ - { - keys: [ "escape" ] - viewId: 1 - } - ] - } - 2: { - TLTL: { - mci: { - TL1: { width: 5 } - TL2: { width: 4 } - } - } - } - 3: { - HM: { - mci: { - HM1: { - // :TODO: clear - "items" : [ "save", "discard", "help" ] - } - } - submit: { - *: [ - { - value: { 1: 0 } - action: @method:editModeMenuSave - } - { - value: { 1: 1 } - action: @systemMethod:prevMenu - } - { - value: { 1: 2 } - action: @method:editModeMenuHelp - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:editModeEscPressed - } - { - keys: [ "?" ] - action: @method:editModeMenuHelp - } - ] - // :TODO: something like the following for overriding keymap - // this should only override specified entries. others will default - /* - "keyMap" : { - "accept" : [ "return" ] - } - */ - } - } - } - } - - - // - // User to User mail aka Email Menu - // - mailMenu: { - art: MAILMNU - desc: Mail Menu - prompt: menuCommand - config: { - interrupt: realtime - } - submit: [ - { - value: { command: "C" } - action: @menu:mailMenuCreateMessage - } - { - value: { command: "I" } - action: @menu:mailMenuInbox - } - { - value: { command: "Q" } - action: @systemMethod:prevMenu - } - { - value: { command: "G" } - action: @menu:fullLogoffSequence - } - { - value: 1 - action: @menu:mailMenu - } - ] - } - - mailMenuCreateMessage: { - desc: Mailing Someone - module: msg_area_post_fse - config: { - art: { - header: MSGEHDR - body: MSGBODY - footerEditor: MSGEFTR - footerEditorMenu: MSGEMFT - help: MSGEHLP - }, - editorMode: edit - editorType: email - messageAreaTag: private_mail - } - form: { - 0: { - mci: { - TL1: { - argName: from - } - ET2: { - argName: to - focus: true - validate: @systemMethod:validateGeneralMailAddressedTo - } - ET3: { - argName: subject - maxLength: 72 - submit: true - validate: @systemMethod:validateMessageSubject - } - } - submit: { - 3: [ - { - value: { subject: null } - action: @method:headerSubmit - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - 1: { - mci: { - MT1: { - width: 79 - argName: message - mode: edit - } - } - - submit: { - *: [ { value: "message", action: "@method:editModeEscPressed" } ] - } - actionKeys: [ - { - keys: [ "escape" ] - viewId: 1 - } - ] - }, - 2: { - TLTL: { - mci: { - TL1: { - width: 5 - } - TL2: { - width: 4 - } - } - } - } - 3: { - HM: { - mci: { - HM1: { - // :TODO: clear - items: [ "save", "discard", "help" ] - } - } - submit: { - *: [ - { - value: { 1: 0 } - action: @method:editModeMenuSave - } - { - value: { 1: 1 } - action: @systemMethod:prevMenu - } - { - value: { 1: 2 } - action: @method:editModeMenuHelp - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:editModeEscPressed - } - { - keys: [ "?" ] - action: @method:editModeMenuHelp - } - ] - } - } - } - } - - mailMenuInbox: { - module: msg_list - art: PRVMSGLIST - config: { - menuViewPost: messageAreaViewPost - messageAreaTag: private_mail - } - form: { - 0: { // main list - mci: { - VM1: { - focus: true - submit: true - argName: message - } - } - submit: { - *: [ - { - value: { message: null } - action: @method:selectMessage - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - { - keys: [ "delete", "d", "shift + d" ] - action: @method:deleteSelected - } - ] - } - 1: { // delete prompt form - submit: { - *: [ - { - value: { promptValue: 0 } - action: @method:deleteMessageYes - } - { - value: { promptValue: 1 } - action: @method:deleteMessageNo - } - ] - } - } - } - } - - //////////////////////////////////////////////////////////////////////// - // File Base - //////////////////////////////////////////////////////////////////////// - - fileBase: { - desc: File Base - art: FMENU - prompt: fileMenuCommand - config: { - interrupt: realtime - } - submit: [ - { - value: { menuOption: "L" } - action: @menu:fileBaseListEntries - } - { - value: { menuOption: "B" } - action: @menu:fileBaseBrowseByAreaSelect - } - { - value: { menuOption: "F" } - action: @menu:fileAreaFilterEditor - } - { - value: { menuOption: "Q" } - action: @systemMethod:prevMenu - } - { - value: { menuOption: "G" } - action: @menu:fullLogoffSequence - } - { - value: { menuOption: "D" } - action: @menu:fileBaseDownloadManager - } - { - value: { menuOption: "W" } - action: @menu:fileBaseWebDownloadManager - } - { - value: { menuOption: "U" } - action: @menu:fileBaseUploadFiles - } - { - value: { menuOption: "S" } - action: @menu:fileBaseSearch - } - { - value: { menuOption: "P" } - action: @menu:fileBaseSetNewScanDate - } - { - value: { menuOption: "E" } - action: @menu:fileBaseExportListFilter - } - ] - } - - fileBaseExportListFilter: { - module: file_base_search - art: FBLISTEXPSEARCH - config: { - fileBaseListEntriesMenu: fileBaseExportList - } - form: { - 0: { - mci: { - ET1: { - focus: true - argName: searchTerms - } - BT2: { - argName: search - text: search - submit: true - } - ET3: { - maxLength: 64 - argName: tags - } - SM4: { - maxLength: 64 - argName: areaIndex - } - SM5: { - items: [ - "upload date", - "uploaded by", - "downloads", - "rating", - "estimated year", - "size", - "filename" - ] - argName: sortByIndex - } - SM6: { - items: [ - "decending", - "ascending" - ] - argName: orderByIndex - } - BT7: { - argName: advancedSearch - text: advanced search - submit: true - } - } - - submit: { - *: [ - { - value: { search: null } - action: @method:search - } - { - value: { advancedSearch: null } - action: @method:search - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseExportList: { - module: file_base_user_list_export - art: FBLISTEXP - config: { - pause: true - templates: { - entry: file_list_entry.asc - } - } - form: { - 0: { - mci: { - TL1: { } - TL2: { } - } - } - } - } - - fileBaseExportListNoResults: { - desc: Browsing Files - art: FBNORES - config: { - pause: true - menuFlags: [ "noHistory", "popParent" ] - } - } - - fileBaseSetNewScanDate: { - module: set_newscan_date - desc: File Base - art: SETFNSDATE - config: { - target: file - scanDateFormat: YYYYMMDD - } - form: { - 0: { - mci: { - ME1: { - focus: true - submit: true - argName: scanDate - maskPattern: "####/##/##" - } - } - submit: { - *: [ - { - value: { scanDate: null } - action: @method:scanDateSubmit - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseListEntries: { - module: file_area_list - desc: Browsing Files - config: { - art: { - browse: FBRWSE - details: FDETAIL - detailsGeneral: FDETGEN - detailsNfo: FDETNFO - detailsFileList: FDETLST - help: FBHELP - } - } - form: { - 0: { - mci: { - MT1: { - mode: preview - } - - HM2: { - focus: true - submit: true - argName: navSelect - items: [ - "prev", "next", "details", "toggle queue", "rate", "change filter", "help", "quit" - ] - focusItemIndex: 1 - } - } - - submit: { - *: [ - { - value: { navSelect: 0 } - action: @method:prevFile - } - { - value: { navSelect: 1 } - action: @method:nextFile - } - { - value: { navSelect: 2 } - action: @method:viewDetails - } - { - value: { navSelect: 3 } - action: @method:toggleQueue - } - { - value: { navSelect: 4 } - action: @menu:fileBaseGetRatingForSelectedEntry - } - { - value: { navSelect: 5 } - action: @menu:fileAreaFilterEditor - } - { - value: { navSelect: 6 } - action: @method:displayHelp - } - { - value: { navSelect: 7 } - action: @systemMethod:prevMenu - } - ] - } - - actionKeys: [ - { - keys: [ "w", "shift + w" ] - action: @method:showWebDownloadLink - } - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - { - keys: [ "t", "shift + t" ] - action: @method:toggleQueue - } - { - keys: [ "f", "shift + f" ] - action: @menu:fileAreaFilterEditor - } - { - keys: [ "v", "shift + v" ] - action: @method:viewDetails - } - { - keys: [ "r", "shift + r" ] - action: @menu:fileBaseGetRatingForSelectedEntry - } - { - keys: [ "?" ] - action: @method:displayHelp - } - ] - } - - 1: { - mci: { - HM1: { - focus: true - submit: true - argName: navSelect - items: [ - "general", "nfo/readme", "file listing" - ] - } - } - - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @method:detailsQuit - } - ] - } - - 2: { - // details - general - mci: {} - } - - 3: { - // details - nfo/readme - mci: { - MT1: { - mode: preview - } - } - } - - 4: { - // details - file listing - mci: { - VM1: { - - } - } - } - } - } - - fileBaseBrowseByAreaSelect: { - desc: Browsing File Areas - module: file_base_area_select - art: FAREASEL - form: { - 0: { - mci: { - VM1: { - focus: true - argName: areaTag - } - } - - submit: { - *: [ - { - value: { areaTag: null } - action: @method:selectArea - } - ] - } - - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseGetRatingForSelectedEntry: { - desc: Rating a File - prompt: fileBaseRateEntryPrompt - config: { - cls: true - } - submit: [ - // :TODO: handle esc/q - { - // pass data back to caller - value: { rating: null } - action: @systemMethod:prevMenu - } - ] - } - - fileBaseListEntriesNoResults: { - desc: Browsing Files - art: FBNORES - config: { - pause: true - menuFlags: [ "noHistory", "popParent" ] - } - } - - fileBaseSearch: { - module: file_base_search - desc: Searching Files - art: FSEARCH - form: { - 0: { - mci: { - ET1: { - focus: true - argName: searchTerms - } - BT2: { - argName: search - text: search - submit: true - } - ET3: { - maxLength: 64 - argName: tags - } - SM4: { - maxLength: 64 - argName: areaIndex - } - SM5: { - items: [ - "upload date", - "uploaded by", - "downloads", - "rating", - "estimated year", - "size", - "filename", - ] - argName: sortByIndex - } - SM6: { - items: [ - "decending", - "ascending" - ] - argName: orderByIndex - } - BT7: { - argName: advancedSearch - text: advanced search - submit: true - } - } - - submit: { - *: [ - { - value: { search: null } - action: @method:search - } - { - value: { advancedSearch: null } - action: @method:search - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileAreaFilterEditor: { - desc: File Filter Editor - module: file_area_filter_edit - art: FFILEDT - form: { - 0: { - mci: { - ET1: { - argName: searchTerms - } - ET2: { - maxLength: 64 - argName: tags - } - SM3: { - maxLength: 64 - argName: areaIndex - } - SM4: { - items: [ - "upload date", - "uploaded by", - "downloads", - "rating", - "estimated year", - "size", - ] - argName: sortByIndex - } - SM5: { - items: [ - "decending", - "ascending" - ] - argName: orderByIndex - } - ET6: { - maxLength: 64 - argName: name - validate: @systemMethod:validateNonEmpty - } - HM7: { - focus: true - items: [ - "prev", "next", "make active", "save", "new", "delete" - ] - argName: navSelect - focusItemIndex: 1 - } - } - - submit: { - *: [ - { - value: { navSelect: 0 } - action: @method:prevFilter - } - { - value: { navSelect: 1 } - action: @method:nextFilter - } - { - value: { navSelect: 2 } - action: @method:makeFilterActive - } - { - value: { navSelect: 3 } - action: @method:saveFilter - } - { - value: { navSelect: 4 } - action: @method:newFilter - } - { - value: { navSelect: 5 } - action: @method:deleteFilter - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseDownloadManager: { - desc: Download Manager - module: file_base_download_manager - config: { - art: { - queueManager: FDLMGR - /* - NYI - details: FDLDET - */ - } - emptyQueueMenu: fileBaseDownloadManagerEmptyQueue - } - form: { - 0: { - mci: { - VM1: { - argName: queueItem - } - HM2: { - focus: true - items: [ "download all", "quit" ] - argName: navSelect - } - } - - submit: { - *: [ - { - value: { navSelect: 0 } - action: @method:downloadAll - } - { - value: { navSelect: 1 } - action: @systemMethod:prevMenu - } - ] - } - - actionKeys: [ - { - keys: [ "a", "shift + a" ] - action: @method:downloadAll - } - { - keys: [ "delete", "r", "shift + r" ] - action: @method:removeItem - } - { - keys: [ "c", "shift + c" ] - action: @method:clearQueue - } - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseWebDownloadManager: { - desc: Web D/L Manager - module: file_base_web_download_manager - config: { - art: { - queueManager: FWDLMGR - batchList: BATDLINF - } - emptyQueueMenu: fileBaseDownloadManagerEmptyQueue - } - form: { - 0: { - mci: { - VM1: { - argName: queueItem - } - HM2: { - focus: true - items: [ "get batch link", "quit", "help" ] - argName: navSelect - } - } - - submit: { - *: [ - { - value: { navSelect: 0 } - action: @method:getBatchLink - } - { - value: { navSelect: 1 } - action: @systemMethod:prevMenu - } - ] - } - - actionKeys: [ - { - keys: [ "b", "shift + b" ] - action: @method:getBatchLink - } - { - keys: [ "delete", "r", "shift + r" ] - action: @method:removeItem - } - { - keys: [ "c", "shift + c" ] - action: @method:clearQueue - } - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseDownloadManagerEmptyQueue: { - desc: Empty Download Queue - art: FEMPTYQ - config: { - pause: true - menuFlags: [ "noHistory", "popParent" ] - } - } - - fileTransferProtocolSelection: { - desc: Protocol selection - module: file_transfer_protocol_select - art: FPROSEL - form: { - 0: { - mci: { - VM1: { - focus: true - argName: protocol - } - } - - submit: { - *: [ - { - value: { protocol: null } - action: @method:selectProtocol - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseUploadFiles: { - desc: Uploading - module: upload - config: { - interrupt: never - art: { - options: ULOPTS - fileDetails: ULDETAIL - processing: ULCHECK - dupes: ULDUPES - } - } - - form: { - // options - 0: { - mci: { - SM1: { - argName: areaSelect - focus: true - } - TM2: { - argName: uploadType - items: [ "blind", "supply filename" ] - } - ET3: { - argName: fileName - maxLength: 255 - validate: @method:validateNonBlindFileName - } - HM4: { - argName: navSelect - items: [ "continue", "cancel" ] - submit: true - } - } - - submit: { - *: [ - { - value: { navSelect: 0 } - action: @method:optionsNavContinue - } - { - value: { navSelect: 1 } - action: @systemMethod:prevMenu - } - ] - } - - "actionKeys" : [ - { - "keys" : [ "escape" ], - action: @systemMethod:prevMenu - } - ] - } - - 1: { - mci: { } - } - - // file details entry - 2: { - mci: { - MT1: { - argName: shortDesc - tabSwitchesView: true - focus: true - } - - ET2: { - argName: tags - } - - ME3: { - argName: estYear - maskPattern: "####" - } - - BT4: { - argName: continue - text: continue - submit: true - } - } - - submit: { - *: [ - { - value: { continue: null } - action: @method:fileDetailsContinue - } - ] - } - } - - // dupes - 3: { - mci: { - VM1: { - /* - Use 'dupeInfoFormat' to custom format: - - areaDesc - areaName - areaTag - desc - descLong - fileId - fileName - fileSha256 - storageTag - uploadTimestamp - - */ - - mode: preview - } - } - } - } - } - - fileBaseNoUploadAreasAvail: { - desc: File Base - art: ULNOAREA - config: { - pause: true - menuFlags: [ "noHistory", "popParent" ] - } - } - - sendFilesToUser: { - desc: Downloading - module: file_transfer - config: { - // defaults - generally use extraArgs - protocol: zmodem8kSexyz - direction: send - } - } - - recvFilesFromUser: { - desc: Uploading - module: file_transfer - config: { - // defaults - generally use extraArgs - protocol: zmodem8kSexyz - direction: recv - } - } - - - //////////////////////////////////////////////////////////////////////// - // Required entries - //////////////////////////////////////////////////////////////////////// - idleLogoff: { - art: IDLELOG - next: @systemMethod:logoff - } - //////////////////////////////////////////////////////////////////////// - // Demo Section - // :TODO: This entire section needs updated!!! - //////////////////////////////////////////////////////////////////////// - "demoMain" : { - "art" : "demo_selection_vm.ans", - "form" : { - "0" : { - "VM" : { - "mci" : { - "VM1" : { - "items" : [ - "Single Line Text Editing Views", - "Spinner & Toggle Views", - "Mask Edit Views", - "Multi Line Text Editor", - "Vertical Menu Views", - "Horizontal Menu Views", - "Art Display", - "Full Screen Editor" - ], - "height" : 10, - "itemSpacing" : 1, - "justify" : "center", - "focusTextStyle" : "small i" - } - }, - "submit" : { - "*" : [ - { - "value" : { "1" : 0 }, - "action" : "@menu:demoEditTextView" - }, - { - "value" : { "1" : 1 }, - "action" : "@menu:demoSpinAndToggleView" - }, - { - "value" : { "1" : 2 }, - "action" : "@menu:demoMaskEditView" - }, - { - "value" : { "1" : 3 }, - "action" : "@menu:demoMultiLineEditTextView" - }, - { - "value" : { "1" : 4 }, - "action" : "@menu:demoVerticalMenuView" - }, - { - "value" : { "1" : 5 }, - "action" : "@menu:demoHorizontalMenuView" - }, - { - "value" : { "1" : 6 }, - "action" : "@menu:demoArtDisplay" - }, - { - "value" : { "1" : 7 }, - "action" : "@menu:demoFullScreenEditor" - } - ] - } - } - } - } - }, - "demoEditTextView" : { - "art" : "demo_edit_text_view1.ans", - "form" : { - "0" : { - "BTETETETET" : { - "mci" : { - "ET1" : { - "width" : 20, - "maxLength" : 20 - }, - "ET2" : { - "width" : 20, - "maxLength" : 40, - "textOverflow" : "..." - }, - "ET3" : { - "width" : 20, - "fillChar" : "-", - "styleSGR1" : "|00|36", - "maxLength" : 20 - }, - "ET4" : { - "width" : 20, - "maxLength" : 20, - "password" : true - }, - "BT5" : { - "width" : 8, - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 5, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 5 - } - ] - } - } - } - }, - "demoSpinAndToggleView" : { - "art" : "demo_spin_and_toggle.ans", - "form" : { - "0" : { - "BTSMSMTM" : { - "mci" : { - "SM1" : { - "items" : [ "Henry Morgan", "François l'Ollonais", "Roche Braziliano", "Black Bart", "Blackbeard" ] - }, - "SM2" : { - "items" : [ "Razor 1911", "DrinkOrDie", "TRSI" ] - }, - "TM3" : { - "items" : [ "Yarly", "Nowaii" ], - "styleSGR1" : "|00|30|01", - "hotKeys" : { "Y" : 0, "N" : 1 } - }, - "BT8" : { - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 8, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 8 - } - ] - } - } - } - }, - "demoMaskEditView" : { - "art" : "demo_mask_edit_text_view1.ans", - "form" : { - "0" : { - "BTMEME" : { - "mci" : { - "ME1" : { - "maskPattern" : "##/##/##", - "styleSGR1" : "|00|30|01", - //"styleSGR2" : "|00|45|01", - "styleSGR3" : "|00|30|35", - "fillChar" : "#" - }, - "BT5" : { - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 5, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 5 - } - ] - } - } - } - }, - "demoMultiLineEditTextView" : { - "art" : "demo_multi_line_edit_text_view1.ans", - "form" : { - "0" : { - "BTMT" : { - "mci" : { - "MT1" : { - "width" : 70, - "height" : 17, - //"text" : "@art:demo_multi_line_edit_text_view_text.txt", - // "text" : "@systemMethod:textFromFile" - text: "Hints:\n\t* Insert / CTRL-V toggles overtype mode\n\t* CTRL-Y deletes the current line\n\t* Try Page Up / Page Down\n\t* Home goes to the start of line text\n\t* End goes to the end of a line\n\n\nTab handling:\n-------------------------------------------------\n\tA\tB\tC\tD\tE\tF\nA\tB\tC\tD\tE\tF\tG\tH\n\tA\tB\tC\tD\tE\tF\nA\tB\tC\tD\tE\tF\tG\tH\nA0\tBB\t1\tCCC\t2\tDDD\t3EEEE\nW\t\tX\t\tY\t\tZ\n\nAn excerpt from A Clockwork Orange:\n\"What sloochatted then, of course, was that my cellmates woke up and started joining in, tolchocking a bit wild in the near-dark, and the shoom seemed to wake up the whole tier, so that you could slooshy a lot of creeching and banging about with tin mugs on the wall, as though all the plennies in all the cells thought a big break was about to commence, O my brothers.\n", - "focus" : true - }, - "BT5" : { - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 5, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 5 - } - ] - } - } - } - }, - "demoHorizontalMenuView" : { - "art" : "demo_horizontal_menu_view1.ans", - "form" : { - "0" : { - "BTHMHM" : { - "mci" : { - "HM1" : { - "items" : [ "One", "Two", "Three" ], - "hotKeys" : { "1" : 0, "2" : 1, "3" : 2 } - }, - "HM2" : { - "items" : [ "Uno", "Dos", "Tres" ], - "hotKeys" : { "U" : 0, "D" : 1, "T" : 2 } - }, - "BT5" : { - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 5, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 5 - } - ] - } - } - } - }, - "demoVerticalMenuView" : { - "art" : "demo_vertical_menu_view1.ans", - "form" : { - "0" : { - "BTVM" : { - "mci" : { - "VM1" : { - "items" : [ - "|33Oblivion/2", - "|33iNiQUiTY", - "|33ViSiON/X" - ], - "focusItems" : [ - "|33Oblivion|01/|00|332", - "|01|33i|00|33N|01i|00|33QU|01i|00|33TY", - "|33ViSiON/X" - ] - // - // :TODO: how to do the following: - // 1) Supply a view a string for a standard vs focused item - // "items" : [...], "focusItems" : [ ... ] ? - // "draw" : "@method:drawItemX", then items: [...] - }, - "BT5" : { - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 5, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 5 - } - ] - } - } - } - - }, - "demoArtDisplay" : { - "art" : "demo_selection_vm.ans", - "form" : { - "0" : { - "VM" : { - "mci" : { - "VM1" : { - "items" : [ - "Defaults - DOS ANSI", - "bw_mindgames.ans - DOS", - "test.ans - DOS", - "Defaults - Amiga", - "Pause at Term Height" - ], - // :TODO: justify not working?? - "focusTextStyle" : "small i" - } - }, - "submit" : { - "*" : [ - { - "value" : { "1" : 0 }, - "action" : "@menu:demoDefaultsDosAnsi" - }, - { - "value" : { "1" : 1 }, - "action" : "@menu:demoDefaultsDosAnsi_bw_mindgames" - }, - { - "value" : { "1" : 2 }, - "action" : "@menu:demoDefaultsDosAnsi_test" - } - ] - } - } - } - } - }, - "demoDefaultsDosAnsi" : { - "art" : "DM-ENIG2.ANS" - }, - "demoDefaultsDosAnsi_bw_mindgames" : { - "art" : "bw_mindgames.ans" - }, - "demoDefaultsDosAnsi_test" : { - "art" : "test.ans" - }, - "demoFullScreenEditor" : { - "module" : "fse", - "config" : { - "editorType" : "netMail", - "art" : { - "header" : "demo_fse_netmail_header.ans", - "body" : "demo_fse_netmail_body.ans", - "footerEditor" : "demo_fse_netmail_footer_edit.ans", - "footerEditorMenu" : "demo_fse_netmail_footer_edit_menu.ans", - "footerView" : "demo_fse_netmail_footer_view.ans", - "help" : "demo_fse_netmail_help.ans" - } - }, - "form" : { - "0" : { - "ETETET" : { - "mci" : { - "ET1" : { - // :TODO: from/to may be set by args - // :TODO: focus may change dep on view vs edit - "width" : 36, - "focus" : true, - "argName" : "to" - }, - "ET2" : { - "width" : 36, - "argName" : "from" - }, - "ET3" : { - "width" : 65, - "maxLength" : 72, - "submit" : [ "enter" ], - "argName" : "subject" - } - }, - "submit" : { - "3" : [ - { - "value" : { "subject" : null }, - "action" : "@method:headerSubmit" - } - ] - } - } - }, - "1" : { - "MT" : { - "mci" : { - "MT1" : { - "width" : 79, - "height" : 17, - "text" : "", // :TODO: should not be req. - "argName" : "message" - } - }, - "submit" : { - "*" : [ - { - "value" : "message", - "action" : "@method:editModeEscPressed" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 1 - } - ] - } - }, - "2" : { - "TLTL" : { - "mci" : { - "TL1" : { - "width" : 5 - }, - "TL2" : { - "width" : 4 - } - } - } - }, - "3" : { - "HM" : { - "mci" : { - "HM1" : { - // :TODO: Continue, Save, Discard, Clear, Quote, Help - "items" : [ "Save", "Discard", "Quote", "Help" ] - } - }, - "submit" : { - "*" : [ - { - "value" : { "1" : 0 }, - "action" : "@method:editModeMenuSave" - }, - { - "value" : { "1" : 1 }, - "action" : "@menu:demoMain" - }, - { - "value" : { "1" : 2 }, - "action" : "@method:editModeMenuQuote" - }, - { - "value" : { "1" : 3 }, - "action" : "@method:editModeMenuHelp" - }, - { - "value" : 1, - "action" : "@method:editModeEscPressed" - } - ] - }, - "actionKeys" : [ // :TODO: Need better name - { - "keys" : [ "escape" ], - "action" : "@method:editModeEscPressed" - } - ] - } - } - } - } - } -} From 094385a1507b8bed31c5faf79c2eb5bdd8c567f2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 7 Jun 2019 23:23:06 -0600 Subject: [PATCH 063/140] Minor bug fixes and improvements * Color help text a bit * Split lines for buffer so padding works - may need to do elsewhere also * Fix a couple crashes --- core/mrc.js | 116 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 69 insertions(+), 47 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index 6fd8ac35..36475d70 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -32,7 +32,7 @@ const FormIds = { mrcChat : 0, }; -var MciViewIds = { +const MciViewIds = { mrcChat : { chatLog : 1, inputArea : 2, @@ -48,19 +48,20 @@ var MciViewIds = { // TODO: this is a bit shit, could maybe do it with an ansi instead const helpText = ` -General Chat: -/rooms & /join - Send a private message +|15General Chat|08: +|03/|11rooms |08& |03/|11join |03 |08- |07List all or join a room +|03/|11pm |03 |08- |07Send a private message ---- -/whoon - Who's on what BBS -/chatters - Who's in what room -/topic - Set the room topic -/bbses & /info - Info about BBS's connected -/meetups - Info about MRC MeetUps +|03/|11whoon |08- |07Who's on what BBS +|03/|11chatters |08- |07Who's in what room +|03/|11clear |08- |07Clear back buffer +|03/|11topic |08- |07Set the room topic +|03/|11bbses & |03/|11info |08- |07Info about BBS's connected +|03/|11meetups |08- |07Info about MRC MeetUps --- -/l33t - l337 5p34k -/kewl - BBS KeWL SPeaK -/rainbow - Crazy rainbow text +|03/|11l33t |08- |07l337 5p34k +|03/|11kewl |08- |07BBS KeWL SPeaK +|03/|11rainbow |08- |07Crazy rainbow text `; @@ -94,7 +95,7 @@ exports.getModule = class mrcModule extends MenuModule { }, movementKeyPressed : (formData, extraArgs, cb) => { - const bodyView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); // :TODO: use const here vs magic # + const bodyView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); switch(formData.key.name) { case 'down arrow' : bodyView.scrollDocumentUp(); break; case 'up arrow' : bodyView.scrollDocumentDown(); break; @@ -108,9 +109,6 @@ exports.getModule = class mrcModule extends MenuModule { }, quit : (formData, extraArgs, cb) => { - this.sendServerMessage('LOGOFF'); - clearInterval(this.heartbeat); - this.state.socket.destroy(); return this.prevMenu(cb); } }; @@ -138,16 +136,19 @@ exports.getModule = class mrcModule extends MenuModule { // connect to multiplexer this.state.socket = net.createConnection(connectOpts, () => { - const self = this; - // handshake with multiplexer - self.state.socket.write(`--DUDE-ITS--|${self.state.alias}\n`); + this.client.once('end', () => { + this.quitServer(); + }); - self.clientConnect(); + // handshake with multiplexer + this.state.socket.write(`--DUDE-ITS--|${this.state.alias}\n`); + + this.clientConnect(); // send register to central MRC and get stats every 60s - self.heartbeat = setInterval(function () { - self.sendHeartbeat(); - self.sendServerMessage('STATS'); + this.heartbeat = setInterval( () => { + this.sendHeartbeat(); + this.sendServerMessage('STATS'); }, 60000); }); @@ -167,26 +168,46 @@ exports.getModule = class mrcModule extends MenuModule { }); } + leave() { + this.quitServer(); + return super.leave(); + } + + quitServer() { + clearInterval(this.heartbeat); + + if(this.state.socket) { + this.sendServerMessage('LOGOFF'); + this.state.socket.destroy(); + } + } + /** * Adds a message to the chat log on screen */ addMessageToChatLog(message) { - const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); - const messageLength = stripMciColorCodes(message).length; - const chatWidth = chatLogView.dimens.width; - let padAmount = 0; - let spaces = 2; - - if (messageLength > chatWidth) { - padAmount = chatWidth - (messageLength % chatWidth) - spaces; - } else { - padAmount = chatWidth - messageLength - spaces ; + if(!Array.isArray(message)) { + message = [ message ]; } - if (padAmount < 0) padAmount = 0; + message.forEach(msg => { + const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); + const messageLength = stripMciColorCodes(msg).length; + const chatWidth = chatLogView.dimens.width; + let padAmount = 0; + let spaces = 2; - const padding = ' |00' + ' '.repeat(padAmount); - chatLogView.addText(pipeToAnsi(message + padding)); + if (messageLength > chatWidth) { + padAmount = chatWidth - (messageLength % chatWidth) - spaces; + } else { + padAmount = chatWidth - messageLength - spaces; + } + + if (padAmount < 0) padAmount = 0; + + const padding = ' |00' + ' '.repeat(padAmount); + chatLogView.addText(pipeToAnsi(msg + padding)); + }); } /** @@ -267,7 +288,7 @@ exports.getModule = class mrcModule extends MenuModule { const messageFormat = this.config.messageFormat || '|00|10<|02{fromUserName}|10>|00 |03{message}|00'; - + const privateMessageFormat = this.config.outgoingPrivateMessageFormat || '|00|10<|02{fromUserName}|10|14->|02{toUserName}>|00 |03{message}|00'; @@ -280,7 +301,7 @@ exports.getModule = class mrcModule extends MenuModule { // pm formattedMessage = stringFormat(privateMessageFormat, textFormatObj); } - + try { this.sendMessageToMultiplexer(to_user || '', '', this.state.room, formattedMessage); } catch(e) { @@ -366,7 +387,7 @@ exports.getModule = class mrcModule extends MenuModule { break; case '?': - this.addMessageToChatLog(helpText); + this.addMessageToChatLog(helpText.split(/\n/g)); break; default: @@ -385,16 +406,17 @@ exports.getModule = class mrcModule extends MenuModule { sendMessageToMultiplexer(to_user, to_site, to_room, body) { const message = { - from_user: this.state.alias, - from_room: this.state.room, - to_user: to_user, - to_site: to_site, - to_room: to_room, - body: body + to_user, + to_site, + to_room, + body, + from_user : this.state.alias, + from_room : this.state.room, }; - // TODO: check socket still exists here - this.state.socket.write(JSON.stringify(message) + '\n'); + if(this.state.socket) { + this.state.socket.write(JSON.stringify(message) + '\n'); + } } /** From afb7854ea50f4bdf00d337d6045a094db0cf4281 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 8 Jun 2019 11:57:36 -0600 Subject: [PATCH 064/140] Clean up logging a bit & implement 'omit' * 'omit' can be set to omit views from form submission * Don't log as much noise --- core/view.js | 6 ++++++ core/view_controller.js | 23 +++++++++-------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/core/view.js b/core/view.js index 7d3c5693..58a305e4 100644 --- a/core/view.js +++ b/core/view.js @@ -220,6 +220,12 @@ View.prototype.setPropertyValue = function(propName, value) { case 'argName' : this.submitArgName = value; break; + case 'omit' : + if(_.isBoolean(value)) { + this.omitFromSubmission = value; break; + } + break; + case 'validate' : if(_.isFunction(value)) { this.validate = value; diff --git a/core/view_controller.js b/core/view_controller.js index 84f33756..9c0ffe19 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -46,6 +46,8 @@ function ViewController(options) { return; // ignore until this is finished! } + self.client.log.trace( { actionBlock }, 'Action match' ); + self.waitActionCompletion = true; menuUtil.handleAction(self.client, formData, actionBlock, (err) => { if(err) { @@ -121,9 +123,7 @@ function ViewController(options) { self.emit('submit', this.getFormData(key)); }; - // :TODO: replace this in favor of overriding toJSON() for various things such that logging will *never* output them this.getLogFriendlyFormData = function(formData) { - // :TODO: these fields should be part of menu.json sensitiveMembers[] var safeFormData = _.cloneDeep(formData); if(safeFormData.value.password) { safeFormData.value.password = '*****'; @@ -330,15 +330,6 @@ function ViewController(options) { } } } - - self.client.log.trace( - { - formValue : formValue, - actionValue : actionValue - }, - 'Action match' - ); - return true; }; @@ -577,7 +568,7 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) { if(false === self.noInput) { self.on('submit', function promptSubmit(formData) { - self.client.log.trace( { formData : self.getLogFriendlyFormData(formData) }, 'Prompt submit'); + self.client.log.trace( { formData }, 'Prompt submit'); const doSubmitNotify = () => { if(options.submitNotify) { @@ -752,8 +743,7 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { } self.on('submit', function formSubmit(formData) { - - self.client.log.trace( { formData : self.getLogFriendlyFormData(formData) }, 'Form submit'); + self.client.log.trace( { formData }, 'Form submit'); // // Locate configuration for this form ID @@ -870,6 +860,11 @@ ViewController.prototype.getFormData = function(key) { return; } + // some form values may be omitted from submission all together + if(view.omitFromSubmission) { + return; + } + viewData = view.getData(); if(_.isUndefined(viewData)) { return; From f2e769a27ab2f26d75505ada456f1ee3ad6dc106 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 8 Jun 2019 11:58:21 -0600 Subject: [PATCH 065/140] Fix up colors, clear out socket --- core/mrc.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index 36475d70..f2bfeec6 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -55,13 +55,13 @@ const helpText = ` |03/|11whoon |08- |07Who's on what BBS |03/|11chatters |08- |07Who's in what room |03/|11clear |08- |07Clear back buffer -|03/|11topic |08- |07Set the room topic -|03/|11bbses & |03/|11info |08- |07Info about BBS's connected +|03/|11topic |03 |08- |07Set the room topic +|03/|11bbses |08& |03/|11info |08- |07Info about BBS's connected |03/|11meetups |08- |07Info about MRC MeetUps --- -|03/|11l33t |08- |07l337 5p34k -|03/|11kewl |08- |07BBS KeWL SPeaK -|03/|11rainbow |08- |07Crazy rainbow text +|03/|11l33t |03 |08- |07l337 5p34k +|03/|11kewl |03 |08- |07BBS KeWL SPeaK +|03/|11rainbow |03 |08- |07Crazy rainbow text `; @@ -179,6 +179,7 @@ exports.getModule = class mrcModule extends MenuModule { if(this.state.socket) { this.sendServerMessage('LOGOFF'); this.state.socket.destroy(); + delete this.state.socket; } } From 33969448204ed7bd2e31cb1ed6083632a96a425e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 8 Jun 2019 11:58:28 -0600 Subject: [PATCH 066/140] Note on 'omit' --- WHATSNEW.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index 7137038a..d64f7d1d 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -32,7 +32,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * Performing a file scan/import using `oputil.js fb scan` now recognizes various `FILES.BBS` formats. * Usernames found in the `config.users.badUserNames` are now not only disallowed from applying, but disconnected at any login attempt. * Total minutes online is now tracked for users. Of course, it only starts after you get the update :) - +* Form entries in `menu.hjson` can now be omitted from submission handlers using `omit: true` ## 0.0.8-alpha * [Mystic BBS style](http://wiki.mysticbbs.com/doku.php?id=displaycodes) extended pipe color codes. These allow for example, to set "iCE" background colors. From ad305b4cccd6086befa758b793db5b9315d85b7e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 8 Jun 2019 12:12:29 -0600 Subject: [PATCH 067/140] Implement 'maxScrollbackLines' config for MRC module --- core/mrc.js | 6 ++++++ misc/menu_template.in.hjson | 3 +++ 2 files changed, 9 insertions(+) diff --git a/core/mrc.js b/core/mrc.js index f2bfeec6..3ab1e91b 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -72,6 +72,8 @@ exports.getModule = class mrcModule extends MenuModule { this.log = Log.child( { module : 'MRC' } ); this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); + this.config.maxScrollbackLines = this.config.maxScrollbackLines || 500; + this.state = { socket: '', alias: this.client.user.username, @@ -208,6 +210,10 @@ exports.getModule = class mrcModule extends MenuModule { const padding = ' |00' + ' '.repeat(padAmount); chatLogView.addText(pipeToAnsi(msg + padding)); + + if(chatLogView.getLineCount() > this.config.maxScrollbackLines) { + chatLogView.deleteLine(0); + } }); } diff --git a/misc/menu_template.in.hjson b/misc/menu_template.in.hjson index 9ae54d17..5c7eba83 100644 --- a/misc/menu_template.in.hjson +++ b/misc/menu_template.in.hjson @@ -1104,6 +1104,9 @@ art: MRC config: { cls: true + + // max lines kept in scrollback buffer + maxScrollbackLines: 500 } form: { 0: { From 433ad72752e26803d610c4c5eb693663cfc73d52 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 9 Jun 2019 00:09:07 -0600 Subject: [PATCH 068/140] Fixes & /clear * Fix use port from config * Add /clear & menu method: clearMessages() --- core/mrc.js | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index 3ab1e91b..c80adf80 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -10,6 +10,7 @@ const { } = require('./color_codes.js'); const stringFormat = require('./string_format.js'); const StringUtil = require('./string_util.js'); +const Config = require('./config.js').get; // deps const _ = require('lodash'); @@ -112,6 +113,11 @@ exports.getModule = class mrcModule extends MenuModule { quit : (formData, extraArgs, cb) => { return this.prevMenu(cb); + }, + + clearMessages : (formData, extraArgs, cb) => { + this.clearMessages(); + return cb(null); } }; } @@ -132,7 +138,7 @@ exports.getModule = class mrcModule extends MenuModule { }, (callback) => { const connectOpts = { - port : 5000, + port : _.get(Config(), 'chatServers.mrc.serverPort', 5000), host : 'localhost', }; @@ -160,6 +166,15 @@ exports.getModule = class mrcModule extends MenuModule { this.processReceivedMessage(data); }); + this.state.socket.once('error', err => { + this.log.warn( { error : err.message }, 'MRC multiplexer socket error' ); + this.state.socket.destroy(); + delete this.state.socket; + + // bail with error - fall back to prev menu + return callback(err); + }); + return(callback); } ], @@ -389,8 +404,11 @@ exports.getModule = class mrcModule extends MenuModule { this.sendServerMessage('LIST'); break; + case 'quit' : + return this.prevMenu(); + case 'clear': - chatLogView.setText(''); + this.clearMessages(); break; case '?': @@ -404,7 +422,11 @@ exports.getModule = class mrcModule extends MenuModule { // just do something to get the cursor back to the right place ¯\_(ツ)_/¯ this.sendServerMessage('STATS'); + } + clearMessages() { + const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); + chatLogView.setText(''); } /** From 487968dac94e7453d317e4647f772e30c6208047 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 9 Jun 2019 09:19:34 -0600 Subject: [PATCH 069/140] Fix my previous dumb --- core/mrc.js | 2 +- core/servers/chat/mrc_multiplexer.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index c80adf80..6987be32 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -138,7 +138,7 @@ exports.getModule = class mrcModule extends MenuModule { }, (callback) => { const connectOpts = { - port : _.get(Config(), 'chatServers.mrc.serverPort', 5000), + port : _.get(Config(), 'chatServers.mrc.multiplexerPort', 5000), host : 'localhost', }; diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js index 9b976eff..fd83d462 100644 --- a/core/servers/chat/mrc_multiplexer.js +++ b/core/servers/chat/mrc_multiplexer.js @@ -104,7 +104,7 @@ exports.getModule = class MrcModule extends ServerModule { buffer = Buffer.concat([buffer, chunk]); } - var lines = buffer.toString().split(lineDelimiter); + let lines = buffer.toString().split(lineDelimiter); if (lines.pop()) { // if buffer is not ended with \r\n, there's more chunks. From 5c978e05bf468ffa7b10deb78ef113a5ac570af7 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 9 Jun 2019 13:41:11 -0600 Subject: [PATCH 070/140] Ability to override idle time and/or temporary disable from MRC --- core/client.js | 21 ++++++++++++++++++++- core/mrc.js | 16 ++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/core/client.js b/core/client.js index d340981b..76d56529 100644 --- a/core/client.js +++ b/core/client.js @@ -439,6 +439,11 @@ Client.prototype.setTermType = function(termType) { }; Client.prototype.startIdleMonitor = function() { + // clear existing, if any + if(this.idleCheck) { + this.stopIdleMonitor(); + } + this.lastKeyPressMs = Date.now(); // @@ -474,6 +479,9 @@ Client.prototype.startIdleMonitor = function() { idleLogoutSeconds = Config().users.preAuthIdleLogoutSeconds; } + // use override value if set + idleLogoutSeconds = this.idleLogoutSecondsOverride || idleLogoutSeconds; + if(nowMs - this.lastKeyPressMs >= (idleLogoutSeconds * 1000)) { this.emit('idle timeout'); } @@ -481,7 +489,18 @@ Client.prototype.startIdleMonitor = function() { }; Client.prototype.stopIdleMonitor = function() { - clearInterval(this.idleCheck); + if(this.idleCheck) { + clearInterval(this.idleCheck); + delete this.idleCheck; + } +}; + +Client.prototype.overrideIdleLogoutSeconds = function(seconds) { + this.idleLogoutSecondsOverride = seconds; +}; + +Client.prototype.restoreIdleLogoutSeconds = function() { + delete this.idleLogoutSecondsOverride; }; Client.prototype.end = function () { diff --git a/core/mrc.js b/core/mrc.js index 6987be32..96637dfa 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -158,6 +158,16 @@ exports.getModule = class mrcModule extends MenuModule { this.sendHeartbeat(); this.sendServerMessage('STATS'); }, 60000); + + // override idle logout seconds if configured + const idleLogoutSeconds = parseInt(this.config.idleLogoutSeconds); + if(0 === idleLogoutSeconds) { + this.log.debug('Temporary disable idle monitor due to config'); + this.client.stopIdleMonitor(); + } else if (!isNaN(idleLogoutSeconds) && idleLogoutSeconds >= 60) { + this.log.debug( { idleLogoutSeconds }, 'Temporary override idle logout seconds due to config'); + this.client.overrideIdleLogoutSeconds(idleLogoutSeconds); + } }); // when we get data, process it @@ -187,6 +197,12 @@ exports.getModule = class mrcModule extends MenuModule { leave() { this.quitServer(); + + // restore idle monitor to previous state + this.log.debug('Restoring idle monitor to previous state'); + this.client.restoreIdleLogoutSeconds(); + this.client.startIdleMonitor(); + return super.leave(); } From da93fd53e9de052d0e9845ae6879a97853674674 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 9 Jun 2019 22:36:24 -0600 Subject: [PATCH 071/140] Start of custom format obj stuff. Want to add more info --- core/mrc.js | 50 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index 96637dfa..8832d103 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -41,7 +41,8 @@ const MciViewIds = { roomTopic : 4, mrcUsers : 5, mrcBbses : 6, - customRangeStart : 10, // 10+ = customs + + customRangeStart : 20, // 20+ = customs } }; @@ -84,6 +85,16 @@ exports.getModule = class mrcModule extends MenuModule { last_ping: 0 }; + this.customFormatObj = { + roomName : '', + roomTopic : '', + roomUserCount : 0, + userCount : 0, + boardCount : 0, + roomCount : 0, + //latencyMs : 0, + }; + this.menuMethods = { sendChatMessage : (formData, extraArgs, cb) => { @@ -269,19 +280,33 @@ exports.getModule = class mrcModule extends MenuModule { break; case 'ROOMTOPIC': - this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomName).setText(`#${params[1]}`); - this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomTopic).setText(pipeToAnsi(params[2])); + this.setText(MciViewIds.mrcChat.roomName, `#${params[1]}`); + this.setText(MciViewIds.mrcChat.roomTopic, params[2]); + + this.customFormatObj.roomName = params[1]; + this.customFormatObj.roomTopic = params[2]; + this.updateCustomViews(); + this.state.room = params[1]; break; case 'USERLIST': this.state.nicks = params[1].split(','); + + this.customFormatObj.roomUserCount = this.state.nicks.length; + this.updateCustomViews(); break; case 'STATS': { const stats = params[1].split(' '); - this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.mrcUsers).setText(stats[2]); - this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.mrcBbses).setText(stats[0]); + this.setText(MciViewIds.mrcChat.mrcUsers, stats[2]); + this.setText(MciViewIds.mrcChat.mrcBbses, stats[0]); + + this.customFormatObj.boardCount = parseInt(stats[0]); + this.customFormatObj.roomCount = parseInt(stats[1]); + this.customFormatObj.userCount = parseInt(stats[2]); + this.updateCustomViews(); + this.state.last_ping = stats[1]; break; } @@ -303,6 +328,18 @@ exports.getModule = class mrcModule extends MenuModule { }); } + setText(mciId, text) { + return this.setViewText('mrcChat', mciId, text); + } + + updateCustomViews() { + return this.updateCustomViewTextsWithFilter( + 'mrcChat', + MciViewIds.mrcChat.customRangeStart, + this.customFormatObj + ); + } + /** * Receives the message input from the user and does something with it based on what it is */ @@ -353,9 +390,6 @@ exports.getModule = class mrcModule extends MenuModule { * Processes a message that begins with a slash */ processSlashCommand(message) { - // get the chat log view in case we need it - const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); - const cmd = message.split(' '); cmd[0] = cmd[0].substr(1).toLowerCase(); From 80eb8ad38df5d7c9a0c6fce9b940bed7cf7c928a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 10 Jun 2019 20:07:45 -0600 Subject: [PATCH 072/140] Add latency est., activity level indicators, etc. --- core/mrc.js | 67 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index 8832d103..b8d752ee 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -82,17 +82,19 @@ exports.getModule = class mrcModule extends MenuModule { room: '', room_topic: '', nicks: [], - last_ping: 0 + lastSentMsg : {}, // used for latency est. }; this.customFormatObj = { - roomName : '', - roomTopic : '', - roomUserCount : 0, - userCount : 0, - boardCount : 0, - roomCount : 0, - //latencyMs : 0, + roomName : '', + roomTopic : '', + roomUserCount : 0, + userCount : 0, + boardCount : 0, + roomCount : 0, + //latencyMs : 0, + activityLevel : 0, + activityLevelIndicator : ' ', }; this.menuMethods = { @@ -298,16 +300,27 @@ exports.getModule = class mrcModule extends MenuModule { break; case 'STATS': { - const stats = params[1].split(' '); - this.setText(MciViewIds.mrcChat.mrcUsers, stats[2]); - this.setText(MciViewIds.mrcChat.mrcBbses, stats[0]); + const [ + boardCount, + roomCount, + userCount, + activityLevel + ] = params[1].split(' ').map(v => parseInt(v)); + + const activityLevelIndicator = this.getActivityLevelIndicator(activityLevel); + + Object.assign( + this.customFormatObj, + { + boardCount, roomCount, userCount, + activityLevel, activityLevelIndicator + } + ); + + this.setText(MciViewIds.mrcChat.mrcUsers, userCount); + this.setText(MciViewIds.mrcChat.mrcBbses, boardCount); - this.customFormatObj.boardCount = parseInt(stats[0]); - this.customFormatObj.roomCount = parseInt(stats[1]); - this.customFormatObj.userCount = parseInt(stats[2]); this.updateCustomViews(); - - this.state.last_ping = stats[1]; break; } @@ -317,6 +330,12 @@ exports.getModule = class mrcModule extends MenuModule { } } else { + if(message.body === this.state.lastSentMsg.msg) { + this.customFormatObj.latencyMs = + moment.duration(moment().diff(this.state.lastSentMsg.time)).asMilliseconds(); + delete this.state.lastSentMsg.msg; + } + if (message.to_room == this.state.room) { // if we're here then we want to show it to the user const currentTime = moment().format(this.client.currentTheme.helpers.getTimeFormat()); @@ -328,6 +347,14 @@ exports.getModule = class mrcModule extends MenuModule { }); } + getActivityLevelIndicator(level) { + let indicators = this.config.activityLevelIndicators; + if(!Array.isArray(indicators) || indicators.length < level + 1) { + indicators = [ ' ', '░', '▒', '▓' ]; + } + return indicators[level].charAt(0); + } + setText(mciId, text) { return this.setViewText('mrcChat', mciId, text); } @@ -378,6 +405,10 @@ exports.getModule = class mrcModule extends MenuModule { } try { + this.state.lastSentMsg = { + msg : formattedMessage, + time : moment(), + }; this.sendMessageToMultiplexer(to_user || '', '', this.state.room, formattedMessage); } catch(e) { this.client.log.warn( { error : e.message }, 'MRC error'); @@ -397,6 +428,7 @@ exports.getModule = class mrcModule extends MenuModule { case 'pm': this.processOutgoingMessage(cmd[2], cmd[1]); break; + case 'rainbow': { // this is brutal, but i love it const line = message.replace(/^\/rainbow\s/, '').split(' ').reduce(function (a, c) { @@ -408,6 +440,7 @@ exports.getModule = class mrcModule extends MenuModule { this.processOutgoingMessage(line); break; } + case 'l33t': this.processOutgoingMessage(StringUtil.stylizeString(message.substr(6), 'l33t')); break; @@ -418,6 +451,7 @@ exports.getModule = class mrcModule extends MenuModule { this.processOutgoingMessage(StringUtil.stylizeString(message.substr(6), mode)); break; } + case 'whoon': this.sendServerMessage('WHOON'); break; @@ -471,6 +505,7 @@ exports.getModule = class mrcModule extends MenuModule { } // just do something to get the cursor back to the right place ¯\_(ツ)_/¯ + // :TODO: fix me! this.sendServerMessage('STATS'); } From d6af9a1caafc6a089a7b642912e1b9617499e91e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 10 Jun 2019 20:12:17 -0600 Subject: [PATCH 073/140] Minor --- core/mrc.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/mrc.js b/core/mrc.js index b8d752ee..754e2728 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -92,7 +92,7 @@ exports.getModule = class mrcModule extends MenuModule { userCount : 0, boardCount : 0, roomCount : 0, - //latencyMs : 0, + latencyMs : 0, activityLevel : 0, activityLevelIndicator : ' ', }; @@ -352,7 +352,7 @@ exports.getModule = class mrcModule extends MenuModule { if(!Array.isArray(indicators) || indicators.length < level + 1) { indicators = [ ' ', '░', '▒', '▓' ]; } - return indicators[level].charAt(0); + return indicators[level]; } setText(mciId, text) { From 4158b07ad06a5c0719bbc681e6369ecb4346c51f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 11 Jun 2019 18:25:24 -0600 Subject: [PATCH 074/140] Better naming, start on temp tokens table, etc. --- core/abracadabra.js | 2 +- core/database.js | 16 +++++ core/user_2fa_otp_config.js | 119 ++++++++++++++++++++++++++++++------ core/user_property.js | 1 + 4 files changed, 119 insertions(+), 19 deletions(-) diff --git a/core/abracadabra.js b/core/abracadabra.js index 9ac4dec7..83aa376b 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -79,7 +79,7 @@ exports.getModule = class AbracadabraModule extends MenuModule { /* :TODO: - * disconnecting wile door is open leaves dosemu + * disconnecting while door is open leaves dosemu * http://bbslink.net/sysop.php support * Font support ala all other menus... or does this just work? */ diff --git a/core/database.js b/core/database.js index 91f56a04..b3778833 100644 --- a/core/database.js +++ b/core/database.js @@ -203,6 +203,22 @@ const DB_INIT_TABLE = { );` ); + // + // Table for temporary tokens, generally used for e.g. 'outside' + // access such as email links. + // Examples: PW reset, enabling of 2FA/OTP, etc. + // + dbs.user.run( + `CREATE TABLE IF NOT EXISTS user_temporary_token ( + user_id INTEGER NOT NULL, + token VARCHAR NOT NULL, + timestamp DATETIME NOT NULL, + purpose VARCHAR NOT NULL, + UNIQUE(user_id, token), + FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE + );` + ); + return cb(null); }, diff --git a/core/user_2fa_otp_config.js b/core/user_2fa_otp_config.js index a47e0e6d..851e197e 100644 --- a/core/user_2fa_otp_config.js +++ b/core/user_2fa_otp_config.js @@ -9,11 +9,16 @@ const { otpFromType, createQRCode, } = require('./user_2fa_otp.js'); +const { Errors } = require('./enig_error.js'); +const { sendMail } = require('./email.js'); +const { getServer } = require('./listening_server.js'); +const WebServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; // deps const async = require('async'); const _ = require('lodash'); const iconv = require('iconv-lite'); +const crypto = require('crypto'); exports.moduleInfo = { name : 'User 2FA/OTP Configuration', @@ -26,10 +31,10 @@ const FormIds = { }; const MciViewIds = { - enableToggle : 1, - typeSelection : 2, - submission : 3, - infoText : 4, + enableToggle : 1, + otpType : 2, + submit : 3, + infoText : 4, customRangeStart : 10, // 10+ = customs }; @@ -53,10 +58,22 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { }, showBackupCodes : (formData, extraArgs, cb) => { return this.showBackupCodes(cb); + }, + saveChanges : (formData, extraArgs, cb) => { + return this.saveChanges(formData, cb); } }; } + initSequence() { + this.webServer = getServer(WebServerPackageName); + if(!this.webServer || !this.webServer.instance.isEnabled()) { + this.client.log.warn('User 2FA/OTP configuration requires the web server to be enabled!'); + return this.prevMenu( () => { /* dummy */ } ); + } + return super.initSequence(); + } + mciReady(mciData, cb) { super.mciReady(mciData, err => { if(err) { @@ -71,8 +88,8 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { (callback) => { const requiredCodes = [ MciViewIds.enableToggle, - MciViewIds.typeSelection, - MciViewIds.submission, + MciViewIds.otpType, + MciViewIds.submit, ]; return this.validateMCIByViewIds('menu', requiredCodes, callback); }, @@ -86,19 +103,19 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { return this.enableToggleUpdate(idx); }); - const typeSelectionView = this.getView('menu', MciViewIds.typeSelection); - initialIndex = this.typeSelectionIndexFromUserOTPType(); - typeSelectionView.setFocusItemIndex(initialIndex); + const otpTypeView = this.getView('menu', MciViewIds.otpType); + initialIndex = this.otpTypeIndexFromUserOTPType(); + otpTypeView.setFocusItemIndex(initialIndex); - typeSelectionView.on('index update', idx => { - return this.typeSelectionUpdate(idx); + otpTypeView.on('index update', idx => { + return this.otpTypeUpdate(idx); }); this.viewControllers.menu.on('return', view => { if(view === enableToggleView) { return this.enableToggleUpdate(enableToggleView.focusedItemIndex); - } else if (view === typeSelectionView) { - return this.typeSelectionUpdate(typeSelectionView.focusedItemIndex); + } else if (view === otpTypeView) { + return this.otpTypeUpdate(otpTypeView.focusedItemIndex); } }); @@ -169,8 +186,74 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { return this.displayDetails(info, cb); } + saveChanges(formData, cb) { + const enabled = 1 === _.get(formData, 'value.enableToggle', 0); + return enabled ? this.saveChangesEnable(formData, cb) : this.saveChangesDisable(cb); + } + + saveChangesEnable(formData, cb) { + const otpTypeProp = this.otpTypeFromOTPTypeIndex(_.get(formData, 'value.otpType')); + + // sanity check + if(!otpFromType(otpTypeProp)) { + return cb(Errors.Invalid('Cannot convert selected index to valid OTP type')); + } + + async.waterfall( + [ + (callback) => { + return this.removeUserOTPProperties(callback); + }, + (callback) => { + return crypto.randomBytes(256, callback); + }, + (token, callback) => { + // :TODO: consider temporary tokens table - this has become semi-common + // token | timestamp | token_type | + // abc | ISO | '2fa_otp_register' + token = token.toString('hex'); + this.client.user.persistProperty(UserProps.AuthFactor2OTPEnableToken, token, err => { + return callback(err, token); + }); + }, + (token, callback) => { + const resetUrl = this.webServer.instance.buildUrl( + `/enable_2fa_otp?token=&otpType=${otpTypeProp}&token=${token}` + ); + + // clear any existing (e.g. same as disable) -> send activation email + + return callback(null); + } + ], + err => { + return cb(err); + } + ); + } + + removeUserOTPProperties(cb) { + const props = [ + UserProps.AuthFactor2OTP, + UserProps.AuthFactor2OTPSecret, + UserProps.AuthFactor2OTPBackupCodes, + ]; + return this.client.user.removeProperties(props, cb); + } + + saveChangesDisable(cb) { + this.removeUserOTPProperties( err => { + if(err) { + return cb(err); + } + + // :TODO: show "saved+disabled" art/message -> prevMenu + return cb(null); + }); + } + isOTPEnabledForUser() { - return this.typeSelectionIndexFromUserOTPType(-1) != -1; + return this.otpTypeIndexFromUserOTPType(-1) != -1; } getInfoText(key) { @@ -185,7 +268,7 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { this.updateCustomViewTextsWithFilter('menu', MciViewIds.customRangeStart, { infoText : this.getInfoText(key) } ); } - typeSelectionIndexFromUserOTPType(defaultIndex = 0) { + otpTypeIndexFromUserOTPType(defaultIndex = 0) { const type = this.client.user.getProperty(UserProps.AuthFactor2OTP); return { [ OTPTypes.RFC6238_TOTP ] : 0, @@ -194,7 +277,7 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { }[type] || defaultIndex; } - otpTypeFromTypeSelectionIndex(idx) { + otpTypeFromOTPTypeIndex(idx) { return { 0 : OTPTypes.RFC6238_TOTP, 1 : OTPTypes.RFC4266_HOTP, @@ -202,8 +285,8 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { }[idx]; } - typeSelectionUpdate(idx) { - const key = this.otpTypeFromTypeSelectionIndex(idx); + otpTypeUpdate(idx) { + const key = this.otpTypeFromOTPTypeIndex(idx); this.updateCustomViewTextsWithFilter('menu', MciViewIds.customRangeStart, { infoText : this.getInfoText(key) } ); } }; diff --git a/core/user_property.js b/core/user_property.js index 88ac11b1..8d7ca91c 100644 --- a/core/user_property.js +++ b/core/user_property.js @@ -63,5 +63,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 + AuthFactor2OTPEnableToken : 'auth_factor2_otp_enable_token', }; From 18eecb6223154e1a40cb04fe227210207f06b665 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 11 Jun 2019 19:54:57 -0600 Subject: [PATCH 075/140] Fix user_temporary_token table --- core/database.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/database.js b/core/database.js index b3778833..e0e58168 100644 --- a/core/database.js +++ b/core/database.js @@ -212,9 +212,9 @@ const DB_INIT_TABLE = { `CREATE TABLE IF NOT EXISTS user_temporary_token ( user_id INTEGER NOT NULL, token VARCHAR NOT NULL, + token_type VARCHAR NOT NULL, timestamp DATETIME NOT NULL, - purpose VARCHAR NOT NULL, - UNIQUE(user_id, token), + UNIQUE(user_id, token_type), FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE );` ); From fa3e3e58021e878ae70e05a59cbd0cf280ef9467 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 11 Jun 2019 21:20:34 -0600 Subject: [PATCH 076/140] Introduce new user_temporary_token & use for token storage of 2FA/OTP registration --- core/config.js | 5 ++ core/user_2fa_otp_config.js | 69 ++++++++++++++---- core/user_property.js | 1 - core/user_temp_token.js | 140 ++++++++++++++++++++++++++++++++++++ core/web_password_reset.js | 2 +- 5 files changed, 203 insertions(+), 14 deletions(-) create mode 100644 core/user_temp_token.js diff --git a/core/config.js b/core/config.js index 0bb82838..ee6ac90d 100644 --- a/core/config.js +++ b/core/config.js @@ -233,6 +233,11 @@ function getDefaultConfig() { twoFactorAuth : { method : 'googleAuth', + + otp : { + registerEmailText : paths.join(__dirname, '../misc/otp_register_email.template.txt'), + registerEmailHtml : paths.join(__dirname, '../misc/otp_register_email.template.html') + } } }, diff --git a/core/user_2fa_otp_config.js b/core/user_2fa_otp_config.js index 851e197e..b95f2b75 100644 --- a/core/user_2fa_otp_config.js +++ b/core/user_2fa_otp_config.js @@ -13,12 +13,17 @@ const { Errors } = require('./enig_error.js'); const { sendMail } = require('./email.js'); const { getServer } = require('./listening_server.js'); const WebServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; +const { + createToken, + WellKnownTokenTypes, +} = require('./user_temp_token.js'); +const Config = require('./config.js').get; // deps const async = require('async'); const _ = require('lodash'); const iconv = require('iconv-lite'); -const crypto = require('crypto'); +const fs = require('fs-extra'); exports.moduleInfo = { name : 'User 2FA/OTP Configuration', @@ -44,6 +49,17 @@ const DefaultMsg = { noBackupCodes : 'No backup codes remaining or set.', }; +const DefaultEmailTextTemplate = + `%USERNAME%: +You have requested to enable 2-Factor Authentication via One-Time-Password +for your account on %BOARDNAME%. + + * If this was not you, please ignore this email and change your password. + * Otherwise, please follow the link below: + + %REGISTER_URL% +`; + exports.getModule = class User2FA_OTPConfigModule extends MenuModule { constructor(options) { super(options); @@ -205,25 +221,54 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { return this.removeUserOTPProperties(callback); }, (callback) => { - return crypto.randomBytes(256, callback); + return createToken(this.client.user.userId, WellKnownTokenTypes.AuthFactor2OTPRegister, callback); }, (token, callback) => { - // :TODO: consider temporary tokens table - this has become semi-common - // token | timestamp | token_type | - // abc | ISO | '2fa_otp_register' - token = token.toString('hex'); - this.client.user.persistProperty(UserProps.AuthFactor2OTPEnableToken, token, err => { - return callback(err, token); + const config = Config(); + const txtTemplateFile = _.get(config, 'users.twoFactorAuth.otp.registerEmailText'); + const htmlTemplateFile = _.get(config, 'users.twoFactorAuth.otp.registerEmailHtml'); + + fs.readFile(txtTemplateFile, 'utf8', (err, textTemplate) => { + textTemplate = textTemplate || DefaultEmailTextTemplate; + fs.readFile(htmlTemplateFile, 'utf8', (err, htmlTemplate) => { + htmlTemplate = htmlTemplate || null; // be explicit for waterfall + return callback(null, token, textTemplate, htmlTemplate); + }); }); }, - (token, callback) => { - const resetUrl = this.webServer.instance.buildUrl( + (token, textTemplate, htmlTemplate, callback) => { + const registerUrl = this.webServer.instance.buildUrl( `/enable_2fa_otp?token=&otpType=${otpTypeProp}&token=${token}` ); - // clear any existing (e.g. same as disable) -> send activation email + const user = this.client.user; - return callback(null); + const replaceTokens = (s) => { + return s + .replace(/%BOARDNAME%/g, Config().general.boardName) + .replace(/%USERNAME%/g, user.username) + .replace(/%TOKEN%/g, token) + .replace(/%REGISTER_URL%/g, registerUrl) + ; + }; + + textTemplate = replaceTokens(textTemplate); + if(htmlTemplate) { + htmlTemplate = replaceTokens(htmlTemplate); + } + + const message = { + to : `${user.getProperty(UserProps.RealName) || user.username} <${user.getProperty(UserProps.EmailAddress)}>`, + // from will be filled in + subject : '2-Factor Authentication Registration', + text : textTemplate, + html : htmlTemplate, + }; + + sendMail(message, (err, info) => { + // :TODO: Log info! + return callback(err); + }); } ], err => { diff --git a/core/user_property.js b/core/user_property.js index 8d7ca91c..88ac11b1 100644 --- a/core/user_property.js +++ b/core/user_property.js @@ -63,6 +63,5 @@ 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 - AuthFactor2OTPEnableToken : 'auth_factor2_otp_enable_token', }; diff --git a/core/user_temp_token.js b/core/user_temp_token.js new file mode 100644 index 00000000..76ca3438 --- /dev/null +++ b/core/user_temp_token.js @@ -0,0 +1,140 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const UserDb = require('./database.js').dbs.user; +const { + getISOTimestampString +} = require('./database.js'); +const { Errors } = require('./enig_error.js'); +const User = require('./user.js'); +const Log = require('./logger.js').log; + +// deps +const crypto = require('crypto'); +const async = require('async'); +const moment = require('moment'); + +exports.createToken = createToken; +exports.deleteToken = deleteToken; +exports.deleteTokenByUserAndType = deleteTokenByUserAndType; +exports.getTokenInfo = getTokenInfo; +exports.temporaryTokenMaintenanceTask = temporaryTokenMaintenanceTask; + +exports.WellKnownTokenTypes = { + AuthFactor2OTPRegister : 'auth_factor2_otp_register', +}; + +function createToken(userId, tokenType, cb) { + async.waterfall( + [ + (callback) => { + return crypto.randomBytes(256, callback); + }, + (token, callback) => { + token = token.toString('hex'); + + UserDb.run( + `INSERT INTO user_temporary_token (user_id, token, token_type, timestamp) + VALUES (?, ?, ?, ?);`, + [ userId, token, tokenType, getISOTimestampString() ], + err => { + return callback(err, token); + } + ); + } + ], + (err, token) => { + return cb(err, token); + } + ); +} + +function deleteToken(token, cb) { + UserDb.run( + `DELETE FROM user_temporary_token + WHERE token = ?;`, + [ token ], + err => { + return cb(err); + } + ); +} + +function deleteTokenByUserAndType(userId, tokenType, cb) { + UserDb.run( + `DELETE FROM user_temporary_token + WHERE user_id = ? AND token_type = ?;`, + [ userId, tokenType ], + err => { + return cb(err); + } + ); +} + +function getTokenInfo(token, cb) { + async.waterfall( + [ + (callback) => { + UserDb.get( + `SELECT user_id, token_type, timestamp + FROM user_temporary_token + WHERE token = ?;`, + [ token ], + (err, row) => { + if(err) { + return callback(err); + } + + if(!row) { + return callback(Errors.DoesNotExist('No entry found for token')); + } + + const info = { + userId : row.user_id, + tokenType : row.token_type, + timestamp : moment(row.timestamp), + }; + return callback(null, info); + } + ); + }, + (info, callback) => { + User.getUser(info.userId, (err, user) => { + info.user = user; + return callback(err, info); + }); + } + ], + (err, info) => { + return cb(err, info); + } + ); +} + +function temporaryTokenMaintenanceTask(args, cb) { + const tokenType = args[0]; + + if(!tokenType) { + return Log.error('Cannot run temporary token maintenance task with out specifying "tokenType" as argument 0'); + } + + const expTime = args[1] || '24 hours'; + + UserDb.run( + `DELETE FROM user_temporary_token + WHERE token IN ( + SELECT token + FROM user_temporary_token + WHERE token_type = ? + AND DATETIME("now") >= DATETIME(timestamp, "+${expTime}") + );`, + [ tokenType ], + err => { + if(err) { + Log.warn( { error : err.message, tokenType }, 'Failed deleting user temporary token'); + } + return cb(err); + } + ); +} diff --git a/core/web_password_reset.js b/core/web_password_reset.js index 90c5f57c..6fbb65b9 100644 --- a/core/web_password_reset.js +++ b/core/web_password_reset.js @@ -22,7 +22,7 @@ const _ = require('lodash'); const PW_RESET_EMAIL_TEXT_TEMPLATE_DEFAULT = `%USERNAME%: -a password reset has been requested for your account on %BOARDNAME%. +A password reset has been requested for your account on %BOARDNAME%. * If this was not you, please ignore this email. * Otherwise, follow this link: %RESET_URL% From 3efea3de9a5071c0fe8e4202a9b152d6b2df0e20 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 12 Jun 2019 21:57:45 -0600 Subject: [PATCH 077/140] Work on 2FA/OTP email system * Web routes/handler/etc. mostly functional * Can now enable -> follow link -> submit -> capture form * Clean up code --- core/bbs.js | 4 + core/config.js | 5 +- core/email.js | 2 +- core/user_2fa_otp_config.js | 86 ++---------- core/user_2fa_otp_web_register.js | 216 ++++++++++++++++++++++++++++++ core/user_temp_token.js | 6 +- core/web_password_reset.js | 2 +- www/otp_register.template.html | 37 +++++ 8 files changed, 273 insertions(+), 85 deletions(-) create mode 100644 core/user_2fa_otp_web_register.js create mode 100644 www/otp_register.template.html diff --git a/core/bbs.js b/core/bbs.js index 4d371fb3..98ba1b7a 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -306,6 +306,10 @@ function initialize(cb) { const WebPasswordReset = require('./web_password_reset.js').WebPasswordReset; return WebPasswordReset.startup(callback); }, + function ready2FA_OTPRegister(callback) { + const User2FA_OTPWebRegister = require('./user_2fa_otp_web_register.js'); + return User2FA_OTPWebRegister.startup(callback); + }, function readyEventScheduler(callback) { const EventSchedulerModule = require('./event_scheduler.js').EventSchedulerModule; EventSchedulerModule.loadAndStart( (err, modInst) => { diff --git a/core/config.js b/core/config.js index ee6ac90d..47868fa0 100644 --- a/core/config.js +++ b/core/config.js @@ -235,8 +235,9 @@ function getDefaultConfig() { method : 'googleAuth', otp : { - registerEmailText : paths.join(__dirname, '../misc/otp_register_email.template.txt'), - registerEmailHtml : paths.join(__dirname, '../misc/otp_register_email.template.html') + registerEmailText : paths.join(__dirname, '../misc/otp_register_email.template.txt'), + registerEmailHtml : paths.join(__dirname, '../misc/otp_register_email.template.html'), + registerPageTemplate : paths.join(__dirname, '../www/otp_register.template.html'), } } }, diff --git a/core/email.js b/core/email.js index 1de3b034..4a41106a 100644 --- a/core/email.js +++ b/core/email.js @@ -15,7 +15,7 @@ exports.sendMail = sendMail; function sendMail(message, cb) { const config = Config(); if(!_.has(config, 'email.transport')) { - return cb(Errors.MissingConfig('Email "email::transport" configuration missing')); + return cb(Errors.MissingConfig('Email "email.transport" configuration missing')); } message.from = message.from || config.email.defaultFrom; diff --git a/core/user_2fa_otp_config.js b/core/user_2fa_otp_config.js index b95f2b75..09a72a20 100644 --- a/core/user_2fa_otp_config.js +++ b/core/user_2fa_otp_config.js @@ -10,20 +10,15 @@ const { createQRCode, } = require('./user_2fa_otp.js'); const { Errors } = require('./enig_error.js'); -const { sendMail } = require('./email.js'); const { getServer } = require('./listening_server.js'); const WebServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; -const { - createToken, - WellKnownTokenTypes, -} = require('./user_temp_token.js'); const Config = require('./config.js').get; +const WebRegister = require('./user_2fa_otp_web_register.js'); // deps const async = require('async'); const _ = require('lodash'); const iconv = require('iconv-lite'); -const fs = require('fs-extra'); exports.moduleInfo = { name : 'User 2FA/OTP Configuration', @@ -49,17 +44,6 @@ const DefaultMsg = { noBackupCodes : 'No backup codes remaining or set.', }; -const DefaultEmailTextTemplate = - `%USERNAME%: -You have requested to enable 2-Factor Authentication via One-Time-Password -for your account on %BOARDNAME%. - - * If this was not you, please ignore this email and change your password. - * Otherwise, please follow the link below: - - %REGISTER_URL% -`; - exports.getModule = class User2FA_OTPConfigModule extends MenuModule { constructor(options) { super(options); @@ -82,8 +66,8 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { } initSequence() { - this.webServer = getServer(WebServerPackageName); - if(!this.webServer || !this.webServer.instance.isEnabled()) { + const webServer = getServer(WebServerPackageName); + if(!webServer || !webServer.instance.isEnabled()) { this.client.log.warn('User 2FA/OTP configuration requires the web server to be enabled!'); return this.prevMenu( () => { /* dummy */ } ); } @@ -215,66 +199,12 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { return cb(Errors.Invalid('Cannot convert selected index to valid OTP type')); } - async.waterfall( - [ - (callback) => { - return this.removeUserOTPProperties(callback); - }, - (callback) => { - return createToken(this.client.user.userId, WellKnownTokenTypes.AuthFactor2OTPRegister, callback); - }, - (token, callback) => { - const config = Config(); - const txtTemplateFile = _.get(config, 'users.twoFactorAuth.otp.registerEmailText'); - const htmlTemplateFile = _.get(config, 'users.twoFactorAuth.otp.registerEmailHtml'); - - fs.readFile(txtTemplateFile, 'utf8', (err, textTemplate) => { - textTemplate = textTemplate || DefaultEmailTextTemplate; - fs.readFile(htmlTemplateFile, 'utf8', (err, htmlTemplate) => { - htmlTemplate = htmlTemplate || null; // be explicit for waterfall - return callback(null, token, textTemplate, htmlTemplate); - }); - }); - }, - (token, textTemplate, htmlTemplate, callback) => { - const registerUrl = this.webServer.instance.buildUrl( - `/enable_2fa_otp?token=&otpType=${otpTypeProp}&token=${token}` - ); - - const user = this.client.user; - - const replaceTokens = (s) => { - return s - .replace(/%BOARDNAME%/g, Config().general.boardName) - .replace(/%USERNAME%/g, user.username) - .replace(/%TOKEN%/g, token) - .replace(/%REGISTER_URL%/g, registerUrl) - ; - }; - - textTemplate = replaceTokens(textTemplate); - if(htmlTemplate) { - htmlTemplate = replaceTokens(htmlTemplate); - } - - const message = { - to : `${user.getProperty(UserProps.RealName) || user.username} <${user.getProperty(UserProps.EmailAddress)}>`, - // from will be filled in - subject : '2-Factor Authentication Registration', - text : textTemplate, - html : htmlTemplate, - }; - - sendMail(message, (err, info) => { - // :TODO: Log info! - return callback(err); - }); - } - ], - err => { + this.removeUserOTPProperties(err => { + if(err) { return cb(err); } - ); + return WebRegister.sendRegisterEmail(this.client.user, otpTypeProp, cb); + }); } removeUserOTPProperties(cb) { @@ -287,7 +217,7 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { } saveChangesDisable(cb) { - this.removeUserOTPProperties( err => { + this.removeUserOTPProperties(this.client.user, err => { if(err) { return cb(err); } diff --git a/core/user_2fa_otp_web_register.js b/core/user_2fa_otp_web_register.js new file mode 100644 index 00000000..840a5dd3 --- /dev/null +++ b/core/user_2fa_otp_web_register.js @@ -0,0 +1,216 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const Config = require('./config.js').get; +const Errors = require('./enig_error.js').Errors; +const getServer = require('./listening_server.js').getServer; +const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; +const { + createToken, + getTokenInfo, + WellKnownTokenTypes, +} = require('./user_temp_token.js'); +const { sendMail } = require('./email.js'); +const UserProps = require('./user_property.js'); +const Log = require('./logger.js').log; + +// deps +const async = require('async'); +const fs = require('fs-extra'); +const _ = require('lodash'); +const url = require('url'); +const querystring = require('querystring'); + +function getWebServer() { + return getServer(webServerPackageName); +} + +const DefaultEmailTextTemplate = + `%USERNAME%: +You have requested to enable 2-Factor Authentication via One-Time-Password +for your account on %BOARDNAME%. + + * If this was not you, please ignore this email and change your password. + * Otherwise, please follow the link below: + + %REGISTER_URL% +`; + +module.exports = class User2FA_OTPWebRegister +{ + static startup(cb) { + return User2FA_OTPWebRegister.registerRoutes(cb); + } + + static sendRegisterEmail(user, otpType, cb) { + async.waterfall( + [ + (callback) => { + return createToken( + user.userId, + WellKnownTokenTypes.AuthFactor2OTPRegister, + { bits : 128 }, + callback + ); + }, + (token, callback) => { + const config = Config(); + const txtTemplateFile = _.get(config, 'users.twoFactorAuth.otp.registerEmailText'); + const htmlTemplateFile = _.get(config, 'users.twoFactorAuth.otp.registerEmailHtml'); + + fs.readFile(txtTemplateFile, 'utf8', (err, textTemplate) => { + textTemplate = textTemplate || DefaultEmailTextTemplate; + fs.readFile(htmlTemplateFile, 'utf8', (err, htmlTemplate) => { + htmlTemplate = htmlTemplate || null; // be explicit for waterfall + return callback(null, token, textTemplate, htmlTemplate); + }); + }); + }, + (token, textTemplate, htmlTemplate, callback) => { + const webServer = getWebServer(); + const registerUrl = webServer.instance.buildUrl( + `/enable_2fa_otp?token=&otpType=${otpType}&token=${token}` + ); + + const replaceTokens = (s) => { + return s + .replace(/%BOARDNAME%/g, Config().general.boardName) + .replace(/%USERNAME%/g, user.username) + .replace(/%TOKEN%/g, token) + .replace(/%REGISTER_URL%/g, registerUrl) + ; + }; + + textTemplate = replaceTokens(textTemplate); + if(htmlTemplate) { + htmlTemplate = replaceTokens(htmlTemplate); + } + + const message = { + to : `${user.getProperty(UserProps.RealName) || user.username} <${user.getProperty(UserProps.EmailAddress)}>`, + // from will be filled in + subject : '2-Factor Authentication Registration', + text : textTemplate, + html : htmlTemplate, + }; + + sendMail(message, (err, info) => { + if(err) { + Log.warn({ error : err.message }, 'Failed sending 2FA/OTP register email'); + } else { + Log.info( { info }, 'Successfully sent 2FA/OTP register email'); + } + return callback(err); + }); + } + ], + err => { + return cb(err); + } + ); + } + + static fileNotFound(webServer, resp) { + return webServer.instance.fileNotFound(resp); + } + + static accessDenied(webServer, resp) { + return webServer.instance.accessDenied(resp); + } + + static routeRegisterGet(req, resp) { + const webServer = getWebServer(); // must be valid, we just got a req! + + const urlParts = url.parse(req.url, true); + const token = urlParts.query && urlParts.query.token; + const otpType = urlParts.query && urlParts.query.otpType; + + if(!token || !otpType) { + return User2FA_OTPWebRegister.accessDenied(webServer, resp); + } + + getTokenInfo(token, (err, tokenInfo) => { + if(err) { + // assume expired + return webServer.instance.respondWithError(resp, 410, 'Invalid or expired registration link.', 'Expired Link'); + } + + if(tokenInfo.tokenType !== 'auth_factor2_otp_register') { + return User2FA_OTPWebRegister.accessDenied(webServer, resp); + } + + const qrImg = ''; // :TODO: fix me + const secret = ''; + const backupCodes = ''; + + const postUrl = webServer.instance.buildUrl('/enable_2fa_otp'); + const config = Config(); + return webServer.instance.routeTemplateFilePage( + _.get(config, 'users.twoFactorAuth.otp.registerPageTemplate'), + (templateData, next) => { + const finalPage = templateData + .replace(/%BOARDNAME%/g, config.general.boardName) + .replace(/%USERNAME%/g, tokenInfo.user.username) + .replace(/%TOKEN%/g, token) + .replace(/%OTP_TYPE%/g, otpType) + .replace(/%POST_URL%/g, postUrl) + .replace(/%QR_IMG%/g, qrImg) + .replace(/%SECRET%/g, secret) + .replace(/%BACKUP_CODES%/g, backupCodes) + ; + return next(null, finalPage); + }, + resp + ); + }); + } + + static routeRegisterPost(req, resp) { + const webServer = getWebServer(); // must be valid, we just got a req! + + const badRequest = () => { + return webServer.instance.respondWithError(resp, 400, 'Bad Request.', 'Bad Request'); + }; + + let bodyData = ''; + req.on('data', data => { + bodyData += data; + }); + + req.on('end', () => { + const formData = querystring.parse(bodyData); + + const config = Config(); + if(!formData.token || !formData.otpType || !formData.otp) { + return badRequest(); + } + }); + + return webServer.instance.respondWithError(resp, 410, 'Invalid or expired registration link.', 'Expired Link'); + } + + static registerRoutes(cb) { + const webServer = getWebServer(); + if(!webServer || !webServer.instance.isEnabled()) { + return cb(null); // no webserver enabled + } + + [ + { + method : 'GET', + path : '^\\/enable_2fa_otp\\?token\\=[a-f0-9]+&otpType\\=[a-zA-Z0-9]+$', + handler : User2FA_OTPWebRegister.routeRegisterGet, + }, + { + method : 'POST', + path : '^\\/enable_2fa_otp$', + handler : User2FA_OTPWebRegister.routeRegisterPost, + } + ].forEach(r => { + webServer.instance.addRoute(r); + }); + + return cb(null); + } +}; diff --git a/core/user_temp_token.js b/core/user_temp_token.js index 76ca3438..89c060d6 100644 --- a/core/user_temp_token.js +++ b/core/user_temp_token.js @@ -25,17 +25,17 @@ exports.WellKnownTokenTypes = { AuthFactor2OTPRegister : 'auth_factor2_otp_register', }; -function createToken(userId, tokenType, cb) { +function createToken(userId, tokenType, options = { bits : 128 }, cb) { async.waterfall( [ (callback) => { - return crypto.randomBytes(256, callback); + return crypto.randomBytes(options.bits, callback); }, (token, callback) => { token = token.toString('hex'); UserDb.run( - `INSERT INTO user_temporary_token (user_id, token, token_type, timestamp) + `INSERT OR REPLACE INTO user_temporary_token (user_id, token, token_type, timestamp) VALUES (?, ?, ?, ?);`, [ userId, token, tokenType, getISOTimestampString() ], err => { diff --git a/core/web_password_reset.js b/core/web_password_reset.js index 6fbb65b9..89c3fd33 100644 --- a/core/web_password_reset.js +++ b/core/web_password_reset.js @@ -133,7 +133,7 @@ class WebPasswordReset { if(err) { Log.warn( { error : err.message }, 'Failed sending password reset email' ); } else { - Log.debug( { info : info }, 'Successfully sent password reset email'); + Log.info( { info : info }, 'Successfully sent password reset email'); } return callback(err); diff --git a/www/otp_register.template.html b/www/otp_register.template.html new file mode 100644 index 00000000..a743928b --- /dev/null +++ b/www/otp_register.template.html @@ -0,0 +1,37 @@ + + + + + Enable 2FA/OTP — ENiGMA½ BBS + + + + + +
+ Enable One-Time-Password + + + + +
+ + \ No newline at end of file From 94747cfe7e813fd31197a217034ec1ea188db218 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 13 Jun 2019 19:47:04 -0600 Subject: [PATCH 078/140] Good progress on 2FA/OTP config: Most of email register lifecycle complete --- core/oputil/oputil_user.js | 3 +- core/user_2fa_otp.js | 10 +-- core/user_2fa_otp_web_register.js | 112 ++++++++++++++++++++++-------- www/otp_register.template.html | 42 +++++------ 4 files changed, 107 insertions(+), 60 deletions(-) diff --git a/core/oputil/oputil_user.js b/core/oputil/oputil_user.js index 7b441875..78ea08ca 100644 --- a/core/oputil/oputil_user.js +++ b/core/oputil/oputil_user.js @@ -370,6 +370,7 @@ function twoFactorAuthOTP(user) { const { OTPTypes, prepareOTP, + createBackupCodes, } = require('../../core/user_2fa_otp.js'); let otpType = argv._[argv._.length - 1]; @@ -414,7 +415,7 @@ function twoFactorAuthOTP(user) { qrType : argv['qr-type'] || 'ascii', }; prepareOTP(otpType, otpOpts, (err, otpInfo) => { - return callback(err, Object.assign(otpInfo, { otpType })); + return callback(err, Object.assign(otpInfo, { otpType, backupCodes : createBackupCodes() })); }); }, function storeOrDisplayQR(otpInfo, callback) { diff --git a/core/user_2fa_otp.js b/core/user_2fa_otp.js index 0b53ad8b..6d5ae6e1 100644 --- a/core/user_2fa_otp.js +++ b/core/user_2fa_otp.js @@ -20,6 +20,7 @@ const crypto = require('crypto'); const qrGen = require('qrcode-generator'); exports.prepareOTP = prepareOTP; +exports.createBackupCodes = createBackupCodes; exports.createQRCode = createQRCode; exports.otpFromType = otpFromType; exports.loginFactor2_OTP = loginFactor2_OTP; @@ -82,7 +83,7 @@ function generateOTPBackupCode() { return bits.join('-'); } -function generateNewBackupCodes() { +function createBackupCodes() { const codes = [...Array(6)].map(() => generateOTPBackupCode()); return codes; } @@ -120,7 +121,7 @@ function createQRCode(otp, options, secret) { data : qrCode.createDataURL, img : qrCode.createImgTag, svg : qrCode.createSvgTag, - }[options.qrType](); + }[options.qrType](options.cellSize); } catch(e) { return; } @@ -141,10 +142,9 @@ function prepareOTP(otpType, options, cb) { otp.generateSecret() : crypto.randomBytes(64).toString('base64').substr(0, 32); - const backupCodes = generateNewBackupCodes(); - const qr = createQRCode(otp, options, secret); + const qr = createQRCode(otp, options, secret); - return cb(null, { secret, backupCodes, qr } ); + return cb(null, { secret, qr } ); } function loginFactor2_OTP(client, token, cb) { diff --git a/core/user_2fa_otp_web_register.js b/core/user_2fa_otp_web_register.js index 840a5dd3..52ee96ad 100644 --- a/core/user_2fa_otp_web_register.js +++ b/core/user_2fa_otp_web_register.js @@ -11,6 +11,11 @@ const { getTokenInfo, WellKnownTokenTypes, } = require('./user_temp_token.js'); +const { + prepareOTP, + createBackupCodes, + otpFromType, +} = require('./user_2fa_otp.js'); const { sendMail } = require('./email.js'); const UserProps = require('./user_property.js'); const Log = require('./logger.js').log; @@ -70,7 +75,7 @@ module.exports = class User2FA_OTPWebRegister (token, textTemplate, htmlTemplate, callback) => { const webServer = getWebServer(); const registerUrl = webServer.instance.buildUrl( - `/enable_2fa_otp?token=&otpType=${otpType}&token=${token}` + `/enable_2fa_otp?token=${token}&otpType=${otpType}` ); const replaceTokens = (s) => { @@ -133,36 +138,48 @@ module.exports = class User2FA_OTPWebRegister getTokenInfo(token, (err, tokenInfo) => { if(err) { // assume expired - return webServer.instance.respondWithError(resp, 410, 'Invalid or expired registration link.', 'Expired Link'); + return webServer.instance.respondWithError( + resp, + 410, + 'Invalid or expired registration link.', 'Expired Link' + ); } if(tokenInfo.tokenType !== 'auth_factor2_otp_register') { return User2FA_OTPWebRegister.accessDenied(webServer, resp); } - const qrImg = ''; // :TODO: fix me - const secret = ''; - const backupCodes = ''; + const prepareOptions = { + qrType : 'data', + cellSize : 8, + username : tokenInfo.user.username, + }; - const postUrl = webServer.instance.buildUrl('/enable_2fa_otp'); - const config = Config(); - return webServer.instance.routeTemplateFilePage( - _.get(config, 'users.twoFactorAuth.otp.registerPageTemplate'), - (templateData, next) => { - const finalPage = templateData - .replace(/%BOARDNAME%/g, config.general.boardName) - .replace(/%USERNAME%/g, tokenInfo.user.username) - .replace(/%TOKEN%/g, token) - .replace(/%OTP_TYPE%/g, otpType) - .replace(/%POST_URL%/g, postUrl) - .replace(/%QR_IMG%/g, qrImg) - .replace(/%SECRET%/g, secret) - .replace(/%BACKUP_CODES%/g, backupCodes) - ; - return next(null, finalPage); - }, - resp - ); + prepareOTP(otpType, prepareOptions, (err, otpInfo) => { + if(err) { + // :TODO: Log error + return User2FA_OTPWebRegister.accessDenied(webServer, resp); + } + + const postUrl = webServer.instance.buildUrl('/enable_2fa_otp'); + const config = Config(); + return webServer.instance.routeTemplateFilePage( + _.get(config, 'users.twoFactorAuth.otp.registerPageTemplate'), + (templateData, next) => { + const finalPage = templateData + .replace(/%BOARDNAME%/g, config.general.boardName) + .replace(/%USERNAME%/g, tokenInfo.user.username) + .replace(/%TOKEN%/g, token) + .replace(/%OTP_TYPE%/g, otpType) + .replace(/%POST_URL%/g, postUrl) + .replace(/%QR_IMG_DATA%/g, otpInfo.qr) + .replace(/%SECRET%/g, otpInfo.secret) + ; + return next(null, finalPage); + }, + resp + ); + }); }); } @@ -181,13 +198,52 @@ module.exports = class User2FA_OTPWebRegister req.on('end', () => { const formData = querystring.parse(bodyData); - const config = Config(); - if(!formData.token || !formData.otpType || !formData.otp) { + if(!formData.token || !formData.otpType || !formData.otp || + !formData.secret) + { return badRequest(); } - }); - return webServer.instance.respondWithError(resp, 410, 'Invalid or expired registration link.', 'Expired Link'); + const otp = otpFromType(formData.otpType); + if(!otp) { + return badRequest(); + } + + const valid = otp.verify( { token : formData.otp, secret : formData.secret } ); + if(!valid) { + return User2FA_OTPWebRegister.accessDenied(webServer, resp); + } + + getTokenInfo(formData.token, (err, tokenInfo) => { + if(err) { + return User2FA_OTPWebRegister.accessDenied(webServer, resp); + } + + const backupCodes = createBackupCodes(); + + const props = { + [ UserProps.AuthFactor2OTP ] : formData.otpType, + [ UserProps.AuthFactor2OTPSecret ] : formData.secret, + [ UserProps.AuthFactor2OTPBackupCodes ] : JSON.stringify(backupCodes), + }; + + tokenInfo.user.persistProperties(props, err => { + if(err) { + return webServer.instance.respondWithError(resp, 500, 'Internal Server Error', 'Internal Server Error'); + } + // :TODO: remove token + + // :TODO: use a html template here too, if provided + resp.writeHead(200); + return resp.end( + `2-Factor Authentication via One-Time-Password has been enabled for this account. Please write down your backup codes and store them in safe place: + +${backupCodes} + ` + ); + }); + }); + }); } static registerRoutes(cb) { diff --git a/www/otp_register.template.html b/www/otp_register.template.html index a743928b..22d00866 100644 --- a/www/otp_register.template.html +++ b/www/otp_register.template.html @@ -5,33 +5,23 @@ Enable 2FA/OTP — ENiGMA½ BBS - -
- Enable One-Time-Password - - - - -
+

+ Your OTP secret:
+ %SECRET% +

+

+ QR Code:
+ +

+
+ Confirm One-Time-Password to continue: + + + + + +
\ No newline at end of file From 4ebbedf4bc7d3bf62c512525a53cd84535bf0a8c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 13 Jun 2019 20:51:35 -0600 Subject: [PATCH 079/140] Clean up tokens --- core/config.js | 9 +++++++++ core/user_2fa_otp_web_register.js | 14 ++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/core/config.js b/core/config.js index 47868fa0..d9a5684b 100644 --- a/core/config.js +++ b/core/config.js @@ -1026,6 +1026,15 @@ function getDefaultConfig() { args : [ '24 hours' ] // items older than this will be removed }, + twoFactorRegisterTokenMaintenance : { + schedule : 'every 24 hours', + action : '@method:core/user_temp_token.js:temporaryTokenMaintenanceTask', + args : [ + 'auth_factor2_otp_register', + '24 hours', // expire time + ] + }, + // // Enable the following entry in your config.hjson to periodically create/update // DESCRIPT.ION files for your file base diff --git a/core/user_2fa_otp_web_register.js b/core/user_2fa_otp_web_register.js index 52ee96ad..a708969e 100644 --- a/core/user_2fa_otp_web_register.js +++ b/core/user_2fa_otp_web_register.js @@ -3,11 +3,11 @@ // ENiGMA½ const Config = require('./config.js').get; -const Errors = require('./enig_error.js').Errors; const getServer = require('./listening_server.js').getServer; const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; const { createToken, + deleteToken, getTokenInfo, WellKnownTokenTypes, } = require('./user_temp_token.js'); @@ -145,7 +145,7 @@ module.exports = class User2FA_OTPWebRegister ); } - if(tokenInfo.tokenType !== 'auth_factor2_otp_register') { + if(tokenInfo.tokenType !== WellKnownTokenTypes.AuthFactor2OTPRegister) { return User2FA_OTPWebRegister.accessDenied(webServer, resp); } @@ -157,7 +157,7 @@ module.exports = class User2FA_OTPWebRegister prepareOTP(otpType, prepareOptions, (err, otpInfo) => { if(err) { - // :TODO: Log error + Log.error({ error : err.message }, 'Failed to prepare OTP'); return User2FA_OTPWebRegister.accessDenied(webServer, resp); } @@ -231,7 +231,13 @@ module.exports = class User2FA_OTPWebRegister if(err) { return webServer.instance.respondWithError(resp, 500, 'Internal Server Error', 'Internal Server Error'); } - // :TODO: remove token + + // we can now remove the token - no need to wait + deleteToken(formData.token, err => { + if(err) { + Log.error({error : err.message, token : formData.token}, 'Failed to delete temporary token'); + } + }); // :TODO: use a html template here too, if provided resp.writeHead(200); From 7481421898d9e17240b4c655046e3574eb81f04a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 13 Jun 2019 22:54:05 -0600 Subject: [PATCH 080/140] Fix bug caused by new getView() --- core/user_config.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/user_config.js b/core/user_config.js index d2748c4b..ca3b20ff 100644 --- a/core/user_config.js +++ b/core/user_config.js @@ -76,12 +76,12 @@ exports.getModule = class UserConfigModule extends MenuModule { }, validatePassConfirmMatch : function(data, cb) { - var passwordView = self.getView(MciCodeIds.Password); + var passwordView = self.getMenuView(MciCodeIds.Password); cb(passwordView.getData() === data ? null : new Error('Passwords do not match')); }, viewValidationListener : function(err, cb) { - var errMsgView = self.getView(MciCodeIds.ErrorMsg); + var errMsgView = self.getMenuView(MciCodeIds.ErrorMsg); var newFocusId; if(errMsgView) { if(err) { @@ -89,7 +89,7 @@ exports.getModule = class UserConfigModule extends MenuModule { if(err.view.getId() === MciCodeIds.PassConfirm) { newFocusId = MciCodeIds.Password; - var passwordView = self.getView(MciCodeIds.Password); + var passwordView = self.getMenuView(MciCodeIds.Password); passwordView.clearText(); err.view.clearText(); } @@ -150,7 +150,7 @@ exports.getModule = class UserConfigModule extends MenuModule { }; } - getView(viewId) { + getMenuView(viewId) { return this.viewControllers.menu.getView(viewId); } @@ -200,13 +200,13 @@ exports.getModule = class UserConfigModule extends MenuModule { self.setViewText('menu', MciCodeIds.TermHeight, user.properties[UserProps.TermHeight].toString()); - var themeView = self.getView(MciCodeIds.Theme); + var themeView = self.getMenuView(MciCodeIds.Theme); if(themeView) { themeView.setItems(_.map(self.availThemeInfo, 'name')); themeView.setFocusItemIndex(currentThemeIdIndex); } - var realNameView = self.getView(MciCodeIds.RealName); + var realNameView = self.getMenuView(MciCodeIds.RealName); if(realNameView) { realNameView.setFocus(true); // :TODO: HACK! menu.hjson sets focus, but manual population above breaks this. Needs a real fix! } From 2b154800c06d6ebceede590ab946d428c6d0c184 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 13 Jun 2019 22:54:56 -0600 Subject: [PATCH 081/140] More OTP updates/fixes * Ensure email address at save * Fix QR display issues for non-Google Auth * More cleanup/etc. --- core/user_2fa_otp.js | 2 +- core/user_2fa_otp_config.js | 75 +++++++++++++++++++++++++++---- core/user_2fa_otp_web_register.js | 20 +++++++-- www/otp_register.template.html | 10 +++-- 4 files changed, 90 insertions(+), 17 deletions(-) diff --git a/core/user_2fa_otp.js b/core/user_2fa_otp.js index 6d5ae6e1..c51d1282 100644 --- a/core/user_2fa_otp.js +++ b/core/user_2fa_otp.js @@ -123,7 +123,7 @@ function createQRCode(otp, options, secret) { svg : qrCode.createSvgTag, }[options.qrType](options.cellSize); } catch(e) { - return; + return ''; } } diff --git a/core/user_2fa_otp_config.js b/core/user_2fa_otp_config.js index 09a72a20..136c9178 100644 --- a/core/user_2fa_otp_config.js +++ b/core/user_2fa_otp_config.js @@ -8,11 +8,11 @@ const { OTPTypes, otpFromType, createQRCode, + createBackupCodes, } = require('./user_2fa_otp.js'); const { Errors } = require('./enig_error.js'); const { getServer } = require('./listening_server.js'); const WebServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; -const Config = require('./config.js').get; const WebRegister = require('./user_2fa_otp_web_register.js'); // deps @@ -42,6 +42,11 @@ const MciViewIds = { const DefaultMsg = { otpNotEnabled : '2FA/OTP is not currently enabled for this account.', noBackupCodes : 'No backup codes remaining or set.', + saveDisabled : '2FA/OTP is now disabled for this account.', + saveEmailSent : 'An 2FA/OTP registration email has been sent with further instructions.', + saveError : 'Failed to send email. Please contact the system operator.', + qrNotAvail : 'QR code not available for this OTP type.', + emailRequired : 'Your account must have a valid email address set to use this feature.', }; exports.getModule = class User2FA_OTPConfigModule extends MenuModule { @@ -59,6 +64,9 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { showBackupCodes : (formData, extraArgs, cb) => { return this.showBackupCodes(cb); }, + generateNewBackupCodes : (formData, extraArgs, cb) => { + return this.generateNewBackupCodes(cb); + }, saveChanges : (formData, extraArgs, cb) => { return this.saveChanges(formData, cb); } @@ -153,11 +161,18 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { username : this.client.user.username, qrType : 'ascii', }; + qrCode = createQRCode( otp, qrOptions, this.client.user.getProperty(UserProps.AuthFactor2OTPSecret) - ).replace(/\n/g, '\r\n'); + ); + + if(qrCode) { + qrCode = qrCode.replace(/\n/g, '\r\n'); + } else { + qrCode = this.config.qrNotAvail || DefaultMsg.qrNotAvail; + } } return this.displayDetails(qrCode, cb); @@ -186,24 +201,66 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { return this.displayDetails(info, cb); } + generateNewBackupCodes(cb) { + if(!this.isOTPEnabledForUser()) { + const info = this.config.otpNotEnabled || DefaultMsg.otpNotEnabled; + return this.displayDetails(info, cb); + } + + const backupCodes = createBackupCodes(); + this.client.user.persistProperty( + UserProps.AuthFactor2OTPBackupCodes, + JSON.stringify(backupCodes), + err => { + if(err) { + return cb(err); + } + const info = backupCodes.join(', '); + return this.displayDetails(info, cb); + } + ); + } + saveChanges(formData, cb) { const enabled = 1 === _.get(formData, 'value.enableToggle', 0); return enabled ? this.saveChangesEnable(formData, cb) : this.saveChangesDisable(cb); } saveChangesEnable(formData, cb) { + // User must have an email address set to save + const emailRegExp = /[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(.[a-z0-9-]+)*/; + const emailAddr = this.client.user.getProperty(UserProps.EmailAddress); + if(!emailAddr || !emailRegExp.test(emailAddr)) { + const info = this.config.emailRequired || DefaultMsg.emailRequired; + return this.displayDetails(info, cb); + } + const otpTypeProp = this.otpTypeFromOTPTypeIndex(_.get(formData, 'value.otpType')); + const saveFailedError = (err) => { + const info = this.config.saveError || DefaultMsg.saveError; + this.displayDetails(info, () => { + return cb(err); + }); + }; + // sanity check if(!otpFromType(otpTypeProp)) { - return cb(Errors.Invalid('Cannot convert selected index to valid OTP type')); + return saveFailedError(Errors.Invalid('Cannot convert selected index to valid OTP type')); } this.removeUserOTPProperties(err => { if(err) { - return cb(err); + return saveFailedError(err); } - return WebRegister.sendRegisterEmail(this.client.user, otpTypeProp, cb); + WebRegister.sendRegisterEmail(this.client.user, otpTypeProp, err => { + if(err) { + return saveFailedError(err); + } + + const info = this.config.saveEmailSent || DefaultMsg.saveEmailSent; + return this.displayDetails(info, cb); + }); }); } @@ -217,18 +274,18 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { } saveChangesDisable(cb) { - this.removeUserOTPProperties(this.client.user, err => { + this.removeUserOTPProperties(err => { if(err) { return cb(err); } - // :TODO: show "saved+disabled" art/message -> prevMenu - return cb(null); + const info = this.config.saveDisabled || DefaultMsg.saveDisabled; + return this.displayDetails(info, cb); }); } isOTPEnabledForUser() { - return this.otpTypeIndexFromUserOTPType(-1) != -1; + return this.client.user.getProperty(UserProps.AuthFactor2OTP) ? true : false; } getInfoText(key) { diff --git a/core/user_2fa_otp_web_register.js b/core/user_2fa_otp_web_register.js index a708969e..2b7d294c 100644 --- a/core/user_2fa_otp_web_register.js +++ b/core/user_2fa_otp_web_register.js @@ -19,6 +19,9 @@ const { const { sendMail } = require('./email.js'); const UserProps = require('./user_property.js'); const Log = require('./logger.js').log; +const { + getConnectionByUserId +} = require('./client_connections.js'); // deps const async = require('async'); @@ -104,7 +107,7 @@ module.exports = class User2FA_OTPWebRegister if(err) { Log.warn({ error : err.message }, 'Failed sending 2FA/OTP register email'); } else { - Log.info( { info }, 'Successfully sent 2FA/OTP register email'); + Log.info({ info }, 'Successfully sent 2FA/OTP register email'); } return callback(err); }); @@ -172,7 +175,7 @@ module.exports = class User2FA_OTPWebRegister .replace(/%TOKEN%/g, token) .replace(/%OTP_TYPE%/g, otpType) .replace(/%POST_URL%/g, postUrl) - .replace(/%QR_IMG_DATA%/g, otpInfo.qr) + .replace(/%QR_IMG_DATA%/g, otpInfo.qr || '') .replace(/%SECRET%/g, otpInfo.secret) ; return next(null, finalPage); @@ -232,6 +235,17 @@ module.exports = class User2FA_OTPWebRegister return webServer.instance.respondWithError(resp, 500, 'Internal Server Error', 'Internal Server Error'); } + // + // User may be online still - find account & update it if so + // + const clientConn = getConnectionByUserId(tokenInfo.user.userId); + if(clientConn && clientConn.user) { + // just update live props, we've already persisted them. + _.each(props, (v, n) => { + clientConn.user.setProperty(n, v); + }); + } + // we can now remove the token - no need to wait deleteToken(formData.token, err => { if(err) { @@ -261,7 +275,7 @@ ${backupCodes} [ { method : 'GET', - path : '^\\/enable_2fa_otp\\?token\\=[a-f0-9]+&otpType\\=[a-zA-Z0-9]+$', + path : '^\\/enable_2fa_otp\\?token\\=[a-f0-9]+&otpType\\=[a-zA-Z0-9_]+$', handler : User2FA_OTPWebRegister.routeRegisterGet, }, { diff --git a/www/otp_register.template.html b/www/otp_register.template.html index 22d00866..90eeba16 100644 --- a/www/otp_register.template.html +++ b/www/otp_register.template.html @@ -11,10 +11,12 @@ Your OTP secret:
%SECRET%

-

- QR Code:
- -

+
Confirm One-Time-Password to continue: From 0f68f20656d000110ae946d87bc2983046bcb31f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 14 Jun 2019 21:43:27 -0600 Subject: [PATCH 082/140] Documentation updates + Add Security doc * Update optuil doc --- core/oputil/oputil_help.js | 2 +- docs/admin/oputil.md | 225 +++++++++++++++++++++------------ docs/configuration/acs.md | 2 +- docs/configuration/security.md | 62 +++++++++ 4 files changed, 208 insertions(+), 83 deletions(-) create mode 100644 docs/configuration/security.md diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index fe76f4b7..62c47297 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -161,7 +161,7 @@ General Information: MessageBase : `usage: oputil.js mb [] -actions: +Actions: areafix CMD1 CMD2 ... ADDR Sends an AreaFix NetMail NetMail is sent to supplied address with the supplied command(s). Multi-part commands diff --git a/docs/admin/oputil.md b/docs/admin/oputil.md index e1164540..7190d268 100644 --- a/docs/admin/oputil.md +++ b/docs/admin/oputil.md @@ -9,17 +9,17 @@ Let's look the main help output as per this writing: ``` usage: oputil.js [--version] [--help] - [] + [] -global args: - -c, --config PATH specify config path (./config/) - -n, --no-prompt assume defaults/don't prompt for input where possible +Global arguments: + -c, --config PATH Specify config path (default is ./config/) + -n, --no-prompt Assume defaults (don't prompt for input where possible) -commands: - user user utilities - config config file management - fb file base management - mb message base management +Commands: + user User management + config Configuration management + fb File base management + mb Message base management ``` Commands break up operations by groups: @@ -41,21 +41,55 @@ Type `./oputil.js --help` for additional help on a particular command. The `user` command covers various user operations. ``` -usage: oputil.js user [] +usage: oputil.js user [] -actions: - info USERNAME display information about a user - pw USERNAME PASSWORD set a user's password - aliases: password, passwd - rm USERNAME permanently removes user from system - aliases: remove, delete, del - rename USERNAME NEWNAME rename a user - aliases: mv - activate USERNAME set status to active - deactivate USERNAME set status to inactive - disable USERNAME set status to disabled - lock USERNAME set status to locked - group USERNAME [+|-]GROUP adds (+) or removes (-) user from a group +Actions: + info USERNAME Display information about a user + + pw USERNAME PASSWORD Set a user's password + (passwd|password) + + rm USERNAME Permanently removes user from system + (del|delete|remove) + + rename USERNAME NEWNAME Rename a user + (mv) + + 2fa-otp USERNAME SPEC Enable 2FA/OTP for the user + (otp) + + The system supports various implementations of Two Factor Authentication (2FA) + One Time Password (OTP) authentication. + + Valid specs: + disable : Removes 2FA/OTP from the user + google : Google Authenticator + hotp : HMAC-Based One-Time Password Algorithm (RFC-4266) + totp : Time-Based One-Time Password Algorithm (RFC-6238) + + activate USERNAME Set a user's status to "active" + + deactivate USERNAME Set a user's status to "inactive" + + disable USERNAME Set a user's status to "disabled" + + lock USERNAME Set a user's status to "locked" + + group USERNAME [+|-]GROUP Adds (+) or removes (-) user from a group + +info arguments: + --security Include security information in output + +2fa-otp arguments: + --qr-type TYPE Specify QR code type + + Valid QR types: + ascii : Plain ASCII (default) + data : HTML data URL + img : HTML image tag + svg : SVG image + + --out PATH Path to write QR code to. defaults to stdout ``` | Action | Description | Examples | Aliases | @@ -64,25 +98,30 @@ actions: | `pw` | Set password | `./oputil.js user pw joeuser s3cr37` | `passwd`, `password` | | `rm` | Removes user | `./oputil.js user del joeuser` | `remove`, `del`, `delete` | | `rename` | Renames a user | `./oputil.js user rename joeuser joe` | `mv` | +| `2fa-otp` | Manage 2FA/OTP for a user | `./oputil.js user 2fa-otp joeuser googleAuth` | `otp` | `activate` | Activates user | `./oputil.js user activate joeuser` | N/A | | `deactivate` | Deactivates user | `./oputil.js user deactivate joeuser` | N/A | | `disable` | Disables user (user will not be able to login) | `./oputil.js user disable joeuser` | N/A | | `lock` | Locks the user account (prevents logins) | `./oputil.js user lock joeuser` | N/A | | `group` | Modifies users group membership | Add to group: `./oputil.js user group joeuser +derp`
Remove from group: `./oputil.js user group joeuser -derp` | N/A | +#### Manage 2FA/OTP +While `oputil.js` can be used to manage a user's 2FA/OTP, it is highly recommended to require users to opt-in themselves. See [Security](/docs/configuration/security.md) for details. + ## Configuration The `config` command allows sysops to perform various system configuration and maintenance tasks. ``` -usage: oputil.js config [] +usage: oputil.js config [] -actions: - new generate a new/initial configuration - cat cat current configuration to stdout +Actions: + new Generate a new / default configuration -cat args: - --no-color disable color - --no-comments strip any comments + cat Write current configuration to stdout + +cat arguments: + --no-color Disable color + --no-comments Strip any comments ``` | Action | Description | Examples | @@ -94,56 +133,75 @@ cat args: The `fb` command provides a powerful file base management interface. ``` -usage: oputil.js fb [] +usage: oputil.js fb [] -actions: - scan AREA_TAG[@STORAGE_TAG] scan specified area - may also contain optional GLOB as last parameter, - for example: scan some_area *.zip +Actions: + scan AREA_TAG[@STORAGE_TAG] Scan specified area - info CRITERIA display information about areas and/or files - matching CRITERIA. + May contain optional GLOB as last parameter. + Example: ./oputil.js fb scan d0pew4r3z *.zip - mv SRC [SRC...] DST move entry(s) from SRC to DST - SRC: FILENAME_WC|SHA|FILE_ID|AREA_TAG[@STORAGE_TAG] - DST: AREA_TAG[@STORAGE_TAG] + info CRITERIA Display information about areas and/or files - rm SRC [SRC...] remove entry(s) from the system matching SRC - SRC: FILENAME_WC|SHA|FILE_ID|AREA_TAG[@STORAGE_TAG] - desc CRITERIA sets a new file description for file base entry - matching CRITERIA. Launches an external editor using - $VISUAL, $EDITOR, or vim/notepad. - import-areas FILEGATE.ZXX import file base areas using FileGate RAID type format + mv SRC [SRC...] DST Move matching entry(s) + (move) -scan args: - --tags TAG1,TAG2,... specify tag(s) to assign to discovered entries + Source may be any of the following: + - Filename including '*' wildcards + - SHA-1 + - File ID + - Area tag with optional @storageTag suffix + Destination is area tag with optional @storageTag suffix - --desc-file [PATH] prefer file descriptions from supplied path over other - other sources such as FILE_ID.DIZ. Path must point to - a valid FILES.BBS or DESCRIPT.ION file. - --update attempt to update information for existing entries - --quick perform quick scan + rm SRC [SRC...] Remove entry(s) from the system + (del|delete|remove) -info args: - --show-desc display short description, if any + Source may be any of the following: + - Filename including '*' wildcards + - SHA-1 + - File ID + - Area tag with optional @storageTag suffix -remove args: - --phys-file also remove underlying physical file + desc CRITERIA Updates an file base entry's description -import-areas args: - --type TYPE sets import areas type. valid options are "zxx" or "na" - --create-dirs create backing storage directories + Launches an external editor using $VISUAL, $EDITOR, or vim/notepad. -general information: - AREA_TAG[@STORAGE_TAG] can specify an area tag and optionally, a storage specific tag - example: retro@bbs + import-areas FILEGATE.ZXX Import file base areas using FileGate RAID type format - CRITERIA file base entry criteria. in general, can be AREA_TAG, SHA, - FILE_ID, or FILENAME_WC. - - FILENAME_WC filename with * and ? wildcard support. may match 0:n entries - SHA full or partial SHA-256 - FILE_ID a file identifier. see file.sqlite3 +scan arguments: + --tags TAG1,TAG2,... Specify hashtag(s) to assign to discovered entries + + --desc-file [PATH] Prefer file descriptions from supplied input file + + If a file description can be found in the supplied input file, prefer that description + over other sources such related FILE_ID.DIZ. Path must point to a valid FILES.BBS or + DESCRIPT.ION file. + + --update Attempt to update information for existing entries + --full-scan Perform a full scan (default is quick) + +info arguments: + --show-desc Display short description, if any + +remove arguments: + --phys-file Also remove underlying physical file + +import-areas arguments: + --type TYPE Sets import areas type + + Valid types are are "zxx" or "na". + + --create-dirs Also create backing storage directories + +General Information: + Generally an area tag can also include an optional storage tag. For example, the + area of 'bbswarez' stored using 'bbswarez_main': bbswarez@bbswarez_main + + When performing an initial import of a large area or storage backing, --full-scan + is the best option. If re-scanning an area for updates a standard / quick scan is + generally good enough. + + File ID's are those found in file.sqlite3. ``` #### Scan File Area @@ -152,7 +210,8 @@ The `scan` action can (re)scan a file area for new entries as well as update (`- ##### Examples Performing a quick scan of a specific area's storage location ("retro_warez", "retro_warez_games) matching only *.zip extensions: ```bash -$ ./oputil.js fb scan --quick retro_warez@retro_warez_games *.zip` +# note that we must quote the wildcard to prevent shell expansion +$ ./oputil.js fb scan --quick retro_warez@retro_warez_games "*.zip"` ``` Update all entries in the "artscene" area supplying the file tags "artscene", and "textmode". @@ -221,19 +280,23 @@ The above command will process FILEGATE.ZXX creating areas and backing directori The `mb` command provides various Message Base related tools: ``` -usage: oputil.js mb [] +usage: oputil.js mb [] -actions: - areafix CMD1 CMD2 ... ADDR sends an AreaFix NetMail to ADDR with the supplied command(s) - one or more commands may be supplied. commands that are multi - part such as "%COMPRESS ZIP" should be quoted. - import-areas PATH import areas using fidonet *.NA or AREAS.BBS file from PATH +Actions: + areafix CMD1 CMD2 ... ADDR Sends an AreaFix NetMail -import-areas args: - --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 comma separated uplinks - --type TYPE area import type. valid options are "bbs" and "na" + NetMail is sent to supplied address with the supplied command(s). Multi-part commands + such as "%COMPRESS ZIP" should be quoted. + + import-areas PATH Import areas using FidoNet *.NA or AREAS.BBS file + +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" ``` | Action | Description | Examples | diff --git a/docs/configuration/acs.md b/docs/configuration/acs.md index 64d67648..dd57ce22 100644 --- a/docs/configuration/acs.md +++ b/docs/configuration/acs.md @@ -38,7 +38,7 @@ The following are ACS codes available as of this writing: | ACachievementCount | User has >= _achievementCount_ achievements | | APachievementPoints | User has >= _achievementPoints_ achievement points | | AFauthFactor | User's current *Authentication Factor* is >= _authFactor_. Authentication factor 1 refers to username + password (or PubKey) while factor 2 refers to 2FA such as One-Time-Password authentication. | -| ARauthFactorReq | Current users **requires** an Authentication Factor >= _authFactorReq_ | +| ARauthFactorReq | Current user **requires** an Authentication Factor >= _authFactorReq_ | ## ACS Strings ACS strings are one or more ACS codes in addition to some basic language semantics. diff --git a/docs/configuration/security.md b/docs/configuration/security.md new file mode 100644 index 00000000..ff2a568f --- /dev/null +++ b/docs/configuration/security.md @@ -0,0 +1,62 @@ +--- +layout: page +title: Security +--- +## Security +Unlike in the golden era of BBSing, modern Internet-connected systems are prone to hacking attempts, eavesdropping, etc. While plain-text passwords, insecure data over [Plain Old Telephone Service (POTS)](https://en.wikipedia.org/wiki/Plain_old_telephone_service), and so on was good enough then, modern systems must employ protections against attacks. ENiGMA½ comes with many security features that help keep the system and your users secure — not limited to: +* Passwords are **never** stored in plain-text, but instead are stored using [Password-Based Key Derivation Function 2 (PBKDF2)](https://en.wikipedia.org/wiki/PBKDF2). Even the system operator can _never_ know your password! +* Alternatives to insecure Telnet logins are built in: [SSH](https://en.wikipedia.org/wiki/Secure_Shell) and secure [WebSockets](https://en.wikipedia.org/wiki/WebSocket) for example. +* A built in web server with [TLS](https://en.wikipedia.org/wiki/Transport_Layer_Security) support (aka HTTPS). +* Optional [Two-Factor Authentication (2FA)](https://en.wikipedia.org/wiki/Multi-factor_authentication) via [One-Time-Password (OTP)](https://en.wikipedia.org/wiki/One-time_password) for users, supporting [Google Authenticator](http://google-authenticator.com/), [Time-Based One-Time Password Algorithm (TOTP, RFC-6238)](https://tools.ietf.org/html/rfc6238), and [HMAC-Based One-Time Password Algorithm (HOTP, RFC-4266)](https://tools.ietf.org/html/rfc4226). + +## Two-Factor Authentication via One-Time Password +Enabling Two-Factor Authentication via One-Time-Password (2FA/OTP) on an account adds an extra layer of security ("_something a user has_") in addition to their password ("_something a user knows_"). Providing 2FA/OTP to your users has some prerequisites: +* [A configured email gateway](/docs/configuration/email.md) such that the system can send out emails. +* One or more secure servers enabled such as [SSH](/docs/servers/ssh.md) or secure [WebSockets](/docs/servers/websocket.md) (that is, WebSockets over a secure connection such as TLS). +* The [web server](/docs/servers/web-server.md) enabled and exposed over TLS (HTTPS). + +:information_source: For WebSockets and the web server, ENiGMA½ _may_ listen on insecure channels if behind a secure web proxy. + +### User Registration Flow +Due to the nature of 2FA/OTP, even if enabled on your system, users must opt-in and enable this feature on their account. Users must also have a valid email address such that a registration link can be sent to them. To opt-in, a process similar to the following is taken: + +1. Navigate to the 2FA/OTP configuration menu and switches the feature to enabled. +2. Selects the "flavor" of 2FA/OTP: Google Authenticator, TOTP, or HOTP. +3. Confirms settings by saving. + +After saving, a registration link will be mailed to the user. Clicking the link provides the following: +1. A secret for manual entry into a OTP device. +2. If applicable, a scannable QR code for easy device entry (e.g. Google Authenticator) +3. A confirmation prompt in which the user must enter a OTP code. If entered correctly, this validates everything is set up properly and 2FA/OTP will be enabled for the account. Future logins will now prompt the user for their OTP after they enter their standard password. + +:warning: Serving 2FA/OTP registration links over insecure (HTTP) can expose secrets intended for the user and is **highly** discouraged! + +:information_source: +ops can also manually enable or disable 2FA/OTP for a user using [oputil](/docs/admin/oputil.md), but this is generally discouraged. + +### ACS Checks +Various places throughout the system that implement [ACS](/docs/configuration/acs.md) can make 2FA specific checks: +* `AR#`: Current users **required** authentication factor. `AR2` for example means 2FA/OTP is required for this user. +* `AF#`: Current users **active** authentication factor. `AF2` means the user is authenticated with some sort of 2FA (such as One-Time-Password). + +See [ACS](/docs/configuration/acs.md) for more information. + +#### Example +The following example illustrates using an `AR` ACS check to require applicable users to go through an additional 2FA/OTP process during login: + +```hjson +login: { + art: USERLOG + next: [ + { + // users with AR2+ must first pass 2FA/OTP + acs: AR2 + next: loginTwoFactorAuthOTP + } + { + // everyone else skips ahead + next: fullLoginSequenceLoginArt + } + ] + // ... +} +``` \ No newline at end of file From f02434bc23838f4257ef3740c161de6694c3791b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 15 Jun 2019 00:07:09 -0600 Subject: [PATCH 083/140] Add 2FA/OTP authentication to menu template --- misc/menu_template.in.hjson | 83 +++++++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 8 deletions(-) diff --git a/misc/menu_template.in.hjson b/misc/menu_template.in.hjson index 5c7eba83..9409a9c9 100644 --- a/misc/menu_template.in.hjson +++ b/misc/menu_template.in.hjson @@ -60,11 +60,20 @@ // // SSH connections are pre-authenticated via the SSH server itself. - // Jump directly to the login sequence + // Jump directly to either the 2FA/OTP auth or the login sequence + // depending on user ACS. // sshConnected: { art: CONNECT - next: fullLoginSequenceLoginArt + next: [ + { + acs: AR2 + next: loginTwoFactorAuthOTPLoop + } + { + next: mainMenu + } + ] config: { nextTimeout: 1500 } } @@ -90,11 +99,6 @@ submit: true focus: true argName: navSelect - // - // To enable forgot password, you will need to have the web server - // enabled and mail/SMTP configured. Once that is in place, swap out - // the commented lines below as well as in the submit block - // items: [ { text: login @@ -104,10 +108,20 @@ text: apply data: apply } + + // + // To enable the forgot password option, you'll need to have + // the web server & email configured. Once that is in place, + // uncomment the section below. + // + // See docs for more information + // + /* { text: forgot pass data: forgot } + */ { text: log off data: logoff @@ -142,7 +156,20 @@ login: { art: USERLOG - next: fullLoginSequenceLoginArt + next: [ + { + // + // Users with 2FA/OTP enabled *must* go through + // an additional OTP authentication step + // + acs: AR2 + next: loginTwoFactorAuthOTPLoop + } + { + // ...everyone else can carry on as per usual + next: fullLoginSequenceLoginArt + } + ] config: { tooNodeMenu: loginAttemptTooNode inactive: loginAttemptAccountInactive @@ -218,6 +245,46 @@ next: logoff } + // + // Empty menu to catch us in a 2FA/OTP auth loop + // until the user either authenticates successfully + // or the system boots them. + // + loginTwoFactorAuthOTPLoop: { + next: loginTwoFactorAuthOTP + } + + loginTwoFactorAuthOTP: { + art: 2FAOTP + next: fullLoginSequenceLoginArt + form: { + 0: { + mci: { + ET1: { + argName: token + focus: true + submit: true + } + } + submit: { + *: [ + { + value: { token: null } + action: @systemMethod:login2FA_OTP + } + ] + } + actionKeys: [ + { + // no turning back at this point... + keys: [ "escape" ] + action: @systemMethod:logoff + } + ] + } + } + } + forgotPassword: { desc: Forgot password prompt: forgotPasswordPrompt From b5a3c030abb159b510faea723495344ad4a7f5ab Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 15 Jun 2019 17:19:42 -0600 Subject: [PATCH 084/140] Doc updates & minor code cleanup --- WHATSNEW.md | 3 ++- core/user_2fa_otp_config.js | 49 +++++++++++++++++++++------------- docs/configuration/security.md | 16 ++++++----- 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index 71e12a69..76f8baf6 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -5,7 +5,8 @@ This document attempts to track **major** changes and additions in ENiGMA½. For + `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. + SSH Public Key Authentication has been added. The system uses a OpenSSH style public key set on the `ssh_public_key` user property. -+ 2-Factor (2FA) authentication is now available using [RFC-4266 - HOTP: HMAC-Based One-Time Password Algorithm)](https://tools.ietf.org/html/rfc4226), [RFC-6238 - TOTP: Time-Based One-Time Password Algorithm](https://tools.ietf.org/html/rfc6238), or [Google Authenticator](http://google-authenticator.com/). QR codes for activation are available as well. One-time backup aka recovery codes can also be used. ++ 2-Factor (2FA) authentication is now available using [RFC-4266 - HOTP: HMAC-Based One-Time Password Algorithm)](https://tools.ietf.org/html/rfc4226), [RFC-6238 - TOTP: Time-Based One-Time Password Algorithm](https://tools.ietf.org/html/rfc6238), or [Google Authenticator](http://google-authenticator.com/). QR codes for activation are available as well. One-time backup aka recovery codes can also be used. See [Security](/docs/configuration/security.md) for more info! +* New ACS codes for new 2FA/OTP: `AR` and `AF`. See [ACS](/docs/configuration/acs.md) for details. + `oputil.js user 2fa USERNAME TYPE` enables 2-factor authentication for a user. * `oputil.js user info USERNAME --security` can now display additional security information such as 2FA/OTP. * `oputil.js fb scan --quick` is now the default. Override with `--full-scan`. diff --git a/core/user_2fa_otp_config.js b/core/user_2fa_otp_config.js index 136c9178..e66f077b 100644 --- a/core/user_2fa_otp_config.js +++ b/core/user_2fa_otp_config.js @@ -40,13 +40,22 @@ const MciViewIds = { }; const DefaultMsg = { - otpNotEnabled : '2FA/OTP is not currently enabled for this account.', - noBackupCodes : 'No backup codes remaining or set.', - saveDisabled : '2FA/OTP is now disabled for this account.', - saveEmailSent : 'An 2FA/OTP registration email has been sent with further instructions.', - saveError : 'Failed to send email. Please contact the system operator.', - qrNotAvail : 'QR code not available for this OTP type.', - emailRequired : 'Your account must have a valid email address set to use this feature.', + infoText: { + disabled : 'Enabling 2-Factor Authentication via One-Time-Password can greatly increase the security of your account.', + enabled : 'A valid email address set in user config is required to enable 2-Factor Authentication.', + rfc6238_TOTP : 'Time-Based One-Time-Password (TOTP, RFC-6238).', + rfc4266_HOTP : 'HMAC-Based One-Time-Passowrd (HOTP, RFC-4266).', + googleAuth : 'Google Authenticator.', + }, + statusText : { + otpNotEnabled : '2FA/OTP is not currently enabled for this account.', + noBackupCodes : 'No backup codes remaining or set.', + saveDisabled : '2FA/OTP is now disabled for this account.', + saveEmailSent : 'An 2FA/OTP registration email has been sent with further instructions.', + saveError : 'Failed to send email. Please contact the system operator.', + qrNotAvail : 'QR code not available for this OTP type.', + emailRequired : 'Your account must have a valid email address set to use this feature.', + } }; exports.getModule = class User2FA_OTPConfigModule extends MenuModule { @@ -155,7 +164,7 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { let qrCode; if(!otp) { - qrCode = this.config.otpNotEnabled || DefaultMsg.otpNotEnabled; + qrCode = this.getStatusText('otpNotEnabled'); } else { const qrOptions = { username : this.client.user.username, @@ -171,7 +180,7 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { if(qrCode) { qrCode = qrCode.replace(/\n/g, '\r\n'); } else { - qrCode = this.config.qrNotAvail || DefaultMsg.qrNotAvail; + qrCode = this.getStatusText('qrNotAvail'); } } @@ -181,15 +190,15 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { showSecret(cb) { const info = this.client.user.getProperty(UserProps.AuthFactor2OTPSecret) || - this.config.otpNotEnabled || DefaultMsg.otpNotEnabled; + this.getStatusText('otpNotEnabled'); return this.displayDetails(info, cb); } showBackupCodes(cb) { let info; - const noBackupCodes = this.config.noBackupCodes || DefaultMsg.noBackupCodes; + const noBackupCodes = this.getStatusText('noBackupCodes'); if(!this.isOTPEnabledForUser()) { - info = this.config.otpNotEnabled || DefaultMsg.otpNotEnabled; + info = this.getStatusText('otpNotEnabled'); } else { try { info = JSON.parse(this.client.user.getProperty(UserProps.AuthFactor2OTPBackupCodes) || '[]').join(', '); @@ -203,7 +212,7 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { generateNewBackupCodes(cb) { if(!this.isOTPEnabledForUser()) { - const info = this.config.otpNotEnabled || DefaultMsg.otpNotEnabled; + const info = this.getStatusText('otpNotEnabled'); return this.displayDetails(info, cb); } @@ -231,14 +240,14 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { const emailRegExp = /[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(.[a-z0-9-]+)*/; const emailAddr = this.client.user.getProperty(UserProps.EmailAddress); if(!emailAddr || !emailRegExp.test(emailAddr)) { - const info = this.config.emailRequired || DefaultMsg.emailRequired; + const info = this.getStatusText('emailRequired'); return this.displayDetails(info, cb); } const otpTypeProp = this.otpTypeFromOTPTypeIndex(_.get(formData, 'value.otpType')); const saveFailedError = (err) => { - const info = this.config.saveError || DefaultMsg.saveError; + const info = this.getStatusText('saveError'); this.displayDetails(info, () => { return cb(err); }); @@ -258,7 +267,7 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { return saveFailedError(err); } - const info = this.config.saveEmailSent || DefaultMsg.saveEmailSent; + const info = this.getStatusText('saveEmailSent'); return this.displayDetails(info, cb); }); }); @@ -279,7 +288,7 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { return cb(err); } - const info = this.config.saveDisabled || DefaultMsg.saveDisabled; + const info = this.getStatusText('saveDisabled'); return this.displayDetails(info, cb); }); } @@ -289,7 +298,11 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { } getInfoText(key) { - return _.get(this.config, [ 'infoText', key ], ''); + return _.get(this.config, [ 'infoText', key ], DefaultMsg.infoText[key]); + } + + getStatusText(key) { + return _.get(this.config, [ 'statusText', key ], DefaultMsg.statusText[key]); } enableToggleUpdate(idx) { diff --git a/docs/configuration/security.md b/docs/configuration/security.md index ff2a568f..afc583aa 100644 --- a/docs/configuration/security.md +++ b/docs/configuration/security.md @@ -18,21 +18,23 @@ Enabling Two-Factor Authentication via One-Time-Password (2FA/OTP) on an account :information_source: For WebSockets and the web server, ENiGMA½ _may_ listen on insecure channels if behind a secure web proxy. ### User Registration Flow -Due to the nature of 2FA/OTP, even if enabled on your system, users must opt-in and enable this feature on their account. Users must also have a valid email address such that a registration link can be sent to them. To opt-in, a process similar to the following is taken: +Due to the nature of 2FA/OTP, even if enabled on your system, users must opt-in and enable this feature on their account. Users must also have a valid email address such that a registration link can be sent to them. To opt-in, users must enable the option, which will cause the system to email them a registration link. Following the link provides the following: -1. Navigate to the 2FA/OTP configuration menu and switches the feature to enabled. -2. Selects the "flavor" of 2FA/OTP: Google Authenticator, TOTP, or HOTP. -3. Confirms settings by saving. - -After saving, a registration link will be mailed to the user. Clicking the link provides the following: 1. A secret for manual entry into a OTP device. 2. If applicable, a scannable QR code for easy device entry (e.g. Google Authenticator) -3. A confirmation prompt in which the user must enter a OTP code. If entered correctly, this validates everything is set up properly and 2FA/OTP will be enabled for the account. Future logins will now prompt the user for their OTP after they enter their standard password. +3. A confirmation prompt in which the user must enter a OTP code. If entered correctly, this validates everything is set up properly and 2FA/OTP will be enabled for the account. Backup codes will also be provided at this time. Future logins will now prompt the user for their OTP after they enter their standard password. :warning: Serving 2FA/OTP registration links over insecure (HTTP) can expose secrets intended for the user and is **highly** discouraged! :information_source: +ops can also manually enable or disable 2FA/OTP for a user using [oputil](/docs/admin/oputil.md), but this is generally discouraged. +#### Recovery +In the situation that a user loses their 2FA/OTP device (such as a lost phone with Google Auth), there are some options: +* Utilize one of their backup codes. +* Contact the SysOp. + +:warning: There is no way for a user to disable 2FA/OTP without first fully logging in! This is by design as a security measure. + ### ACS Checks Various places throughout the system that implement [ACS](/docs/configuration/acs.md) can make 2FA specific checks: * `AR#`: Current users **required** authentication factor. `AR2` for example means 2FA/OTP is required for this user. From 91fa8243c8efce1abe32db805e4aa823855382ac Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 15 Jun 2019 18:11:54 -0600 Subject: [PATCH 085/140] Documentation + User 2FA/OTP Config mod docs * Add to nav --- core/user_2fa_otp_config.js | 11 +++---- docs/_includes/nav.md | 2 ++ docs/modding/top-x.md | 2 +- docs/modding/user-2fa-otp-config.md | 51 +++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 docs/modding/user-2fa-otp-config.md diff --git a/core/user_2fa_otp_config.js b/core/user_2fa_otp_config.js index e66f077b..61ef0a0c 100644 --- a/core/user_2fa_otp_config.js +++ b/core/user_2fa_otp_config.js @@ -31,20 +31,19 @@ const FormIds = { }; const MciViewIds = { - enableToggle : 1, - otpType : 2, - submit : 3, - infoText : 4, + enableToggle : 1, + otpType : 2, + submit : 3, customRangeStart : 10, // 10+ = customs }; const DefaultMsg = { infoText: { - disabled : 'Enabling 2-Factor Authentication via One-Time-Password can greatly increase the security of your account.', + disabled : 'Enabling 2-Factor Authentication via One-Time-Password (2FA/OTP) can greatly increase the security of your account.', enabled : 'A valid email address set in user config is required to enable 2-Factor Authentication.', rfc6238_TOTP : 'Time-Based One-Time-Password (TOTP, RFC-6238).', - rfc4266_HOTP : 'HMAC-Based One-Time-Passowrd (HOTP, RFC-4266).', + rfc4266_HOTP : 'HMAC-Based One-Time-Password (HOTP, RFC-4266).', googleAuth : 'Google Authenticator.', }, statusText : { diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index e67f1163..5ecbe58e 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -25,6 +25,7 @@ - [Colour Codes]({{ site.baseurl }}{% link configuration/colour-codes.md %}) - [Access Condition System (ACS)]({{ site.baseurl }}{% link configuration/acs.md %}) - [Event Scheduler]({{ site.baseurl }}{% link configuration/event-scheduler.md %}) + - [Security]({{ site.baseurl }}{% link configuration/security.md %}) - Scheduled jobs - File Base @@ -83,6 +84,7 @@ - [Set Newscan Date]({{ site.baseurl }}{% link modding/set-newscan-date.md %}) - [Node to Node Messaging]({{ site.baseurl }}{% link modding/node-msg.md %}) - [Top X]({{ site.baseurl }}{% link modding/top-x.md %}) + - [2FA/OTP Config]({{ site.baseurl }}{% link modding/user-2fa-otp-config.md %}) - Administration - [oputil]({{ site.baseurl }}{% link admin/oputil.md %}) diff --git a/docs/modding/top-x.md b/docs/modding/top-x.md index 50d69bee..41ac5ab7 100644 --- a/docs/modding/top-x.md +++ b/docs/modding/top-x.md @@ -48,7 +48,7 @@ mciMap: { } ``` -### Theming +## Theming Generally `mciMap` entries will point to a Vertical List View Menu (`%VM1`, `%VM2`, etc.). The following `itemFormat` object is provided: * `value`: The value acquired from the supplied data source. * `userName`: User's username. diff --git a/docs/modding/user-2fa-otp-config.md b/docs/modding/user-2fa-otp-config.md new file mode 100644 index 00000000..499f6083 --- /dev/null +++ b/docs/modding/user-2fa-otp-config.md @@ -0,0 +1,51 @@ +--- +layout: page +title: TopX +--- +## The 2FA/OTP Config Module +The `user_2fa_otp_config` module provides opt-in, configuration, and viewing of Two-Factor Authentication via One-Time-Password (2FA/OTP) settings. For more information on 2FA/OTP see [Security](/docs/configuration/security.md). + +## Configuration + +### Config Block +Available `config` block entries: +* `infoText`: Overrides default informational text string(s). See **Info Text** below. +* `statusText:` Overrides default status text string(s). See **Status Text** below. + +Example: +```hjson +config: { + infoText: { + googleAuth: Google Authenticator available on mobile phones, etc. + } + statusText: { + saveError: Doh! Failed to save :( + } +} +``` + +#### Info Text (infoText) +Overrides default informational text relative to current selections. Available keys: +* `disabled`: Displayed when OTP switched to enabled. +* `enabled`: Displayed when OTP switched to disabled. +* `rfc6238_TOTP`: Describes TOTP. +* `rfc4266_HOTP`: Describes HOTP. +* `googleAuth`: Describes Google Authenticator OTP. + +#### Status Text (statusText) +Overrides default status text for various conditions. Available keys: +* `otpNotEnabled` +* `noBackupCodes` +* `saveDisabled` +* `saveEmailSent` +* `saveError` +* `qrNotAvail` +* `emailRequired` + +## Theming +The following MCI codes are available: +* MCI 1: (ie: `TM1`): Toggle 2FA/OTP enabled/disabled. +* MCI 2: (ie: `SM2`): 2FA/OTP type selection. +* MCI 3: (ie: `TM3`): Submit/cancel toggle. +* MCI 10...99: Custom entries with the following format members available: + * `{infoText}`: **Info Text** for current selection. From 64c06d88093f66ef79ad19414ed65f9540a39f94 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 15 Jun 2019 18:41:58 -0600 Subject: [PATCH 086/140] Minor update on ACS defaults --- docs/modding/user-2fa-otp-config.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/modding/user-2fa-otp-config.md b/docs/modding/user-2fa-otp-config.md index 499f6083..5b0f045d 100644 --- a/docs/modding/user-2fa-otp-config.md +++ b/docs/modding/user-2fa-otp-config.md @@ -3,7 +3,9 @@ layout: page title: TopX --- ## The 2FA/OTP Config Module -The `user_2fa_otp_config` module provides opt-in, configuration, and viewing of Two-Factor Authentication via One-Time-Password (2FA/OTP) settings. For more information on 2FA/OTP see [Security](/docs/configuration/security.md). +The `user_2fa_otp_config` module provides opt-in, configuration, and viewing of Two-Factor Authentication via One-Time-Password (2FA/OTP) settings. In order to allow users access to 2FA/OTP, the system must be properly configured. See [Security](/docs/configuration/security.md) for more information. + +:information_source: By default, the 2FA/OTP configuration menu may only be accessed by users connected securely (ACS `SC`). It is highly recommended to leave this default as accessing these settings over a plain-text connection could expose private secrets! ## Configuration From 42ac6f86890bc816059551227207450b8747e8c5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 15 Jun 2019 18:42:15 -0600 Subject: [PATCH 087/140] ACS checks can now be applied to form actions --- WHATSNEW.md | 17 +++++++++++++++++ core/menu_util.js | 3 ++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index 76f8baf6..53dc54cb 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -10,6 +10,23 @@ This document attempts to track **major** changes and additions in ENiGMA½. For + `oputil.js user 2fa USERNAME TYPE` enables 2-factor authentication for a user. * `oputil.js user info USERNAME --security` can now display additional security information such as 2FA/OTP. * `oputil.js fb scan --quick` is now the default. Override with `--full-scan`. +* ACS checks can now be applied to form actions. For example: +```hjson +{ + value: { command: "SEC" } + action: [ + { + // secure connections can go here + acs: SC + action: @menu:securityMenu + } + { + // non-secure connections + action: @menu:secureConnectionRequired + } + ] +} +``` ## 0.0.9-alpha * Development is now against Node.js 10.x LTS. While other Node.js series may continue to work, you're own your own and YMMV! diff --git a/core/menu_util.js b/core/menu_util.js index c05f90d9..18b17d9f 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -172,7 +172,8 @@ function handleAction(client, formData, conf, cb) { return cb(Errors.MissingParam('Missing config')); } - const actionAsset = asset.parseAsset(conf.action); + const action = client.acs.getConditionalValue(conf.action, 'action'); // handle any conditionals + const actionAsset = asset.parseAsset(action); if(!_.isObject(actionAsset)) { return cb(Errors.Invalid('Unable to parse "conf.action"')); } From abc6a362751e6b8032e49c173f5d0cbb4d9bdd88 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 15 Jun 2019 22:18:51 -0600 Subject: [PATCH 088/140] OTP/2FA config template & luciano blocktronics theme --- .../luciano_blocktronics/2FACONFSCR.ans | Bin 0 -> 2672 bytes .../luciano_blocktronics/2FAOTPSECREQ.ans | Bin 0 -> 272 bytes art/themes/luciano_blocktronics/theme.hjson | 41 ++++++++ core/user_2fa_otp_config.js | 3 +- misc/menu_template.in.hjson | 96 ++++++++++++++++++ 5 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 art/themes/luciano_blocktronics/2FACONFSCR.ans create mode 100644 art/themes/luciano_blocktronics/2FAOTPSECREQ.ans diff --git a/art/themes/luciano_blocktronics/2FACONFSCR.ans b/art/themes/luciano_blocktronics/2FACONFSCR.ans new file mode 100644 index 0000000000000000000000000000000000000000..3c2560007a954e0fedf9f1c121b4844f005e5026 GIT binary patch literal 2672 zcmb_e%TC)s6opl#Y`dw-W;QIV+K!0}S%oxhS%e^B%7Rr$O;wc%uRwnb@jJ2VKdE}o zojYTjpss49#B=99&OP@Lw$7q{8uh!8HCtz~Em!qZl~uWQ7Fo#SAwH-CS*?J#ib}@t z(|sE?+3^-~BQ|BntX9rpmip_}ic*8%-NgRkSI?eIN5>{Vo!}7JcOv>U80M2>GdRh= zj1KdI;UGVl98x>7??u5(4o40B{9xQX9u20`)5-Bi4dXy4#KexKKm!hX!U5CVtIvQn zpDpIar7{4tbsoJ4mccHu`C#9HYCEu34}~{3>cHU9D9Oc4Z;^}OxLRJDyX91 zUmBF0KNqE4$4Z4wRTUA8y^#X>tRH?9zsw!DUChoe3v+X8=2zc9lR%24LG1au#rbUh z^Zr_N@4Rv#0N9!VsUVFlq)I?xwwT`*OEpfUx9y^(`n^~bx3gto7QpyI#E@Zyrd~^1 zggOUVt%|l$VX`wUF$r-$&1?x-8d<$b!(h;fAD6^Q(~P}|aG;1|1sAYHc?G*yj(fiH zB_F9f!Y6`l;B_`qdS#~Umii33?xwT=@^1-?*VaKt+)*kfwwsPuJ|hLiak=5 zE1aR|Y`bBH(4lfxFBZDuA`zGP#vjsGAN*eT@*ESiXfKE=IB@r24j>^OTmzSo*Pas) zURz+MB}JEyP$pF3oF6RnF0`N za;wxQlD>48Tl7I84#nY838^*XNLJF?U^Oz_-X2TG?|`+38zCF{=8y=xmpJV zD3YvvuC4YIk$gucUwZcOQ9&T?v$XvM@(#`#Dy~<>30%1;gesd<_p`LLGOVRv3!-CB zLm6su=K58Nqy{snmY BfCB&k literal 0 HcmV?d00001 diff --git a/art/themes/luciano_blocktronics/2FAOTPSECREQ.ans b/art/themes/luciano_blocktronics/2FAOTPSECREQ.ans new file mode 100644 index 0000000000000000000000000000000000000000..ce0079ac319ed0339eb9babd9d3892dcc1dd4bc3 GIT binary patch literal 272 zcmb1+Hn27^ur@Z&EX^!R zO;ISxS4d1wPAx80FmiL$_YVmGsxK+Y%u6p;1e;vdlz<;9w7h z{34hY;i*Z%`N`R-CB=GNQo)X)&aMUqAU9A67#SE^8kiZHPGewTU}OwnU Date: Sun, 16 Jun 2019 11:01:58 -0600 Subject: [PATCH 089/140] Add docs on template files --- art/themes/luciano_blocktronics/2FAOTP.ans | Bin 0 -> 295 bytes docs/modding/user-2fa-otp-config.md | 20 ++++++++++++++++++++ misc/otp_register_email.template.html | 11 +++++++++++ misc/otp_register_email.template.txt | 7 +++++++ www/otp_register.template.html | 8 ++++---- 5 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 art/themes/luciano_blocktronics/2FAOTP.ans create mode 100644 misc/otp_register_email.template.html create mode 100644 misc/otp_register_email.template.txt diff --git a/art/themes/luciano_blocktronics/2FAOTP.ans b/art/themes/luciano_blocktronics/2FAOTP.ans new file mode 100644 index 0000000000000000000000000000000000000000..7828fe6af5c6ce89ac5b0594c308c72988affcaf GIT binary patch literal 295 zcmb1+Hn27^ur@Z&<$?j}XhTD1>1boKTz@}Tg%D3)SA_t_;9v!yxV5oCu9Y-U3s4fs zH#W>wbqz5DN`PrDE}&GdbhIT0fnr#lU0c?yWmsGH0sI#kqfdch_k%6J5ftjJ%CI$uuM#caJ U9v}?_!k$jP3SsV!5FVTa039wl#Q*>R literal 0 HcmV?d00001 diff --git a/docs/modding/user-2fa-otp-config.md b/docs/modding/user-2fa-otp-config.md index 5b0f045d..7b41939f 100644 --- a/docs/modding/user-2fa-otp-config.md +++ b/docs/modding/user-2fa-otp-config.md @@ -51,3 +51,23 @@ The following MCI codes are available: * MCI 3: (ie: `TM3`): Submit/cancel toggle. * MCI 10...99: Custom entries with the following format members available: * `{infoText}`: **Info Text** for current selection. + +### Web and Email Templates +A template system is also available to customize registration emails and the landing page. + +#### Emails +Multipart MIME emails are send built using template files pointed to by `users.twoFactorAuth.otp.registerEmailText` and `users.toFactorAuth.otp.registerEmailHtml` supporting the following variables: +* `%BOARDNAME%`: BBS name. +* `%USERNAME%`: Username receiving email. +* `%TOKEN%`: Temporary registration token generally used in URL. +* `%REGISTER_URL%`: Full registration URL. + +#### Landing Page +The landing page template is pointed to by `users.twoFactorAuth.otp.registerPageTemplate` and supports the following variables: +* `%BOARDNAME%`: BBS name. +* `%USERNAME%`: Username receiving email. +* `%TOKEN%`: Temporary registration token generally used in URL. +* `%OTP_TYPE%`: OTP type such as `googleAuth`. +* `%POST_URL%`: URL to POST form to. +* `%QR_IMG_DATA%`: QR code in URL image data format. Not always available depending on OTP type and will be set to blank in these cases. +* `%SECRET%`: Secret for manual entry. diff --git a/misc/otp_register_email.template.html b/misc/otp_register_email.template.html new file mode 100644 index 00000000..f9fcd65b --- /dev/null +++ b/misc/otp_register_email.template.html @@ -0,0 +1,11 @@ +%USERNAME%:
+
+

+ You have requested to enable 2-Factor Authentication via One-Time-Password for your account on %BOARDNAME%.
+

+

+ If this was not you, please ignore this email and consider changing your password. Otherwise, please follow this link or copy and paste the link below:

+ %REGISTER_URL%
+

+ + diff --git a/misc/otp_register_email.template.txt b/misc/otp_register_email.template.txt new file mode 100644 index 00000000..c275515e --- /dev/null +++ b/misc/otp_register_email.template.txt @@ -0,0 +1,7 @@ +%USERNAME%: + +You have requested to enable 2-Factor Authentication via One-Time-Password for your account on %BOARDNAME%. + +If this was not you, please ignore this email and consider changing your password. Otherwise, please follow the link below: + +%REGISTER_URL% diff --git a/www/otp_register.template.html b/www/otp_register.template.html index 90eeba16..20a63ed2 100644 --- a/www/otp_register.template.html +++ b/www/otp_register.template.html @@ -1,11 +1,11 @@ - - + + Enable 2FA/OTP — ENiGMA½ BBS - - + +

Your OTP secret:
From abc6465af40458d8b3ec7ab13516d4c735c03989 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 16 Jun 2019 15:30:47 -0600 Subject: [PATCH 090/140] Template fixes --- core/user_2fa_otp_config.js | 2 +- misc/menu_template.in.hjson | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/core/user_2fa_otp_config.js b/core/user_2fa_otp_config.js index 8ad50425..1a7bf0c6 100644 --- a/core/user_2fa_otp_config.js +++ b/core/user_2fa_otp_config.js @@ -153,7 +153,7 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { } }; this.gotoMenu( - this.menuConfig.config.user2FAOTP_ShowDetails || 'user2FAOTP_ShowDetails', + this.menuConfig.config.userTwoFactorAuthOTPConfigShowDetails || 'userTwoFactorAuthOTPConfigShowDetails', modOpts, cb ); diff --git a/misc/menu_template.in.hjson b/misc/menu_template.in.hjson index 9de82c76..9be72102 100644 --- a/misc/menu_template.in.hjson +++ b/misc/menu_template.in.hjson @@ -1304,6 +1304,19 @@ userTwoFactorAuthOTPSecConnRequired: { desc: Insecure Warning art: 2FAOTPSECREQ + config: { + cls: true + pause: true + } + } + + userTwoFactorAuthOTPConfigShowDetails: { + desc: 2FA/OTP Details + module: show_art + config: { + pause: true + method: extraArgs + } } nodeMessage: { From b9a2881c88b44765dc44410723f96786e755d2ac Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 16 Jun 2019 15:32:57 -0600 Subject: [PATCH 091/140] Fix luciano theme --- art/themes/luciano_blocktronics/theme.hjson | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index ab49f1fa..52a4f038 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -396,8 +396,8 @@ TM1: { width: 20 items: [ - "enabled" "disabled" + "enabled" ] focusTextStyle: upper styleSGR1: |08 From ec3b6173c924128095896394dfe5fa170c1b55ae Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 16 Jun 2019 17:28:13 -0600 Subject: [PATCH 092/140] Better 'secure connection required' screen --- .../luciano_blocktronics/2FAOTPSECREQ.ans | Bin 272 -> 248 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/art/themes/luciano_blocktronics/2FAOTPSECREQ.ans b/art/themes/luciano_blocktronics/2FAOTPSECREQ.ans index ce0079ac319ed0339eb9babd9d3892dcc1dd4bc3..c81720ec0e3b1386639e8aaea7e74ca87e2f57c6 100644 GIT binary patch literal 248 zcmb1+Hn27^ur@Z&=kf>0cnp|207thSkQz*$-C@oG^D9OkyR!B=tECEVOM*}TDGMP&%*fG@E r)xZE~AGLsyfuW^=nW0%N0|NsiV*mpikOl%_PbXi6Fn31?4^9FAN5L~f literal 272 zcmb1+Hn27^ur@Z&EX^!R zO;ISxS4d1wPAx80FmiL$_YVmGsxK+Y%u6p;1e;vdlz<;9w7h z{34hY;i*Z%`N`R-CB=GNQo)X)&aMUqAU9A67#SE^8kiZHPGewTU}OwnU Date: Sun, 16 Jun 2019 18:56:25 -0600 Subject: [PATCH 093/140] Template minor fix --- misc/menu_template.in.hjson | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/menu_template.in.hjson b/misc/menu_template.in.hjson index 9be72102..a2a183a5 100644 --- a/misc/menu_template.in.hjson +++ b/misc/menu_template.in.hjson @@ -71,7 +71,7 @@ next: loginTwoFactorAuthOTPLoop } { - next: mainMenu + next: fullLoginSequenceLoginArt } ] config: { nextTimeout: 1500 } From 20d3b0fec9f0b58bf62d6c9323f4abc2c59daef5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 17 Jun 2019 21:08:50 -0600 Subject: [PATCH 094/140] Doh, fix username vs realname --- README.md | 1 + core/fse.js | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b72d42cb..92c9a940 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ ENiGMA½ is a modern BBS software with a nostalgic flair! * Renegade style [pipe color codes](/docs/configuration/colour-codes.md). * [SQLite](http://sqlite.org/) storage of users, message areas, etc. * Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password encryption. + * Support for 2-Factor Authentication with One-Time-Passwords * [Door support](docs/modding/door-servers.md) including common dropfile formats for legacy DOS doors. Built in [BBSLink](http://bbslink.net/), [DoorParty](http://forums.throwbackbbs.com/), [Exodus](https://oddnetwork.org/exodus/) and [CombatNet](http://combatnet.us/) support! * [Bunyan](https://github.com/trentm/node-bunyan) logging! * [Message networks](docs/messageareas/message-networks.md) with FidoNet Type Network (FTN) + BinkleyTerm Style Outbound (BSO) message import/export. Messages Bases can also be exposed via [Gopher](docs/servers/gopher.md), or [NNTP](docs/servers/nntp.md)! diff --git a/core/fse.js b/core/fse.js index d86961b2..890fb009 100644 --- a/core/fse.js +++ b/core/fse.js @@ -326,10 +326,17 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul buildMessage(cb) { const headerValues = this.viewControllers.header.getFormData().value; + const getFromUserName = () => { + const area = getMessageAreaByTag(this.messageAreaTag); + return (area && area.realNames) ? + this.client.user.getProperty(UserProps.RealName) || this.client.user.username : + this.client.user.username; + }; + const msgOpts = { areaTag : this.messageAreaTag, toUserName : headerValues.to, - fromUserName : this.client.user.getProperty(UserProps.RealName) || this.client.user.username, + fromUserName : getFromUserName(), subject : headerValues.subject, // :TODO: don't hard code 1 here: message : this.viewControllers.body.getView(MciViewIds.body.message).getData( { forceLineTerms : this.replyIsAnsi } ), From 2b2b1ef3bc13401fee9a5ef05f57ea05c41531f5 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sat, 22 Jun 2019 23:13:38 +0100 Subject: [PATCH 095/140] Decrease MRC gui by one line --- art/themes/luciano_blocktronics/mrc.ans | Bin 721 -> 720 bytes art/themes/luciano_blocktronics/theme.hjson | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/art/themes/luciano_blocktronics/mrc.ans b/art/themes/luciano_blocktronics/mrc.ans index a3079f3c879ba6ccb6f8aac3571711279406892e..aba8b14259739f0c37f92bcef76ca5a830c48110 100644 GIT binary patch delta 51 rcmcb}dVzIAFyrPB#&AZpbS4G{M#caJ2?j6__H^=92y=IY@Zcl>1vCco delta 51 rcmcb>dXaTQFyrPR#t25WOeO{fM#caJNd_1#brQ diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 52a4f038..86f1ed3b 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -1128,7 +1128,7 @@ mci: { MT1: { width: 72 - height: 19 + height: 18 } ET2: { width: 69 // fnarr! From 65b48a2af2547f5de6ee8cf0dd6a4b2f8198f67e Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sat, 22 Jun 2019 23:16:08 +0100 Subject: [PATCH 096/140] Fix MRC prettyBoardName and /INFO --- core/servers/chat/mrc_multiplexer.js | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js index fd83d462..67ca9dc6 100644 --- a/core/servers/chat/mrc_multiplexer.js +++ b/core/servers/chat/mrc_multiplexer.js @@ -36,6 +36,7 @@ exports.getModule = class MrcModule extends ServerModule { this.log = Log.child( { server : 'MRC' } ); const config = Config(); + this.boardName = config.general.prettyBoardName || config.general.boardName; this.mrcConnectOpts = { host : config.chatServers.mrc.serverHostname || 'mrc.bottomlessabyss.net', port : config.chatServers.mrc.serverPort || 5000, @@ -44,11 +45,9 @@ exports.getModule = class MrcModule extends ServerModule { } _connectionHandler() { - const config = Config(); - const boardName = config.general.prettyBoardName || config.general.boardName; const enigmaVersion = 'ENiGMA½-BBS_' + require('../../../package.json').version; - const handshake = `${boardName}~${enigmaVersion}/${os.platform()}.${os.arch()}/${protocolVersion}`; + const handshake = `${this.boardName}~${enigmaVersion}/${os.platform()}.${os.arch()}/${protocolVersion}`; this.log.debug({ handshake : handshake }, 'Handshaking with MRC server'); this.sendRaw(handshake); @@ -209,9 +208,7 @@ exports.getModule = class MrcModule extends ServerModule { * Processes messages received from the central MRC server */ receiveFromMRC(message) { - const config = Config(); - const siteName = slugify(config.general.boardName); if (message.from_user == 'SERVER' && message.body == 'HELLO') { // reply with extra bbs info @@ -223,7 +220,7 @@ exports.getModule = class MrcModule extends ServerModule { } else if (message.from_user == 'SERVER' && message.body.toUpperCase() == 'PING') { // reply to heartbeat - this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `IMALIVE:${siteName}`); + this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `IMALIVE:${this.boardName}`); } else { // if not a heartbeat, and we have clients then we need to send something to them @@ -263,12 +260,10 @@ exports.getModule = class MrcModule extends ServerModule { * Converts a message back into the MRC format and sends it to the central MRC server */ sendToMrcServer(fromUser, fromRoom, toUser, toSite, toRoom, messageBody) { - const config = Config(); - const siteName = slugify(config.general.boardName); const line = [ fromUser, - siteName, + this.boardName, sanitiseRoomName(fromRoom), sanitiseName(toUser || ''), sanitiseName(toSite || ''), @@ -307,14 +302,3 @@ function sanitiseMessage(message) { return message.replace(/[^\x20-\x7D]/g, ''); } -/** - * SLugifies the BBS name for use as an MRC "site name" - */ -function slugify(text) { - return text.toString() - .replace(/\s+/g, '_') // Replace spaces with _ - .replace(/[^\w\-]+/g, '') // Remove all non-word chars - .replace(/\-\-+/g, '_') // Replace multiple - with single - - .replace(/^-+/, '') // Trim - from start of text - .replace(/-+$/, ''); // Trim - from end of text -} \ No newline at end of file From fdce4c939cbb093024d2134b6c88d0bce6ef6904 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 22 Jun 2019 20:26:58 -0600 Subject: [PATCH 097/140] Minor --- core/fse.js | 1 - 1 file changed, 1 deletion(-) diff --git a/core/fse.js b/core/fse.js index 890fb009..d77fbfed 100644 --- a/core/fse.js +++ b/core/fse.js @@ -338,7 +338,6 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul toUserName : headerValues.to, fromUserName : getFromUserName(), subject : headerValues.subject, - // :TODO: don't hard code 1 here: message : this.viewControllers.body.getView(MciViewIds.body.message).getData( { forceLineTerms : this.replyIsAnsi } ), }; From 2f391bdbab3d0d0857d2be111a38acd265290a21 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 30 Jun 2019 19:25:47 -0600 Subject: [PATCH 098/140] Spelling --- core/msg_list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/msg_list.js b/core/msg_list.js index f73dae8a..380fc5e4 100644 --- a/core/msg_list.js +++ b/core/msg_list.js @@ -227,7 +227,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( return callback(err); }); }, - function getLastReadMesageId(callback) { + function getLastReadMessageId(callback) { // messageList entries can contain |isNew| if they want to be considered new if(configProvidedMessageList) { self.lastReadId = 0; From 53aea7098fea9c8a8ea31f4d736398162f7552ee Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 6 Jul 2019 21:15:19 -0600 Subject: [PATCH 099/140] Deps updated working fine --- package.json | 34 ++++---- yarn.lock | 239 ++++++++++++++++++++++++++------------------------- 2 files changed, 139 insertions(+), 134 deletions(-) diff --git a/package.json b/package.json index bbc8a94f..235018a7 100644 --- a/package.json +++ b/package.json @@ -22,41 +22,41 @@ "retro" ], "dependencies": { - "async": "^2.6.2", - "binary-parser": "^1.3.2", + "async": "3.1.0", + "binary-parser": "1.4.0", "buffers": "github:NuSkooler/node-buffers", "bunyan": "^1.8.12", "exiftool": "^0.0.3", - "fs-extra": "^7.0.1", - "glob": "^7.1.2", - "graceful-fs": "^4.1.15", + "fs-extra": "8.1.0", + "glob": "7.1.4", + "graceful-fs": "4.2.0", "hashids": "^1.1.1", "hjson": "^3.1.2", - "iconv-lite": "^0.4.23", - "inquirer": "^6.2.2", + "iconv-lite": "0.5.0", + "inquirer": "6.4.1", "later": "1.2.0", "lodash": "^4.17.10", "lru-cache": "^5.1.1", - "mime-types": "^2.1.22", + "mime-types": "2.1.24", "minimist": "1.2.x", "moment": "^2.24.0", "nntp-server": "^1.0.3", "node-pty": "^0.8.1", - "nodemailer": "^5.1.1", + "nodemailer": "6.2.1", + "otplib": "11.0.1", + "qrcode-generator": "1.4.3", "rlogin": "^1.0.0", - "sane": "^4.0.2", + "sane": "4.1.0", "sanitize-filename": "^1.6.1", - "sqlite3": "^4.0.6", + "sqlite3": "4.0.9", "sqlite3-trans": "^1.2.1", - "ssh2": "^0.8.2", + "ssh2": "0.8.4", "temptmp": "^1.1.0", "uuid": "^3.2.1", - "uuid-parse": "^1.0.0", - "ws": "^6.1.3", + "uuid-parse": "1.1.0", + "ws": "7.0.1", "xxhash": "^0.2.4", - "yazl": "^2.5.1", - "otplib": "^10.0.1", - "qrcode-generator": "1.4.3" + "yazl": "^2.5.1" }, "devDependencies": {}, "engines": { diff --git a/yarn.lock b/yarn.lock index 0b081218..d9aade26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,14 @@ # yarn lockfile v1 +"@cnakazawa/watch@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" + integrity sha512-r5160ogAvGyHsal38Kux7YYtodEKOj89RGb28ht1jh3SJb08VwRwAKKJL0bGb04Zd/3r9FL3BFIc3bBidYffCA== + dependencies: + exec-sh "^0.3.2" + minimist "^1.2.0" + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -32,10 +40,10 @@ ansi-regex@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= -ansi-regex@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.0.0.tgz#70de791edf021404c3fd615aa89118ae0432e5a9" - integrity sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w== +ansi-regex@^4.1.0: + version "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" @@ -114,17 +122,15 @@ assign-symbols@^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-limiter@^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@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.2.tgz#18330ea7e6e313887f5d2f2a904bac6fe4dd5381" - integrity sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg== - dependencies: - lodash "^4.17.11" +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" @@ -171,10 +177,10 @@ bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2: dependencies: tweetnacl "^0.14.3" -binary-parser@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/binary-parser/-/binary-parser-1.3.2.tgz#5bd04f948ada1a6d78c528762308a9a335d63db9" - integrity sha512-VDhHcpeF1/ZZy1XvDmYD67bBjRNm1gacw+772xNd5BnTH6ax5TzlDV5dl7216/UlQXQoN9vug07ehk7e0PhNUw== +binary-parser@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/binary-parser/-/binary-parser-1.4.0.tgz#5f32d9b28f2027968dc660b680699dc030a6ea92" + integrity sha512-z4TOFQFy5YzhEy50mM+WV6BRu3xo0Vpe5qEtVwnUZkhibuNrPKkBXTpzSs2xcSW6TMVeXas77fEsEVV+lj4/iw== brace-expansion@^1.1.7: version "1.1.11" @@ -241,12 +247,12 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" -capture-exit@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-1.2.0.tgz#1c5fcc489fd0ab00d4f1ac7ae1072e3173fbab6f" - integrity sha1-HF/MSJ/QqwDU8ax64QcuMXP7q28= +capture-exit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" + integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g== dependencies: - rsvp "^3.3.3" + rsvp "^4.8.4" caseless@~0.12.0: version "0.12.0" @@ -491,13 +497,6 @@ escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= -exec-sh@^0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.2.tgz#2a5e7ffcbd7d0ba2755bdecb16e5a427dfbdec36" - integrity sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw== - dependencies: - merge "^1.2.0" - exec-sh@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b" @@ -655,12 +654,12 @@ from2@^2.3.0: inherits "^2.0.1" readable-stream "^2.0.0" -fs-extra@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" - integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== +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== dependencies: - graceful-fs "^4.1.2" + graceful-fs "^4.2.0" jsonfile "^4.0.0" universalify "^0.1.0" @@ -709,6 +708,18 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +glob@7.1.4: + version "7.1.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" + integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^6.0.1: version "6.0.4" resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" @@ -743,12 +754,12 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" -graceful-fs@^4.1.15: - version "4.1.15" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" - integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== +graceful-fs@4.2.0, graceful-fs@^4.2.0: + version "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.1.2, graceful-fs@^4.1.6: +graceful-fs@^4.1.6: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" integrity sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg= @@ -826,7 +837,14 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" -iconv-lite@^0.4.23, iconv-lite@^0.4.24, iconv-lite@^0.4.4: +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== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@^0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -858,10 +876,10 @@ ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== -inquirer@^6.2.2: - version "6.2.2" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.2.tgz#46941176f65c9eb20804627149b743a218f25406" - integrity sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA== +inquirer@6.4.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.4.1.tgz#7bd9e5ab0567cd23b41b0180b68e0cfa82fc3c0b" + integrity sha512-/Jw+qPZx4EDYsaT6uz7F4GJRNFMRdKNeUZw3ZnKV8lyuUgz/YWRCSUAJMZSVhSq4Ec0R2oYnyi6b3d4JXcL5Nw== dependencies: ansi-escapes "^3.2.0" chalk "^2.4.2" @@ -874,7 +892,7 @@ inquirer@^6.2.2: run-async "^2.2.0" rxjs "^6.4.0" string-width "^2.1.0" - strip-ansi "^5.0.0" + strip-ansi "^5.1.0" through "^2.3.6" is-accessor-descriptor@^0.1.6: @@ -1129,11 +1147,6 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -merge@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" - integrity sha1-dTHjnUlJwoGma4xabgJl6LBYlNo= - micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -1153,15 +1166,22 @@ micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" +mime-db@1.40.0: + version "1.40.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" + integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== + 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-db@~1.38.0: - version "1.38.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad" - integrity sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg== +mime-types@2.1.24: + version "2.1.24" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" + integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== + dependencies: + mime-db "1.40.0" mime-types@^2.1.12, mime-types@~2.1.19: version "2.1.20" @@ -1170,13 +1190,6 @@ mime-types@^2.1.12, mime-types@~2.1.19: dependencies: mime-db "~1.36.0" -mime-types@^2.1.22: - version "2.1.22" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.22.tgz#fe6b355a190926ab7698c9a0556a11199b2199bd" - integrity sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog== - dependencies: - mime-db "~1.38.0" - mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" @@ -1273,10 +1286,10 @@ nan@^2.10.0, nan@^2.4.0: resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.1.tgz#90e22bccb8ca57ea4cd37cc83d3819b52eea6766" integrity sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA== -nan@~2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" - integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== +nan@^2.12.1: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== nanomatch@^1.2.9: version "1.2.13" @@ -1358,10 +1371,10 @@ node-pty@^0.8.1: dependencies: nan "2.12.1" -nodemailer@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-5.1.1.tgz#0c48d1ecab02e86d9ff6c620ee75ed944b763505" - integrity sha512-hKGCoeNdFL2W7S76J/Oucbw0/qRlfG815tENdhzcqTpSjKgAN91mFOqU2lQUflRRxFM7iZvCyaFcAR9noc/CqQ== +nodemailer@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.2.1.tgz#20d773925eb8f7a06166a0b62c751dc8290429f3" + integrity sha512-TagB7iuIi9uyNgHExo8lUDq3VK5/B0BpbkcjIgNvxbtVrjNqq0DwAOTuzALPVkK76kMhTSzIgHqg8X1uklVs6g== nopt@^4.0.1: version "4.0.1" @@ -1478,10 +1491,10 @@ osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" -otplib@^10.0.1: - version "10.0.1" - resolved "https://registry.yarnpkg.com/otplib/-/otplib-10.0.1.tgz#d37fcd13203298c0b94937d55c5a3527ed877875" - integrity sha512-FtbKelYtio2af5LDBWz3bWS6T03taHJAIv3evMrXuvoM50z5jbWoEMabPCk0A0JqiLGBzAIDJWfR9gSsvRYZHA== +otplib@11.0.1: + version "11.0.1" + resolved "https://registry.yarnpkg.com/otplib/-/otplib-11.0.1.tgz#7d64aa87029f07c99c7f96819fb10cdb67dea886" + integrity sha512-oi57teljNyWTC/JqJztHOtSGeFNDiDh5C1myd+faocUtFAX27Sm1mbx69kpEJ8/JqrblI3kAm4Pqd6tZJoOIBQ== dependencies: thirty-two "1.0.2" @@ -1698,10 +1711,10 @@ rlogin@^1.0.0: resolved "https://registry.yarnpkg.com/rlogin/-/rlogin-1.0.0.tgz#db07322b31219126625d9d0aa9872d7ebe8ac403" integrity sha1-2wcyKzEhkSZiXZ0KqYctfr6KxAM= -rsvp@^3.3.3: - version "3.6.2" - resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a" - integrity sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw== +rsvp@^4.8.4: + version "4.8.5" + 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" @@ -1739,20 +1752,20 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sane@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/sane/-/sane-4.0.2.tgz#5bd4a3f1268fd7a921a2dc657047de635c8f8f25" - integrity sha512-/3STCUfNSgMVpoREJc1i6ajKFlYZ5OflzZTOhlqPLa+01Ey+QR9iGZK7K5/qIRsQbEDCvqEJH/PL7yZywmnWsA== +sane@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded" + integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA== dependencies: + "@cnakazawa/watch" "^1.0.3" anymatch "^2.0.0" - capture-exit "^1.2.0" + capture-exit "^2.0.0" exec-sh "^0.3.2" execa "^1.0.0" fb-watchman "^2.0.0" micromatch "^3.1.4" minimist "^1.1.1" walker "~1.0.5" - watch "~0.18.0" sanitize-filename@^1.6.1: version "1.6.1" @@ -1895,30 +1908,30 @@ sqlite3-trans@^1.2.1: dependencies: lodash "^4.17.4" -sqlite3@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-4.0.6.tgz#e587b583b5acc6cb38d4437dedb2572359c080ad" - integrity sha512-EqBXxHdKiwvNMRCgml86VTL5TK1i0IKiumnfxykX0gh6H6jaKijAXvE9O1N7+omfNSawR2fOmIyJZcfe8HYWpw== +sqlite3@4.0.9: + version "4.0.9" + resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-4.0.9.tgz#cff74550fa5a1159956815400bdef69245557640" + integrity sha512-IkvzjmsWQl9BuBiM4xKpl5X8WCR4w0AeJHRdobCdXZ8dT/lNc1XS6WqvY35N6+YzIIgzSBeY5prdFObID9F9tA== dependencies: - nan "~2.10.0" + nan "^2.12.1" node-pre-gyp "^0.11.0" request "^2.87.0" -ssh2-streams@~0.4.2: - version "0.4.2" - resolved "https://registry.yarnpkg.com/ssh2-streams/-/ssh2-streams-0.4.2.tgz#bac0d18727396d16049f5f0c8517a46516b45719" - integrity sha512-2rSj3oTIJnbAIzR3+XwIYef9wCOVrPQZNLL+fFPPjnPxf09tKkAbgrlYgh/1qynBTz65AUOS+s1zuko4M/GKCw== +ssh2-streams@~0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/ssh2-streams/-/ssh2-streams-0.4.4.tgz#7f07464c4b19ee93324995ec7164f110c5a13658" + integrity sha512-yNfPZgJO/N69TvYkpDHZBkXAXQzTpfzRkOphQu3PeUpZnrjp9VNa8RKDZkZDpjsWItay+I4NMAbZZ7DqHRt0AQ== dependencies: asn1 "~0.2.0" bcrypt-pbkdf "^1.0.2" streamsearch "~0.1.2" -ssh2@^0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-0.8.2.tgz#f7a172458d3a7a13d520438264f90de8a3ee72af" - integrity sha512-oaXu7faddvPFGavnLBkk0RFwLXvIzCPq6KqAC3ExlnFPAVIE1uo7pWHe9xmhNHXm+nIe7yg9qsssOm+ip2jijw== +ssh2@0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-0.8.4.tgz#0a657d9371c1fe9f9e349bcff6144febee256aa6" + integrity sha512-qztb9t4b34wJSiWVpeTMVVN/5KCuBoyctBc2BcSe/Uq4NRnF0gB16Iu5p72ILhdYATcMNwB5WppzPIEs/3wB8Q== dependencies: - ssh2-streams "~0.4.2" + ssh2-streams "~0.4.4" sshpk@^1.7.0: version "1.14.2" @@ -1994,12 +2007,12 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.0.0.tgz#f78f68b5d0866c20b2c9b8c61b5298508dc8756f" - integrity sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow== +strip-ansi@^5.1.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.0.0" + ansi-regex "^4.1.0" strip-eof@^1.0.0: version "1.0.0" @@ -2168,10 +2181,10 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= -uuid-parse@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/uuid-parse/-/uuid-parse-1.0.0.tgz#f4657717624b0e4b88af36f98d89589a5bbee569" - integrity sha1-9GV3F2JLDkuIrzb5jYlYmlu+5Wk= +uuid-parse@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/uuid-parse/-/uuid-parse-1.1.0.tgz#7061c5a1384ae0e1f943c538094597e1b5f3a65b" + integrity sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A== uuid@^3.2.1, uuid@^3.3.2: version "3.3.2" @@ -2194,14 +2207,6 @@ walker@~1.0.5: dependencies: makeerror "1.0.x" -watch@~0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986" - integrity sha1-KAlUdsbffJDJYxOJkMClQj60uYY= - dependencies: - exec-sh "^0.2.0" - minimist "^1.2.0" - which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -2221,12 +2226,12 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -ws@^6.1.3: - version "6.1.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.3.tgz#d2d2e5f0e3c700ef2de89080ebc0ac6e1bf3a72d" - integrity sha512-tbSxiT+qJI223AP4iLfQbkbxkwdFcneYinM2+x46Gx2wgvbaOMO36czfdfVUBRTHvzAMRhDd98sA5d/BuWbQdg== +ws@7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.0.1.tgz#1a04e86cc3a57c03783f4910fdb090cf31b8e165" + integrity sha512-ILHfMbuqLJvnSgYXLgy4kMntroJpe8hT41dOVWM8bxRuw6TK4mgMp9VJUNsZTEc5Bh+Mbs0DJT4M0N+wBG9l9A== dependencies: - async-limiter "~1.0.0" + async-limiter "^1.0.0" xtend@~4.0.1: version "4.0.1" From 8fac7cb69f791d470c02fa5b7fbf817170a2fca0 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 6 Jul 2019 22:49:12 -0600 Subject: [PATCH 100/140] README updates --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 92c9a940..efa593cd 100644 --- a/README.md +++ b/README.md @@ -53,12 +53,12 @@ ENiGMA has been tested with many terminals. However, the following are suggested * [NetRunner](http://mysticbbs.com/downloads.html) * [MagiTerm](https://magickabbs.com/index.php/magiterm/) -## Boards -* WQH: :skull: [Xibalba](https://l33t.codes/xibalba-bbs) :skull: (**ssh://xibalba.l33t.codes:44511** or **telnet://xibalba.l33t.codes:44510**) +## Some Boards +* :skull: [Xibalba - ENiGMA WHQ](https://l33t.codes/xibalba-bbs) :skull: (**ssh://xibalba.l33t.codes:44511** or **telnet://xibalba.l33t.codes:44510**) * [fORCE9](http://bbs.force9.org/): (**telnet://bbs.force9.org**) * [Undercurrents](https://undercurrents.io): (**ssh://undercurrents.io**) * [PlaneT Afr0](https://planetafr0.org/): (**ssh://planetafr0.org:8889**) - +* [Goblin Studio](https://goblin.strangled.net): (**ssh://goblin.strangled.net:8889**) ## Installation On *nix type systems: @@ -82,6 +82,7 @@ Please see [Installation Methods](https://nuskooler.github.io/enigma-bbs/install * [Apam](https://github.com/apamment) of [Magicka](https://magickabbs.com/) * [nail/blocktronics](http://blocktronics.org/tag/nail/) for the [sickmade Xibalba logo](http://pc.textmod.es/pack/blocktronics-420/n-xbalba.ans)! * [Whazzit/blocktronics](http://blocktronics.org/tag/whazzit/) for the amazing Mayan ANSI pieces scattered about Xibalba BBS! +* [Smooth](https://16colo.rs/tags/artist/smooth)/[fUEL](https://fuel.wtf/) for lots of dope art. Why not [snag a T-Shirt](https://www.redbubble.com/people/araknet/works/39126831-enigma-1-2-software-logo-design-by-smooth-of-fuel?p=t-shirt)? ## License Released under the [BSD 2-clause](https://opensource.org/licenses/BSD-2-Clause) license: From d728ddea4c823b5416aeac45096ba01c751cfaf3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 8 Jul 2019 19:24:37 -0600 Subject: [PATCH 101/140] Fix v3 async whilst --- core/menu_module.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/menu_module.js b/core/menu_module.js index 526482da..95557e32 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -186,7 +186,7 @@ exports.MenuModule = class MenuModule extends PluginModule { let opts = { cls : true }; // clear screen for first message async.whilst( - () => this.client.interruptQueue.hasItems(), + (callback) => callback(null, this.client.interruptQueue.hasItems()), next => { this.client.interruptQueue.displayNext(opts, err => { opts = {}; From 1ec721212d18ce5e5903af0fa35e5c8a0141dbde Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 8 Jul 2019 19:27:54 -0600 Subject: [PATCH 102/140] v3 async until --- core/file_transfer.js | 2 +- core/file_util.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/file_transfer.js b/core/file_transfer.js index 83dedc13..5f0138e2 100644 --- a/core/file_transfer.js +++ b/core/file_transfer.js @@ -205,7 +205,7 @@ exports.getModule = class TransferFileModule extends MenuModule { let tryDstPath; async.until( - () => movedOk, // until moved OK + (callback) => callback(null, movedOk), // until moved OK (cb) => { if(0 === renameIndex) { // try originally supplied path first diff --git a/core/file_util.js b/core/file_util.js index fdea4e45..56d8d5ac 100644 --- a/core/file_util.js +++ b/core/file_util.js @@ -38,7 +38,7 @@ function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) { } async.until( - () => opOk, // until moved OK + (callback) => callback(null, opOk), // until moved OK (cb) => { if(0 === renameIndex) { // try originally supplied path first From 289b32c169a985805b66faca6e01504879403f88 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 8 Jul 2019 19:49:55 -0600 Subject: [PATCH 103/140] Allow idle monitor to be disabled --- WHATSNEW.md | 1 + core/client.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index 53dc54cb..be707a09 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -27,6 +27,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For ] } ``` +* `idleLogoutSeconds` and `preAuthIdleLogoutSeconds` can now be set to `0` to fully disable the idle monitor. ## 0.0.9-alpha * Development is now against Node.js 10.x LTS. While other Node.js series may continue to work, you're own your own and YMMV! diff --git a/core/client.js b/core/client.js index b044bcdf..566aec5d 100644 --- a/core/client.js +++ b/core/client.js @@ -476,7 +476,7 @@ Client.prototype.startIdleMonitor = function() { // use override value if set idleLogoutSeconds = this.idleLogoutSecondsOverride || idleLogoutSeconds; - if(nowMs - this.lastKeyPressMs >= (idleLogoutSeconds * 1000)) { + if(idleLogoutSeconds > 0 && (nowMs - this.lastKeyPressMs >= (idleLogoutSeconds * 1000))) { this.emit('idle timeout'); } }, 1000 * 60); From 3eed388e337ca0f9327694e39d12857dcfffd6f8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 11 Jul 2019 22:49:15 -0600 Subject: [PATCH 104/140] BBSLink + WebSockets does not update key press monitor --- core/bbs_link.js | 25 ++++++++++++++++--------- core/servers/login/websocket.js | 2 +- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/core/bbs_link.js b/core/bbs_link.js index 01eb3bfe..093f0bb2 100644 --- a/core/bbs_link.js +++ b/core/bbs_link.js @@ -135,27 +135,34 @@ exports.getModule = class BBSLinkModule extends MenuModule { }; let clientTerminated; + let dataOut; self.client.term.write(resetScreen()); - self.client.term.write(' Connecting to BBSLink.net, please wait...\n'); + self.client.term.write(` Connecting to ${self.config.host}, please wait...\n`); const doorTracking = trackDoorRunBegin(self.client, `bbslink_${self.config.door}`); const bridgeConnection = net.createConnection(connectOpts, function connected() { self.client.log.info(connectOpts, 'BBSLink bridge connection established'); - self.client.term.output.pipe(bridgeConnection); + dataOut = (data) => { + return bridgeConnection.write(data); + }; + + self.client.term.output.on('data', dataOut); self.client.once('end', function clientEnd() { self.client.log.info('Connection ended. Terminating BBSLink connection'); clientTerminated = true; - bridgeConnection.end(); + bridgeConnection.end(); }); }); - const restorePipe = function() { - self.client.term.output.unpipe(bridgeConnection); - self.client.term.output.resume(); + const restore = () => { + if(dataOut && self.client.term.output) { + self.client.term.output.removeListener('data', dataOut); + dataOut = null; + } trackDoorRunEnd(doorTracking); }; @@ -167,14 +174,14 @@ exports.getModule = class BBSLinkModule extends MenuModule { }); bridgeConnection.on('end', function connectionEnd() { - restorePipe(); + restore(); return callback(clientTerminated ? Errors.General('Client connection terminated') : null); }); bridgeConnection.on('error', function error(err) { self.client.log.info('BBSLink bridge connection error: ' + err.message); - restorePipe(); - callback(err); + restore(); + return callback(err); }); } ], diff --git a/core/servers/login/websocket.js b/core/servers/login/websocket.js index 6a6b01dc..dadcdbe6 100644 --- a/core/servers/login/websocket.js +++ b/core/servers/login/websocket.js @@ -88,7 +88,7 @@ function WebSocketClient(ws, req, serverType) { }); // - // Montior connection status with ping/pong + // Monitor connection status with ping/pong // ws.on('pong', () => { Log.trace(`Pong from ${this.socketBridge.remoteAddress}`); From 35dd2c758cb2b2f0116ad29e9a017a6c0c6f9e24 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 28 Jul 2019 23:18:12 +0100 Subject: [PATCH 105/140] Add install checks for gcc and make, allow specify of branch to install from --- misc/install.sh | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/misc/install.sh b/misc/install.sh index 1203705c..5da8e0e4 100755 --- a/misc/install.sh +++ b/misc/install.sh @@ -3,7 +3,7 @@ { # this ensures the entire script is downloaded before execution ENIGMA_NODE_VERSION=${ENIGMA_NODE_VERSION:=10} -ENIGMA_BRANCH=master +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} TIME_FORMAT=`date "+%Y-%m-%d %H:%M:%S"` @@ -21,7 +21,7 @@ _____________________ _____ ____________________ __________\\_ / /__ _\\ <*> ENiGMA½ // https://github.com/NuSkooler/enigma-bbs <*> /__/ -ENiGMA½ will be installed to ${ENIGMA_INSTALL_DIR}, from source ${ENIGMA_SOURCE}. +ENiGMA½ will be installed to ${ENIGMA_INSTALL_DIR}, from source ${ENIGMA_SOURCE}, branch ${ENIGMA_BRANCH}. ENiGMA½ requires Node.js. Version ${ENIGMA_NODE_VERSION}.x current will be installed via nvm. If you already have nvm installed, this install script will update it to the latest version. @@ -37,6 +37,7 @@ fatal_error() { } enigma_install_needs() { + echo "Checking $1 installation" command -v $1 >/dev/null 2>&1 || fatal_error "ENiGMA½ requires $1 but it's not installed. Please install it and restart the installer." } @@ -45,14 +46,11 @@ log() { } enigma_install_init() { - log "Checking git installation" enigma_install_needs git - - log "Checking curl installation" enigma_install_needs curl - - log "Checking Python installation" enigma_install_needs python + enigma_install_needs make + enigma_install_needs gcc } install_nvm() { From f3efc35e8361b131615263c371eb8fd9296907c4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 31 Jul 2019 20:26:13 -0600 Subject: [PATCH 106/140] Adjust max length for edit 'to' --- misc/menu_template.in.hjson | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/misc/menu_template.in.hjson b/misc/menu_template.in.hjson index a2a183a5..c2bbdd3f 100644 --- a/misc/menu_template.in.hjson +++ b/misc/menu_template.in.hjson @@ -616,6 +616,7 @@ argName: to focus: true text: @sysStat:sysop_username + maxLength: 36 // :TODO: readOnly: true } ET3: { @@ -1532,6 +1533,7 @@ argName: to focus: true text: @sysStat:sysop_username + maxLength: 36 // :TODO: readOnly: true } ET3: { @@ -2534,6 +2536,7 @@ argName: to focus: true validate: @systemMethod:validateNonEmpty + maxLength: 36 } ET3: { argName: subject @@ -2692,6 +2695,7 @@ focus: true text: All validate: @systemMethod:validateNonEmpty + maxLength: 36 } ET3: { argName: subject @@ -2851,6 +2855,7 @@ argName: to focus: true validate: @systemMethod:validateGeneralMailAddressedTo + maxLength: 36 } ET3: { argName: subject From 3e99fdea48b65ad87f7c00e634bddb72e3b01d66 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 31 Jul 2019 20:26:22 -0600 Subject: [PATCH 107/140] Doc typo fixes --- UPGRADE.md | 2 +- WHATSNEW.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index ed5e687d..b048759d 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -37,7 +37,7 @@ npm install ``` # Problems -Report your issue on Xibalba BBS, hop in #enigma-bbs on Freenet and chat, or +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.9-alpha to 0.0.10-alpha diff --git a/WHATSNEW.md b/WHATSNEW.md index be707a09..72eaeba6 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -45,7 +45,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * Add ability to skip file and/or message areas during newscan. Set config.omitFileAreaTags and config.omitMessageAreaTags in new_scan configuration of your menu.hjson * `{userName}` (sanitized) and `{userNameRaw}` as well as `{cwd}` have been added to param options when launching a door. * Any module may now register for a system startup initialization via the `initializeModules(initInfo, cb)` export. -* User event log is now functional. Various events a user performs will be persisted to the `system.db` `user_event_log` table for up to 90 days. An example usage can be found in the updated `last_callers` module where events are turned into Ami/X style actions. Please see `UPGRADE.md`! +* User event log is now functional. Various events a user performs will be persisted to the `system.sqlite3` `user_event_log` table for up to 90 days. An example usage can be found in the updated `last_callers` module where events are turned into Ami/X style actions. Please see `UPGRADE.md`! * New MCI codes including general purpose movement codes. See [MCI codes](docs/art/mci.md) * `install.sh` will now attempt to use NPM's `--build-from-source` option when ARM is detected. * `oputil.js config new` will now generate a much more complete configuration file with comments, examples, etc. `oputil.js config cat` dumps your current config to stdout. From c71404a641d3c975a5761a52adfce46f9cb64805 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 9 Aug 2019 18:16:23 -0600 Subject: [PATCH 108/140] oputil.js fb scan improvement & fixes * Start of --verbose for oputil.js in general (mostly placeholder at this point) * Fix --full-scan and rename to just --full --- WHATSNEW.md | 2 +- core/oputil/oputil_file_base.js | 14 ++++++++++++-- core/oputil/oputil_help.js | 5 +++-- docs/admin/oputil.md | 5 +++-- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index 72eaeba6..fc6ed98f 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -9,7 +9,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * New ACS codes for new 2FA/OTP: `AR` and `AF`. See [ACS](/docs/configuration/acs.md) for details. + `oputil.js user 2fa USERNAME TYPE` enables 2-factor authentication for a user. * `oputil.js user info USERNAME --security` can now display additional security information such as 2FA/OTP. -* `oputil.js fb scan --quick` is now the default. Override with `--full-scan`. +* `oputil.js fb scan --quick` is now the default. Override with `--full`. * ACS checks can now be applied to form actions. For example: ```hjson { diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index efefa05b..67962fc0 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -209,7 +209,7 @@ function scanFileAreaForChanges(areaInfo, options, cb) { async.series( [ function quickCheck(next) { - if(options['full-scan']) { + if(options['full']) { return next(null); } @@ -229,6 +229,16 @@ function scanFileAreaForChanges(areaInfo, options, cb) { areaTag : areaInfo.areaTag, storageTag : storageLoc.storageTag }, + (stepInfo, next) => { + if(argv.verbose) { + if(stepInfo.error) { + console.error(` error: ${stepInfo.error}`); + } else { + console.info(` processing: ${stepInfo.step}`); + } + } + return next(null); + }, (err, fileEntry, dupeEntries) => { if(err) { console.info(`Error: ${err.message}`); @@ -477,7 +487,7 @@ function scanFileAreas() { } options.descFile = argv['desc-file']; // --desc-file or --desc-file PATH - options['full-scan'] = argv['-full-scan']; + options['full'] = argv.full; options.areaAndStorageInfo = getAreaAndStorage(argv._.slice(2)); diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index 62c47297..abcf61fe 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -14,6 +14,7 @@ const usageHelp = exports.USAGE_HELP = { Global arguments: -c, --config PATH Specify config path (default is ${getDefaultConfigPath()}) -n, --no-prompt Assume defaults (don't prompt for input where possible) + --verbose Verbose output, where applicable Commands: user User management @@ -131,7 +132,7 @@ scan arguments: DESCRIPT.ION file. --update Attempt to update information for existing entries - --full-scan Perform a full scan (default is quick) + --full Perform a full scan (default is quick) info arguments: --show-desc Display short description, if any @@ -152,7 +153,7 @@ General Information: Generally an area tag can also include an optional storage tag. For example, the area of 'bbswarez' stored using 'bbswarez_main': bbswarez@bbswarez_main - When performing an initial import of a large area or storage backing, --full-scan + When performing an initial import of a large area or storage backing, --full is the best option. If re-scanning an area for updates a standard / quick scan is generally good enough. diff --git a/docs/admin/oputil.md b/docs/admin/oputil.md index 7190d268..0b05f7aa 100644 --- a/docs/admin/oputil.md +++ b/docs/admin/oputil.md @@ -14,6 +14,7 @@ usage: oputil.js [--version] [--help] Global arguments: -c, --config PATH Specify config path (default is ./config/) -n, --no-prompt Assume defaults (don't prompt for input where possible) + --verbose Verbose output, where applicable Commands: user User management @@ -178,7 +179,7 @@ scan arguments: DESCRIPT.ION file. --update Attempt to update information for existing entries - --full-scan Perform a full scan (default is quick) + --full Perform a full scan (default is quick) info arguments: --show-desc Display short description, if any @@ -197,7 +198,7 @@ General Information: Generally an area tag can also include an optional storage tag. For example, the area of 'bbswarez' stored using 'bbswarez_main': bbswarez@bbswarez_main - When performing an initial import of a large area or storage backing, --full-scan + When performing an initial import of a large area or storage backing, --full is the best option. If re-scanning an area for updates a standard / quick scan is generally good enough. From e861607330909b8ef91a4e05520e8164fbb94fa8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 9 Aug 2019 23:17:10 -0600 Subject: [PATCH 109/140] Update .gitignore slightly --- .gitignore | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 28a19883..b55cbcfa 100644 --- a/.gitignore +++ b/.gitignore @@ -2,11 +2,13 @@ *.pem # Various directories -config/config.hjson -logs/ +config/ db/ -dropfiles/ +drop/ +file_base/ +logs/ +mail/ node_modules/ docs/_site/ docs/.sass-cache/ -` \ No newline at end of file +.vscode/ From 8fde4ccd60d39a9e359197f020383b7fcd1d58b0 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 9 Aug 2019 23:17:41 -0600 Subject: [PATCH 110/140] Initial auto-signature support --- art/themes/luciano_blocktronics/autosig.ans | Bin 0 -> 407 bytes art/themes/luciano_blocktronics/theme.hjson | 14 ++++ core/autosig_edit.js | 76 ++++++++++++++++++++ core/fse.js | 18 +++-- core/user_property.js | 1 + 5 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 art/themes/luciano_blocktronics/autosig.ans create mode 100644 core/autosig_edit.js diff --git a/art/themes/luciano_blocktronics/autosig.ans b/art/themes/luciano_blocktronics/autosig.ans new file mode 100644 index 0000000000000000000000000000000000000000..1af14a63ea6d4c7d6ff9f751b661d819980920fb GIT binary patch literal 407 zcmcJLF%N<;5QWRyxVhP-F<~@b3xdc5NC*Q#1zb8A;^z7PC*!pk^#|w;&3o^@v@aC~ z(h$-hMlOX>X`t~zB8V^>`gYaTl`Qvl-=1tpJ7EBY2{*+HE;~{I;M%Q^mIB;4Lz*tu zI9~4K^~O1cF-l1>J>wui={LRNUm;PHexSd2VPa)#N9gChwtd3Le!=7b`J&5)q6DBK dzZwlHfh{CUk1@|Xuo-hRuk%Kpt88-b^9jBcQ2hV^ literal 0 HcmV?d00001 diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 86f1ed3b..6d8490dc 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -447,6 +447,20 @@ } } + editAutoSignature: { + 0: { + mci: { + MT1: { + height: 8 + width: 73 + } + BT2: { + focusTextStyle: upper + } + } + } + } + messageSearch: { 0: { mci: { diff --git a/core/autosig_edit.js b/core/autosig_edit.js new file mode 100644 index 00000000..c9995280 --- /dev/null +++ b/core/autosig_edit.js @@ -0,0 +1,76 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const { MenuModule } = require('./menu_module.js'); +const UserProps = require('./user_property.js'); + +// deps +const async = require('async'); +const _ = require('lodash'); + +exports.moduleInfo = { + name : 'User Auto-Sig Editor', + desc : 'Module for editing auto-sigs', + author : 'NuSkooler', +}; + +const FormIds = { + edit : 0, +}; + +const MciViewIds = { + editor : 1, + save : 2, +}; + +exports.getModule = class UserAutoSigEditorModule extends MenuModule { + constructor(options) { + super(options); + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); + + this.menuMethods = { + saveChanges : (formData, extraArgs, cb) => { + return this.saveChanges(cb); + } + }; + } + + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } + + async.series( + [ + (callback) => { + return this.prepViewController('edit', FormIds.edit, mciData.menu, callback); + }, + (callback) => { + const requiredCodes = [ MciViewIds.editor, MciViewIds.save ]; + return this.validateMCIByViewIds('edit', requiredCodes, callback); + }, + (callback) => { + const sig = this.client.user.getProperty(UserProps.AutoSignature) || ''; + this.setViewText('edit', MciViewIds.editor, sig); + return callback(null); + } + ], + err => { + return cb(err); + } + ); + }); + } + + saveChanges(cb) { + const sig = this.getView('edit', MciViewIds.editor).getData().trim(); + this.client.user.persistProperty(UserProps.AutoSignature, sig, err => { + if(err) { + this.client.log.error( { error : err.message }, 'Could not save auto-sig'); + } + return this.prevMenu(cb); + }); + } +}; diff --git a/core/fse.js b/core/fse.js index d77fbfed..8b2214d8 100644 --- a/core/fse.js +++ b/core/fse.js @@ -325,20 +325,21 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul buildMessage(cb) { const headerValues = this.viewControllers.header.getFormData().value; + const area = getMessageAreaByTag(this.messageAreaTag); const getFromUserName = () => { - const area = getMessageAreaByTag(this.messageAreaTag); return (area && area.realNames) ? this.client.user.getProperty(UserProps.RealName) || this.client.user.username : this.client.user.username; }; + let messageBody = this.viewControllers.body.getView(MciViewIds.body.message).getData( { forceLineTerms : this.replyIsAnsi } ); + const msgOpts = { areaTag : this.messageAreaTag, toUserName : headerValues.to, fromUserName : getFromUserName(), subject : headerValues.subject, - message : this.viewControllers.body.getView(MciViewIds.body.message).getData( { forceLineTerms : this.replyIsAnsi } ), }; if(this.isReply()) { @@ -351,11 +352,20 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul // to packetAnsiMsgEncoding (generally cp437) as various boards // really don't like ANSI messages in UTF-8 encoding (they should!) // - msgOpts.meta = { System : { 'explicit_encoding' : _.get(Config(), 'scannerTossers.ftn_bso.packetAnsiMsgEncoding', 'cp437') } }; - msgOpts.message = `${ansi.reset()}${ansi.eraseData(2)}${ansi.goto(1,1)}\r\n${ansi.up()}${msgOpts.message}`; + msgOpts.meta = { System : { 'explicit_encoding' : _.get(Config(), 'scannerTossers.ftn_bso.packetAnsiMsgEncoding', 'cp437') } }; + messageBody = `${ansi.reset()}${ansi.eraseData(2)}${ansi.goto(1,1)}\r\n${ansi.up()}${messageBody}`; } } + if(false != area.autoSignatures) { + const sig = this.client.user.getProperty(UserProps.AutoSignature); + if(sig) { + messageBody += `\r\n-- \r\n${sig}`; + } + } + + msgOpts.message = messageBody; + this.message = new Message(msgOpts); return cb(null); diff --git a/core/user_property.js b/core/user_property.js index 88ac11b1..cc68ef09 100644 --- a/core/user_property.js +++ b/core/user_property.js @@ -27,6 +27,7 @@ module.exports = { LastLoginTs : 'last_login_timestamp', LoginCount : 'login_count', UserComment : 'user_comment', // NYI + AutoSignature : 'auto_signature', DownloadQueue : 'dl_queue', // download_queue.js From 0e6aa563798c930f2582f871f55b3aa1f3444706 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 9 Aug 2019 23:37:00 -0600 Subject: [PATCH 111/140] Fix art: ESC is only cancel --- art/themes/luciano_blocktronics/autosig.ans | Bin 407 -> 410 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/art/themes/luciano_blocktronics/autosig.ans b/art/themes/luciano_blocktronics/autosig.ans index 1af14a63ea6d4c7d6ff9f751b661d819980920fb..ce5b6d0df9a2bc945e9b899655f539e58a5b617f 100644 GIT binary patch delta 44 ucmbQvJd1gPxwMh9bhNQiu7U!PG6zyX+Rz#(;2P|_(O-#?QFU?=qbdOSj0(p9 delta 41 ucmbQmJe_%hxrC{+bhM$hu~DvTFpy Date: Sat, 10 Aug 2019 11:09:34 -0600 Subject: [PATCH 112/140] Comments --- core/fse.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/fse.js b/core/fse.js index 8b2214d8..3c1b70d3 100644 --- a/core/fse.js +++ b/core/fse.js @@ -357,6 +357,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } } + // + // Append auto-signature, if enabled for the area & the user has one + // if(false != area.autoSignatures) { const sig = this.client.user.getProperty(UserProps.AutoSignature); if(sig) { @@ -364,8 +367,8 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } } + // finally, create the message msgOpts.message = messageBody; - this.message = new Message(msgOpts); return cb(null); From 236e5dedb15931fb23dfa3028b0836a9c051e0e5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 10 Aug 2019 11:09:56 -0600 Subject: [PATCH 113/140] Switch default zip file handler to InfoZip package --- core/config.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/core/config.js b/core/config.js index d9a5684b..8b335d44 100644 --- a/core/config.js +++ b/core/config.js @@ -577,7 +577,7 @@ function getDefaultConfig() { desc : 'ZIP Archive', sig : '504b0304', offset : 0, - archiveHandler : '7Zip', + archiveHandler : 'InfoZip', }, /* 'application/x-cbr' : { @@ -651,7 +651,7 @@ function getDefaultConfig() { archives : { archivers : { - '7Zip' : { + '7Zip' : { // p7zip package compress : { cmd : '7za', args : [ 'a', '-tzip', '{archivePath}', '{fileList}' ], @@ -671,6 +671,26 @@ function getDefaultConfig() { }, }, + InfoZip: { + compress : { + cmd : 'zip', + args : [ '{archivePath}', '{fileList}' ], + }, + decompress : { + cmd : 'unzip', + args : [ '{archivePath}', '-d', '{extractPath}' ], + }, + list : { + cmd : 'unzip', + args : [ '-l', '{archivePath}' ], + entryMatch : '^\\s*([0-9]+)\\s+[0-9]{4}-[0-9]{2}-[0-9]{2}\\s+[0-9]{2}:[0-9]{2}\\s+([^\\r\\n]+)$', + }, + extract : { + cmd : 'unzip', + args : [ '{archivePath}', '{fileList}', '-d', '{extractPath}' ], + } + }, + Lha : { // // 'lha' command can be obtained from: From b647e665b95bf4a47011ba2b1771aad6f99ac6ba Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 10 Aug 2019 11:15:54 -0600 Subject: [PATCH 114/140] Document p7Zip -> InfoZip changes --- UPGRADE.md | 1 + WHATSNEW.md | 1 + 2 files changed, 2 insertions(+) diff --git a/UPGRADE.md b/UPGRADE.md index b048759d..ef6fd95a 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -42,6 +42,7 @@ Report your issue on Xibalba BBS, hop in #enigma-bbs on FreeNode and chat, or # 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`. # 0.0.8-alpha to 0.0.9-alpha * Development is now against Node.js 10.x LTS. Follow your standard upgrade path to update to Node 10.x before using 0.0.9-alpha! diff --git a/WHATSNEW.md b/WHATSNEW.md index fc6ed98f..293c9dc5 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -28,6 +28,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For } ``` * `idleLogoutSeconds` and `preAuthIdleLogoutSeconds` can now be set to `0` to fully disable the idle monitor. +* Switched default archive handler for zip files from 7zip to InfoZip (`zip` and `unzip`) commands. See [UPGRADE](UPGRADE.md). ## 0.0.9-alpha * Development is now against Node.js 10.x LTS. While other Node.js series may continue to work, you're own your own and YMMV! From a494ac95ec72a1b9ecfdb537dd0fb95bddfc2b64 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 10 Aug 2019 11:19:10 -0600 Subject: [PATCH 115/140] More doc updates RE: InfoZip --- docs/configuration/archivers.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/configuration/archivers.md b/docs/configuration/archivers.md index 3d3f952a..aa820482 100644 --- a/docs/configuration/archivers.md +++ b/docs/configuration/archivers.md @@ -11,9 +11,15 @@ Archivers are manged via the `archives:archivers` configuration block of `config The following archivers are pre-configured in ENiGMA½ as of this writing. Remember that you can override settings or add new handlers! #### ZZip -* Formats: .7z, .bzip2, .zip, .gzip/.gz, and more +* Formats: .7z, .bzip2, .gzip/.gz, and more * Key: `7Zip` * Homepage/package: [7-zip.org](http://www.7-zip.org/). Generally obtained from a `p7zip` package in *nix environments. See http://p7zip.sourceforge.net/ for details. +* Notes: Previous defaulted to using 7zip for .zip files as well, but newer versions of the package give "volume" errors at times. See InfoZip below. + +#### InfoZip +* Formats: .zip +* Key: InfoZip +* Homepage/package: http://infozip.sourceforge.net/. Often already available in Linux. You will need `zip` and `unzip` in ENiGMA's path. #### Lha * Formats: LHA files such as .lzh. From 5db0a33a8a9c90b7a1182487e45dea4f977aca8d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 10 Aug 2019 12:17:56 -0600 Subject: [PATCH 116/140] Add missing entry to template --- misc/menu_template.in.hjson | 40 +++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/misc/menu_template.in.hjson b/misc/menu_template.in.hjson index c2bbdd3f..71901719 100644 --- a/misc/menu_template.in.hjson +++ b/misc/menu_template.in.hjson @@ -2090,6 +2090,10 @@ value: { command: "S" } action: @menu:messageSearch } + { + value: { command: "A" } + action: @menu:editAutoSignature + } { value: 1 action: @menu:messageArea @@ -2097,6 +2101,42 @@ ] } + editAutoSignature: { + desc: Auto Sig Editor + module: autosig_edit + art: autosig + form: { + 0: { + mci: { + MT1: { + argName: signature + tabSwitchesView: true + } + BT2: { + text: save + argName: save + submit: true + } + } + submit: { + *: [ + { + value: { save: null } + action: @method:saveChanges + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + messageSearch: { desc: Message Search module: message_base_search From da545d88200e895fd90e5716db3b1183052abd62 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 10 Aug 2019 12:18:47 -0600 Subject: [PATCH 117/140] Fix default art --- art/themes/luciano_blocktronics/MSGMNU.ANS | Bin 3685 -> 3712 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/art/themes/luciano_blocktronics/MSGMNU.ANS b/art/themes/luciano_blocktronics/MSGMNU.ANS index 9c373944c901ce9fba4302f0fee7c0ef272e5d00..a532804b295b5498300b1a9f94965e6bba959f37 100644 GIT binary patch delta 50 zcmaDV(;&Oy6|0=Nvvjm!ZlZLwL9T*yw6R%kYD#8_LSku2zHV`5`sTN+K^#n?e3Kpd FQ~;~D590s; delta 23 ecmZpWeJZoz6)Urmk@IE_wjd6sU%ZoJ_*4L34F@d% From 47eb1b490dc1630c8557e5db3654c2c55a15d758 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 10 Aug 2019 20:10:38 -0600 Subject: [PATCH 118/140] Artwork --- art/themes/luciano_blocktronics/MSGMNU.ANS | Bin 3712 -> 3712 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/art/themes/luciano_blocktronics/MSGMNU.ANS b/art/themes/luciano_blocktronics/MSGMNU.ANS index a532804b295b5498300b1a9f94965e6bba959f37..1c80fa723d300550d42c4f37e852813f98ead021 100644 GIT binary patch delta 20 bcmZpWZIIo-!N#tjARTRNl)IUqt&jr%G^+&3 delta 20 bcmZpWZIIo-!Nx8fZETdQps<;rt&jr%HTwj~ From e80d3f109309c4b4b6bcc9f3670602b708e08c68 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 10 Aug 2019 20:14:36 -0600 Subject: [PATCH 119/140] Add missing 'my messages' to message menu --- art/themes/luciano_blocktronics/MSGMNU.ANS | Bin 3712 -> 3714 bytes misc/menu_template.in.hjson | 12 ++++++++++++ 2 files changed, 12 insertions(+) diff --git a/art/themes/luciano_blocktronics/MSGMNU.ANS b/art/themes/luciano_blocktronics/MSGMNU.ANS index 1c80fa723d300550d42c4f37e852813f98ead021..01777dd9d826cdde0b21675c94359fe944131654 100644 GIT binary patch delta 84 zcmZpWZIa#4&Bmet1d~fSl$n6k1h!;GgUO=o$&){@=}g|l?#^WHJh_@(kIB$rG8adt hsD-n1w6R&PbhME%h-ID&(!6;N$39j@@yXGAssOal71#g( delta 85 zcmZpYZIIp2&BiJnZDi^^c?( Date: Sat, 10 Aug 2019 20:16:30 -0600 Subject: [PATCH 120/140] Fix missing 'del/d' for delete in artwork --- .../luciano_blocktronics/PRVMSGLIST.ANS | Bin 2229 -> 2280 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/art/themes/luciano_blocktronics/PRVMSGLIST.ANS b/art/themes/luciano_blocktronics/PRVMSGLIST.ANS index 180ace2b077bc7bcc5370277e08f310cff326424..3969d72f9dbe35c22e07e76d0d0112303513722e 100644 GIT binary patch delta 90 zcmdlg_(E_)Bddjif`W9kp|!D5u8XUWbhNQSu0D_i@_@oXjun^#WE+5#q@?DgmZSoO i%yTENWPLKZhgEg*4z{ Date: Sat, 10 Aug 2019 22:12:55 -0600 Subject: [PATCH 121/140] Doc on autoSignatures area option --- docs/messageareas/configuring-a-message-area.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/messageareas/configuring-a-message-area.md b/docs/messageareas/configuring-a-message-area.md index 63af2826..e099c9e0 100644 --- a/docs/messageareas/configuring-a-message-area.md +++ b/docs/messageareas/configuring-a-message-area.md @@ -51,6 +51,7 @@ Message Areas are topic specific containers for messages that live within a part | `sort` | :-1: | Set to a number to override the default alpha-numeric sort order based on the `name` field. | | `default` | :-1: | Specify `true` to make this the default area (e.g. assigned to new users) | | `acs` | :-1: | A standard [ACS](/docs/configuration/acs.md) block. See **ACS** below. | +| `autoSignatures` | :-1: | Set to `false` to disable auto-signatures in this area. | ### ACS An optional standard [ACS](/docs/configuration/acs.md) block can be supplied with the following rules: From 1e08810188dbff1ecaed9f4631442586daa469a3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 22 Aug 2019 21:48:39 -0600 Subject: [PATCH 122/140] Update InfoZip list extractor --- core/config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/config.js b/core/config.js index 8b335d44..44b5f4cc 100644 --- a/core/config.js +++ b/core/config.js @@ -683,7 +683,8 @@ function getDefaultConfig() { list : { cmd : 'unzip', args : [ '-l', '{archivePath}' ], - entryMatch : '^\\s*([0-9]+)\\s+[0-9]{4}-[0-9]{2}-[0-9]{2}\\s+[0-9]{2}:[0-9]{2}\\s+([^\\r\\n]+)$', + // Annoyingly, dates can be in YYYY-MM-DD or MM-DD-YYYY format + entryMatch : '^\\s*([0-9]+)\\s+[0-9]{2,4}-[0-9]{2}-[0-9]{2,4}\\s+[0-9]{2}:[0-9]{2}\\s+([^\\r\\n]+)$', }, extract : { cmd : 'unzip', From 16f4d62548985740d89a4da38ffce39d5eb74a3c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 3 Sep 2019 19:52:04 -0600 Subject: [PATCH 123/140] Initial support for random 'next', 'action', ... --- core/menu_stack.js | 5 ++++- core/menu_util.js | 36 +++++++++++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/core/menu_stack.js b/core/menu_stack.js index 080d0efa..42cd6987 100644 --- a/core/menu_stack.js +++ b/core/menu_stack.js @@ -7,6 +7,9 @@ const { Errors, ErrorReasons } = require('./enig_error.js'); +const { + getResolvedSpec +} = require('./menu_util.js'); // deps const _ = require('lodash'); @@ -53,7 +56,7 @@ module.exports = class MenuStack { next(cb) { const currentModuleInfo = this.top(); const menuConfig = currentModuleInfo.instance.menuConfig; - const nextMenu = this.client.acs.getConditionalValue(menuConfig.next, 'next'); + const nextMenu = getResolvedSpec(this.client, menuConfig.next, 'next'); if(!nextMenu) { return cb(Array.isArray(menuConfig.next) ? Errors.MenuStack('No matching condition for "next"', ErrorReasons.NoConditionMatch) : diff --git a/core/menu_util.js b/core/menu_util.js index 18b17d9f..a429415b 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -17,6 +17,7 @@ const _ = require('lodash'); exports.loadMenu = loadMenu; exports.getFormConfigByIDAndMap = getFormConfigByIDAndMap; exports.handleAction = handleAction; +exports.getResolvedSpec = getResolvedSpec; exports.handleNext = handleNext; function getMenuConfig(client, name, cb) { @@ -172,7 +173,7 @@ function handleAction(client, formData, conf, cb) { return cb(Errors.MissingParam('Missing config')); } - const action = client.acs.getConditionalValue(conf.action, 'action'); // handle any conditionals + const action = getResolvedSpec(client, conf.action, 'action'); // random/conditionals/etc. const actionAsset = asset.parseAsset(action); if(!_.isObject(actionAsset)) { return cb(Errors.Invalid('Unable to parse "conf.action"')); @@ -216,9 +217,38 @@ function handleAction(client, formData, conf, cb) { } } -function handleNext(client, nextSpec, conf, cb) { - nextSpec = client.acs.getConditionalValue(nextSpec, 'next'); // handle any conditionals +function getResolvedSpec(client, spec, memberName) { + // + // 'next', 'action', etc. can come in various flavors: + // (1) Simple string: + // next: foo + // (2) Array of objects with 'acs' checks; any object missing 'acs' + // is assumed to be "true": + // next: [ + // { + // acs: AR2 + // next: foo + // } + // { + // next: baz + // } + // ] + // (3) Simple array of strings. A random selection will be made: + // next: [ "foo", "baz", "fizzbang" ] + // + if(!Array.isArray(spec)) { + return spec; // (1) simple string, as-is + } + if(_.isObject(spec[0])) { + return client.acs.getConditionalValue(spec, memberName); // (2) ACS conditionals + } + + return spec[Math.floor(Math.random() * spec.length)]; // (3) random +} + +function handleNext(client, nextSpec, conf, cb) { + nextSpec = getResolvedSpec(client, nextSpec, 'next'); const nextAsset = asset.getAssetWithShorthand(nextSpec, 'menu'); // :TODO: getAssetWithShorthand() can return undefined - handle it! From 13c45315d4b97265db1daf9c9211743dce16a87d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 4 Sep 2019 18:59:50 -0600 Subject: [PATCH 124/140] More ACS docs --- docs/configuration/menu-hjson.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/configuration/menu-hjson.md b/docs/configuration/menu-hjson.md index 9294c08c..37b7f669 100644 --- a/docs/configuration/menu-hjson.md +++ b/docs/configuration/menu-hjson.md @@ -180,6 +180,23 @@ opOnlyMenu: { } ``` +### Action Matches +Action blocks (`action`) can perform ACS checks: +``` +// ... +{ + action: [ + { + acs: SC1 + action: @menu:secureMenu + } + { + action: @menu:nonSecureMenu + } + ] +} +``` + ### Flow Control The `next` member of a menu may be an array of objects containing an `acs` check as well as the destination. Depending on the current user's ACS, the system will pick the appropriate target. The last element in an array without an `acs` can be used as a catch all. Example: ``` From e9820ab2d355441f6356271149e9a9371cd9981f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 4 Sep 2019 19:50:16 -0600 Subject: [PATCH 125/140] Add docs on random actions --- WHATSNEW.md | 13 +++++++++++++ docs/configuration/menu-hjson.md | 17 +++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/WHATSNEW.md b/WHATSNEW.md index 293c9dc5..b1ec8ce6 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -29,6 +29,19 @@ This document attempts to track **major** changes and additions in ENiGMA½. For ``` * `idleLogoutSeconds` and `preAuthIdleLogoutSeconds` can now be set to `0` to fully disable the idle monitor. * Switched default archive handler for zip files from 7zip to InfoZip (`zip` and `unzip`) commands. See [UPGRADE](UPGRADE.md). +* Menu submit `action`'s can now in addition to being a simple string such as `@menu:someMenu`, or an array of objects with ACS checks, be a simple array of strings. In this case, a random match will be made. For example: +```hjson +submit: [ + { + value: { command: "FOO" } + action: [ + // one of the following actions will be matched: + "@menu:menuStyle1" + "@menu:menuStyle2" + ] + } +] +``` ## 0.0.9-alpha * Development is now against Node.js 10.x LTS. While other Node.js series may continue to work, you're own your own and YMMV! diff --git a/docs/configuration/menu-hjson.md b/docs/configuration/menu-hjson.md index 37b7f669..8ddda2d8 100644 --- a/docs/configuration/menu-hjson.md +++ b/docs/configuration/menu-hjson.md @@ -74,6 +74,23 @@ Submit actions are declared using the `action` member of a submit handler block. | `@method:methodName` | Executes *methodName* local to the calling module. That is, the module set by the `module` member of a menu entry. | | `@method:/path/to/some_module.js:methodName` | Executes *methodName* exported by the module at */path/to/some_module.js*. | +#### Advanced Action Handling +In addition to simple simple actions, `action` may also be: +* An array of objects containing ACS checks and a sub `action` if that ACS is matched. See **Action Matches** in the ACS documentation below for details. +* An array of actions. In this case a random selection will be made. Example: +```hjson +submit: [ + { + value: { command: "FOO" } + action: [ + // one of the following actions will be matched: + "@menu:menuStyle1" + "@menu:menuStyle2" + ] + } +] +``` + #### Method Signature Methods executed using `@method`, or `@systemMethod` have the following signature: ``` From 24e18b5a7eeaffd0e0fe370d8e708839d72c4169 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 9 Sep 2019 21:35:12 -0600 Subject: [PATCH 126/140] Clean up defualt mesage/file conf/areas at login if user no longer has read access --- core/user_login.js | 82 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 5 deletions(-) diff --git a/core/user_login.js b/core/user_login.js index bc23e3b3..627fb5ae 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -16,6 +16,16 @@ const UserProps = require('./user_property.js'); const SysProps = require('./system_property.js'); const SystemLogKeys = require('./system_log.js'); const User = require('./user.js'); +const { + getMessageConferenceByTag, + getMessageAreaByTag, + getDefaultMessageConferenceTag, + getDefaultMessageAreaTagByConfTag, +} = require('./message_area.js'); +const { + getFileAreaByTag, + getDefaultFileAreaTag, +} = require('./file_base_area.js'); // deps const async = require('async'); @@ -101,15 +111,77 @@ function userLogin(client, username, password, options, cb) { Events.emit(Events.getSystemEvents().UserLogin, { user } ); setClientTheme(client, user.properties[UserProps.ThemeId]); - if(user.authenticated) { - return recordLogin(client, cb); - } - // recordLogin() must happen after 2FA! - return cb(null); + postLoginPrep(client, err => { + if(err) { + return cb(err); + } + + if(user.authenticated) { + return recordLogin(client, cb); + } + + // recordLogin() must happen after 2FA! + return cb(null); + }); }); } +function postLoginPrep(client, cb) { + + const defaultMsgAreaTag = (confTag) => { + return getDefaultMessageAreaTagByConfTag(client, confTag) || + getDefaultMessageAreaTagByConfTag(client, getDefaultMessageConferenceTag(client)) || + ''; + }; + + async.series( + [ + (callback) => { + // + // User may (no longer) have read (view) rights to their current + // message, conferences and/or areas. Move them out if so. + // + let confTag = client.user.getProperty(UserProps.MessageConfTag); + const conf = getMessageConferenceByTag(confTag) || {}; + const area = getMessageAreaByTag(client.user.getProperty(UserProps.MessageAreaTag), confTag) || {}; + + if(!client.acs.hasMessageConfRead(conf)) { + confTag = getDefaultMessageConferenceTag(client) || ''; + client.user.persistProperties({ + [ UserProps.MessageConfTag ] : confTag, + [ UserProps.MessageAreaTag ] : defaultMsgAreaTag(confTag), + }, + err => { + return callback(err); + }); + } else if (!client.acs.hasMessageAreaRead(area)) { + client.user.persistProperty(UserProps.MessageAreaTag, defaultMsgAreaTag(confTag), err => { + return callback(err); + }); + } else { + return callback(null); + } + }, + (callback) => { + // Likewise for file areas + const area = getFileAreaByTag(client.user.getProperty(UserProps.FileAreaTag)) || {}; + if(!client.acs.hasFileAreaRead(area)) { + const areaTag = getDefaultFileAreaTag(client) || ''; + client.user.persistProperty(UserProps.FileAreaTag, areaTag, err => { + return callback(err); + }); + } else { + return callback(null); + } + } + ], + err => { + return cb(err); + } + ); +} + function recordLogin(client, cb) { assert(client.user.authenticated); // don't get in situations where this isn't true From 8027a73ea559fc389adeca6350047dfac448cc5f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 9 Sep 2019 21:44:03 -0600 Subject: [PATCH 127/140] Another message area check --- core/message_area.js | 9 ++++++++- core/user_login.js | 6 ++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/core/message_area.js b/core/message_area.js index 7f4993ec..f06c21a4 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -404,8 +404,15 @@ function getMessageListForArea(client, areaTag, filter, cb) Object.assign(filter, { areaTag } ); } + if(client) { + const area = getMessageAreaByTag(areaTag); + if(!client.acs.hasMessageAreaRead(area)) { + return cb(null, []); + } + } + if(Message.isPrivateAreaTag(areaTag)) { - filter.privateTagUserId = client.user.userId; + filter.privateTagUserId = client ? client.user.userId : 'INVALID_USER_ID'; } return Message.findMessages(filter, cb); diff --git a/core/user_login.js b/core/user_login.js index 627fb5ae..6c5e6813 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -130,9 +130,11 @@ function userLogin(client, username, password, options, cb) { function postLoginPrep(client, cb) { const defaultMsgAreaTag = (confTag) => { - return getDefaultMessageAreaTagByConfTag(client, confTag) || + return ( + getDefaultMessageAreaTagByConfTag(client, confTag) || getDefaultMessageAreaTagByConfTag(client, getDefaultMessageConferenceTag(client)) || - ''; + '' + ); }; async.series( From dff8e12dcc8fae5d7b5cdd7b88f0def675a15934 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 11 Sep 2019 21:21:33 -0600 Subject: [PATCH 128/140] Message ACS improvements & minor fixes + getSuitableMessageConfAndAreaTags(): Get a suitable conf/area tag pair with priority on defaults + hasMessageConfAndAreaRead() helper + filterMessageAreaTagsByReadACS() helper * Always include confTag/areaTag when fetching a conf or area (convenience) * Fix 'toRemoteUser' assignment in message * Better kick out of message conf/areas users does not have access to --- core/fse.js | 2 +- core/message_area.js | 81 ++++++++++++++++++++++++++++++++++++++++---- core/user_login.js | 29 +++++----------- 3 files changed, 84 insertions(+), 28 deletions(-) diff --git a/core/fse.js b/core/fse.js index 3c1b70d3..5e018df7 100644 --- a/core/fse.js +++ b/core/fse.js @@ -307,7 +307,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul fromUserId : _.get(this.message, 'meta.System.local_from_user_id', localUserIdNotAvail), toUserId : _.get(this.message, 'meta.System.local_to_user_id', localUserIdNotAvail), fromRemoteUser : _.get(this.message, 'meta.System.remote_from_user', remoteUserNotAvail), - toRemoteUser : _.get(this.messgae, 'meta.System.remote_to_user', remoteUserNotAvail), + toRemoteUser : _.get(this.message, 'meta.System.remote_to_user', remoteUserNotAvail), subject : this.message.subject, modTimestamp : this.message.modTimestamp.format(modTimestampFormat), msgNum : this.messageIndex + 1, diff --git a/core/message_area.js b/core/message_area.js index f06c21a4..353430ce 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -26,10 +26,13 @@ exports.getAvailableMessageAreasByConfTag = getAvailableMessageAreasByConfTag; exports.getSortedAvailMessageAreasByConfTag = getSortedAvailMessageAreasByConfTag; exports.getDefaultMessageConferenceTag = getDefaultMessageConferenceTag; exports.getDefaultMessageAreaTagByConfTag = getDefaultMessageAreaTagByConfTag; +exports.getSuitableMessageConfAndAreaTags = getSuitableMessageConfAndAreaTags; exports.getMessageConferenceByTag = getMessageConferenceByTag; exports.getMessageAreaByTag = getMessageAreaByTag; exports.changeMessageConference = changeMessageConference; exports.changeMessageArea = changeMessageArea; +exports.hasMessageConfAndAreaRead = hasMessageConfAndAreaRead; +exports.filterMessageAreaTagsByReadACS = filterMessageAreaTagsByReadACS; exports.tempChangeMessageConfAndArea = tempChangeMessageConfAndArea; exports.getMessageListForArea = getMessageListForArea; exports.getNewMessageCountInAreaForUser = getNewMessageCountInAreaForUser; @@ -185,7 +188,10 @@ function getDefaultMessageAreaTagByConfTag(client, confTag, disableAcsCheck) { } } - defaultArea = _.findKey(areaPool, (area) => { + defaultArea = _.findKey(areaPool, (area, areaTag) => { + if(Message.isPrivateAreaTag(areaTag)) { + return false; + } return (true === disableAcsCheck || client.acs.hasMessageAreaRead(area)); }); @@ -193,8 +199,47 @@ function getDefaultMessageAreaTagByConfTag(client, confTag, disableAcsCheck) { } } +function getSuitableMessageConfAndAreaTags(client) { + // + // Attempts to get a pair of suitable conf/area tags: + // * Where the client/user has proper ACS to both + // * Try to use defaults where possible + // * If default conf/area is not an option, use any + // that pass ACS. + // * Returns a tuple [confTag, areaTag]; areaTag + // and possibly confTag may both be set to '' if + // if we fail to find something. + // + let confTag = getDefaultMessageConferenceTag(client); + if(!confTag) { + return ['', '']; // can't have an area without a conf + } + + let areaTag = getDefaultMessageAreaTagByConfTag(client, confTag); + if(!areaTag) { + // OK, perhaps *any* area in *any* conf? + _.forEach(Config().messageConferences, (conf, ct) => { + if(!client.acs.hasMessageConfRead(conf)) { + return; + } + _.forEach(conf.areas, (area, at) => { + if(!_.includes(Message.WellKnownAreaTags, at) && client.acs.hasMessageAreaRead(area)) { + confTag = ct; + areaTag = at; + return false; // stop inner iteration + } + }); + if(areaTag) { + return false; // stop iteration + } + }); + } + + return [confTag, areaTag || '']; +} + function getMessageConferenceByTag(confTag) { - return Config().messageConferences[confTag]; + return Object.assign({ confTag }, Config().messageConferences[confTag]); } function getMessageConfTagByAreaTag(areaTag) { @@ -209,17 +254,23 @@ function getMessageAreaByTag(areaTag, optionalConfTag) { // :TODO: this could be cached if(_.isString(optionalConfTag)) { - if(_.has(confs, [ optionalConfTag, 'areas', areaTag ])) { - return confs[optionalConfTag].areas[areaTag]; + if(_.isObject(confs, [ optionalConfTag, 'areas', areaTag ])) { + return Object.assign( + { + areaTag, + confTag : optionalConfTag, + }, + confs[optionalConfTag].areas[areaTag] + ); } } else { // // No confTag to work with - we'll have to search through them all // let area; - _.forEach(confs, (v) => { - if(_.has(v, [ 'areas', areaTag ])) { - area = v.areas[areaTag]; + _.forEach(confs, (conf, confTag) => { + if(_.isObject(conf, [ 'areas', areaTag ])) { + area = Object.assign({ areaTag, confTag }, conf.areas[areaTag]); return false; // stop iteration } }); @@ -350,6 +401,22 @@ function changeMessageArea(client, areaTag, cb) { changeMessageAreaWithOptions(client, areaTag, { persist : true }, cb); } +function hasMessageConfAndAreaRead(client, area) { + const conf = getMessageConfTagByAreaTag(area.areaTag); + return client.acs.hasMessageConfRead(conf) && client.acs.hasMessageAreaRead(area); +} + +function filterMessageAreaTagsByReadACS(client, areaTags) { + if(!Array.isArray(areaTags)) { + areaTags = [ areaTags ]; + } + + return areaTags.filter( areaTag => { + const area = getMessageAreaByTag(areaTag); + return hasMessageConfAndAreaRead(client, area); + }); +} + function getNewMessageCountInAreaForUser(userId, areaTag, cb) { getMessageAreaLastReadId(userId, areaTag, (err, lastMessageId) => { lastMessageId = lastMessageId || 0; diff --git a/core/user_login.js b/core/user_login.js index 6c5e6813..6b9a0e00 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -21,6 +21,7 @@ const { getMessageAreaByTag, getDefaultMessageConferenceTag, getDefaultMessageAreaTagByConfTag, + getSuitableMessageConfAndAreaTags, } = require('./message_area.js'); const { getFileAreaByTag, @@ -128,15 +129,6 @@ function userLogin(client, username, password, options, cb) { } function postLoginPrep(client, cb) { - - const defaultMsgAreaTag = (confTag) => { - return ( - getDefaultMessageAreaTagByConfTag(client, confTag) || - getDefaultMessageAreaTagByConfTag(client, getDefaultMessageConferenceTag(client)) || - '' - ); - }; - async.series( [ (callback) => { @@ -144,23 +136,20 @@ function postLoginPrep(client, cb) { // User may (no longer) have read (view) rights to their current // message, conferences and/or areas. Move them out if so. // - let confTag = client.user.getProperty(UserProps.MessageConfTag); - const conf = getMessageConferenceByTag(confTag) || {}; - const area = getMessageAreaByTag(client.user.getProperty(UserProps.MessageAreaTag), confTag) || {}; + const confTag = client.user.getProperty(UserProps.MessageConfTag); + const conf = getMessageConferenceByTag(confTag) || {}; + const area = getMessageAreaByTag(client.user.getProperty(UserProps.MessageAreaTag), confTag) || {}; - if(!client.acs.hasMessageConfRead(conf)) { - confTag = getDefaultMessageConferenceTag(client) || ''; + if(!client.acs.hasMessageConfRead(conf) || !client.acs.hasMessageAreaRead(area)) { + // move them out of both area and possibly conf to something suitable, hopefully. + const [newConfTag, newAreaTag] = getSuitableMessageConfAndAreaTags(client); client.user.persistProperties({ - [ UserProps.MessageConfTag ] : confTag, - [ UserProps.MessageAreaTag ] : defaultMsgAreaTag(confTag), + [ UserProps.MessageConfTag ] : newConfTag, + [ UserProps.MessageAreaTag ] : newAreaTag, }, err => { return callback(err); }); - } else if (!client.acs.hasMessageAreaRead(area)) { - client.user.persistProperty(UserProps.MessageAreaTag, defaultMsgAreaTag(confTag), err => { - return callback(err); - }); } else { return callback(null); } From d5268c7b9e7a94681433a4a614205c3ce8e8e389 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 11 Sep 2019 21:56:33 -0600 Subject: [PATCH 129/140] Fix getMessageAreaByTag() and improve hasMessageConfAndAreaRead() --- core/message_area.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/core/message_area.js b/core/message_area.js index 353430ce..13751e65 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -254,7 +254,7 @@ function getMessageAreaByTag(areaTag, optionalConfTag) { // :TODO: this could be cached if(_.isString(optionalConfTag)) { - if(_.isObject(confs, [ optionalConfTag, 'areas', areaTag ])) { + if(_.has(confs, [ optionalConfTag, 'areas', areaTag ])) { return Object.assign( { areaTag, @@ -269,7 +269,7 @@ function getMessageAreaByTag(areaTag, optionalConfTag) { // let area; _.forEach(confs, (conf, confTag) => { - if(_.isObject(conf, [ 'areas', areaTag ])) { + if(_.has(conf, [ 'areas', areaTag ])) { area = Object.assign({ areaTag, confTag }, conf.areas[areaTag]); return false; // stop iteration } @@ -401,9 +401,12 @@ function changeMessageArea(client, areaTag, cb) { changeMessageAreaWithOptions(client, areaTag, { persist : true }, cb); } -function hasMessageConfAndAreaRead(client, area) { - const conf = getMessageConfTagByAreaTag(area.areaTag); - return client.acs.hasMessageConfRead(conf) && client.acs.hasMessageAreaRead(area); +function hasMessageConfAndAreaRead(client, areaOrTag) { + if(_.isString(areaOrTag)) { + areaOrTag = getMessageAreaByTag(areaOrTag) || {}; + } + const conf = getMessageConferenceByTag(areaOrTag.confTag); + return client.acs.hasMessageConfRead(conf) && client.acs.hasMessageAreaRead(areaOrTag); } function filterMessageAreaTagsByReadACS(client, areaTags) { @@ -472,8 +475,7 @@ function getMessageListForArea(client, areaTag, filter, cb) } if(client) { - const area = getMessageAreaByTag(areaTag); - if(!client.acs.hasMessageAreaRead(area)) { + if(!hasMessageConfAndAreaRead(client, areaTag)) { return cb(null, []); } } From 4f6a0b175c3f1a5ab58a949cd52d8363c9209144 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 11 Sep 2019 21:56:54 -0600 Subject: [PATCH 130/140] Don't return messages user doesn't have access to! --- core/my_messages.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/core/my_messages.js b/core/my_messages.js index 721e1f9d..65a9335d 100644 --- a/core/my_messages.js +++ b/core/my_messages.js @@ -5,6 +5,9 @@ const MenuModule = require('./menu_module.js').MenuModule; const Message = require('./message.js'); const UserProps = require('./user_property.js'); +const { + hasMessageConfAndAreaRead +} = require('./message_area.js'); exports.moduleInfo = { name : 'My Messages', @@ -30,7 +33,27 @@ exports.getModule = class MyMessagesModule extends MenuModule { this.client.log.warn( { error : err.message }, 'Error finding messages addressed to current user'); return this.prevMenu(); } - this.messageList = messageList; + + // + // We need to filter out messages belonging to conf/areas the user + // doesn't have access to. + // + // Keep a cache around for quick lookup. + // + const acsCache = new Map(); // areaTag:boolean + this.messageList = messageList.filter(msg => { + let cached = acsCache.get(msg.areaTag); + if(false === cached) { + return false; + } + if(true === cached) { + return true; + } + cached = hasMessageConfAndAreaRead(this.client, msg.areaTag); + acsCache.set(msg.areaTag, cached); + return cached; + }); + this.finishedLoading(); }); } From 36afcc0298456b85aee038eefcb5e533d1171764 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 11 Sep 2019 21:57:07 -0600 Subject: [PATCH 131/140] Cleanup unused --- core/user_login.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/user_login.js b/core/user_login.js index 6b9a0e00..99faa1a2 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -19,8 +19,6 @@ const User = require('./user.js'); const { getMessageConferenceByTag, getMessageAreaByTag, - getDefaultMessageConferenceTag, - getDefaultMessageAreaTagByConfTag, getSuitableMessageConfAndAreaTags, } = require('./message_area.js'); const { From afd6d4265f99c4e31ddb65d00ef340fe5b996bb5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 11 Sep 2019 22:03:24 -0600 Subject: [PATCH 132/140] More filtering of messages results without ACS + filterMessageListByReadACS() * Use filterMessageListByReadACS() in my messages * Use filterMessageListByReadACS() in message search --- core/message_area.js | 24 ++++++++++++++++++++++++ core/message_base_search.js | 28 ++++++++++++++++++++++------ core/my_messages.js | 23 +++-------------------- 3 files changed, 49 insertions(+), 26 deletions(-) diff --git a/core/message_area.js b/core/message_area.js index 13751e65..a0806bc0 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -33,6 +33,7 @@ exports.changeMessageConference = changeMessageConference; exports.changeMessageArea = changeMessageArea; exports.hasMessageConfAndAreaRead = hasMessageConfAndAreaRead; exports.filterMessageAreaTagsByReadACS = filterMessageAreaTagsByReadACS; +exports.filterMessageListByReadACS = filterMessageListByReadACS; exports.tempChangeMessageConfAndArea = tempChangeMessageConfAndArea; exports.getMessageListForArea = getMessageListForArea; exports.getNewMessageCountInAreaForUser = getNewMessageCountInAreaForUser; @@ -420,6 +421,29 @@ function filterMessageAreaTagsByReadACS(client, areaTags) { }); } +function filterMessageListByReadACS(client, messageList) { + // + // Filter out messages belonging to conf/areas the user + // doesn't have access to. + // + + // Keep a cache around for quick lookup. + const acsCache = new Map(); // areaTag:boolean + + return messageList.filter(msg => { + let cached = acsCache.get(msg.areaTag); + if(false === cached) { + return false; + } + if(true === cached) { + return true; + } + cached = hasMessageConfAndAreaRead(client, msg.areaTag); + acsCache.set(msg.areaTag, cached); + return cached; + }); +} + function getNewMessageCountInAreaForUser(userId, areaTag, cb) { getMessageAreaLastReadId(userId, areaTag, (err, lastMessageId) => { lastMessageId = lastMessageId || 0; diff --git a/core/message_base_search.js b/core/message_base_search.js index 9684b8f0..859d2320 100644 --- a/core/message_base_search.js +++ b/core/message_base_search.js @@ -7,6 +7,8 @@ const { getSortedAvailMessageConferences, getAvailableMessageAreasByConfTag, getSortedAvailMessageAreasByConfTag, + hasMessageConfAndAreaRead, + filterMessageListByReadACS, } = require('./message_area.js'); const Errors = require('./enig_error.js').Errors; const Message = require('./message.js'); @@ -101,6 +103,14 @@ exports.getModule = class MessageBaseSearch extends MenuModule { limit : 2048, // :TODO: best way to handle this? we should probably let the user know if some results are returned }; + const returnNoResults = () => { + return this.gotoMenu( + this.menuConfig.config.noResultsMenu || 'messageSearchNoResults', + { menuFlags : [ 'popParent' ] }, + cb + ); + }; + if(isAdvanced) { filter.toUserName = value.toUserName; filter.fromUserName = value.fromUserName; @@ -113,7 +123,11 @@ exports.getModule = class MessageBaseSearch extends MenuModule { (area, areaTag) => areaTag ); } else if(value.areaTag) { - filter.areaTag = value.areaTag; // specific conf + area + if(hasMessageConfAndAreaRead(this.client, value.areaTag)) { + filter.areaTag = value.areaTag; // specific conf + area + } else { + return returnNoResults(); + } } } @@ -122,12 +136,14 @@ exports.getModule = class MessageBaseSearch extends MenuModule { return cb(err); } + // don't include results without ACS -- if the user searched by + // explicit conf/area tag, we should have already filtered (above) + if(!value.confTag && !value.areaTag) { + messageList = filterMessageListByReadACS(this.client, messageList); + } + if(0 === messageList.length) { - return this.gotoMenu( - this.menuConfig.config.noResultsMenu || 'messageSearchNoResults', - { menuFlags : [ 'popParent' ] }, - cb - ); + return returnNoResults(); } const menuOpts = { diff --git a/core/my_messages.js b/core/my_messages.js index 65a9335d..ed1d3fe1 100644 --- a/core/my_messages.js +++ b/core/my_messages.js @@ -6,7 +6,7 @@ const MenuModule = require('./menu_module.js').MenuModule; const Message = require('./message.js'); const UserProps = require('./user_property.js'); const { - hasMessageConfAndAreaRead + filterMessageListByReadACS } = require('./message_area.js'); exports.moduleInfo = { @@ -34,25 +34,8 @@ exports.getModule = class MyMessagesModule extends MenuModule { return this.prevMenu(); } - // - // We need to filter out messages belonging to conf/areas the user - // doesn't have access to. - // - // Keep a cache around for quick lookup. - // - const acsCache = new Map(); // areaTag:boolean - this.messageList = messageList.filter(msg => { - let cached = acsCache.get(msg.areaTag); - if(false === cached) { - return false; - } - if(true === cached) { - return true; - } - cached = hasMessageConfAndAreaRead(this.client, msg.areaTag); - acsCache.set(msg.areaTag, cached); - return cached; - }); + // don't include results without ACS + this.messageList = filterMessageListByReadACS(this.client, messageList); this.finishedLoading(); }); From 7783262af6d98fa12636e86122fac41be6c63b41 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 15 Sep 2019 21:30:00 -0600 Subject: [PATCH 133/140] ES6 --- core/acs.js | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/core/acs.js b/core/acs.js index 1ae4fa93..f1596d5d 100644 --- a/core/acs.js +++ b/core/acs.js @@ -14,6 +14,20 @@ class ACS { this.subject = subject; } + static get Defaults() { + return { + MessageConfRead : 'GM[users]', // list/read + MessageConfWrite : 'GM[users]', // post/write + + MessageAreaRead : 'GM[users]', // list/read; requires parent conf read + MessageAreaWrite : 'GM[users]', // post/write; requires parent conf write + + FileAreaRead : 'GM[users]', // list + FileAreaWrite : 'GM[sysops]', // upload + FileAreaDownload : 'GM[users]', // download + }; + } + check(acs, scope, defaultAcs) { acs = acs ? acs[scope] : defaultAcs; acs = acs || defaultAcs; @@ -32,10 +46,18 @@ class ACS { return this.check(conf.acs, 'read', ACS.Defaults.MessageConfRead); } + hasMessageConfWrite(conf) { + return this.check(conf.acs, 'write', ACS.Defaults.MessageConfWrite); + } + hasMessageAreaRead(area) { return this.check(area.acs, 'read', ACS.Defaults.MessageAreaRead); } + hasMessageAreaWrite(area) { + return this.check(area.acs, 'write', ACS.Defaults.MessageAreaWrite); + } + // // File Base / Areas // @@ -44,6 +66,7 @@ class ACS { } hasFileAreaWrite(area) { + // :TODO: create 'upload' alias? return this.check(area.acs, 'write', ACS.Defaults.FileAreaWrite); } @@ -91,13 +114,4 @@ class ACS { } } -ACS.Defaults = { - MessageAreaRead : 'GM[users]', - MessageConfRead : 'GM[users]', - - FileAreaRead : 'GM[users]', - FileAreaWrite : 'GM[sysops]', - FileAreaDownload : 'GM[users]', -}; - module.exports = ACS; From 1fdaaf5633acac887d45f99983a40dc2f4c28414 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 16 Sep 2019 22:05:03 -0600 Subject: [PATCH 134/140] Add 'write' access for post FSE + hasMessageConfAndAreaWrite() + MenuModule helpers: gotoMenuOrPrev(), gotoMenuOrShowMessage(). These will likely replace many other pieces of code soon. --- core/menu_module.js | 38 ++++++++++++++++++++++++++++++++++++++ core/message_area.js | 9 +++++++++ core/msg_area_post_fse.js | 26 +++++++++++++++++++++----- 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/core/menu_module.js b/core/menu_module.js index 95557e32..804065a7 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -259,6 +259,44 @@ exports.MenuModule = class MenuModule extends PluginModule { return this.client.menuStack.goto(name, options, cb); } + gotoMenuOrPrev(name, options, cb) { + this.client.menuStack.goto(name, options, err => { + if(!err) { + if(cb) { + return cb(null); + } + } + + return this.prevMenu(cb); + }); + } + + gotoMenuOrShowMessage(name, message, options, cb) { + if(!cb && _.isFunction(options)) { + cb = options; + options = {}; + } + + options = options || { clearScreen: true }; + + this.gotoMenu(name, options, err => { + if(err) { + if(options.clearScreen) { + this.client.term.rawWrite(ansi.resetScreen()); + } + + this.client.term.write(`${message}\n`); + return this.pausePrompt( () => { + return this.prevMenu(cb); + }); + } + + if(cb) { + return cb(null); + } + }); + } + reload(cb) { const prevMenu = this.client.menuStack.pop(); prevMenu.instance.leave(); diff --git a/core/message_area.js b/core/message_area.js index a0806bc0..579f1c9b 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -32,6 +32,7 @@ exports.getMessageAreaByTag = getMessageAreaByTag; exports.changeMessageConference = changeMessageConference; exports.changeMessageArea = changeMessageArea; exports.hasMessageConfAndAreaRead = hasMessageConfAndAreaRead; +exports.hasMessageConfAndAreaWrite = hasMessageConfAndAreaWrite; exports.filterMessageAreaTagsByReadACS = filterMessageAreaTagsByReadACS; exports.filterMessageListByReadACS = filterMessageListByReadACS; exports.tempChangeMessageConfAndArea = tempChangeMessageConfAndArea; @@ -410,6 +411,14 @@ function hasMessageConfAndAreaRead(client, areaOrTag) { return client.acs.hasMessageConfRead(conf) && client.acs.hasMessageAreaRead(areaOrTag); } +function hasMessageConfAndAreaWrite(client, areaOrTag) { + if(_.isString(areaOrTag)) { + areaOrTag = getMessageAreaByTag(areaOrTag) || {}; + } + const conf = getMessageConferenceByTag(areaOrTag.confTag); + return client.acs.hasMessageConfWrite(conf) && client.acs.hasMessageAreaWrite(areaOrTag); +} + function filterMessageAreaTagsByReadACS(client, areaTags) { if(!Array.isArray(areaTags)) { areaTags = [ areaTags ]; diff --git a/core/msg_area_post_fse.js b/core/msg_area_post_fse.js index 613cee04..f47717b2 100644 --- a/core/msg_area_post_fse.js +++ b/core/msg_area_post_fse.js @@ -4,6 +4,9 @@ const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; const persistMessage = require('./message_area.js').persistMessage; const UserProps = require('./user_property.js'); +const { + hasMessageConfAndAreaWrite, +} = require('./message_area.js'); const _ = require('lodash'); const async = require('async'); @@ -59,12 +62,25 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule { } enter() { - if(_.isString(this.client.user.properties[UserProps.MessageAreaTag]) && - !_.isString(this.messageAreaTag)) - { - this.messageAreaTag = this.client.user.properties[UserProps.MessageAreaTag]; - } + this.messageAreaTag = + this.messageAreaTag || + this.client.user.getProperty(UserProps.MessageAreaTag); super.enter(); } + + initSequence() { + if(!hasMessageConfAndAreaWrite(this.client, this.messageAreaTag)) { + const noAcsMenu = + this.menuConfig.config.messageBasePostMessageNoAccess || + 'messageBasePostMessageNoAccess'; + + return this.gotoMenuOrShowMessage( + noAcsMenu, + 'You do not have the proper access to post here!', + ); + } + + super.initSequence(); + } }; \ No newline at end of file From 351ae527760f2293c2d20e9e15b1cab129aae4a4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 29 Oct 2019 21:17:58 -0600 Subject: [PATCH 135/140] Dep updates --- core/client_connections.js | 2 +- core/file_area_web.js | 2 +- package.json | 34 ++--- yarn.lock | 268 +++++++++++++++++++++---------------- 4 files changed, 171 insertions(+), 135 deletions(-) diff --git a/core/client_connections.js b/core/client_connections.js index 21aa5c1c..e2c8d577 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -9,7 +9,7 @@ const UserProps = require('./user_property.js'); // deps const _ = require('lodash'); const moment = require('moment'); -const hashids = require('hashids'); +const hashids = require('hashids/cjs'); exports.getActiveConnections = getActiveConnections; exports.getActiveConnectionList = getActiveConnectionList; diff --git a/core/file_area_web.js b/core/file_area_web.js index b36c8b72..b6a3d8ae 100644 --- a/core/file_area_web.js +++ b/core/file_area_web.js @@ -19,7 +19,7 @@ const UserProps = require('./user_property.js'); const SysProps = require('./system_menu_method.js'); // deps -const hashids = require('hashids'); +const hashids = require('hashids/cjs'); const moment = require('moment'); const paths = require('path'); const async = require('async'); diff --git a/package.json b/package.json index 235018a7..f57d5ffa 100644 --- a/package.json +++ b/package.json @@ -23,39 +23,39 @@ ], "dependencies": { "async": "3.1.0", - "binary-parser": "1.4.0", + "binary-parser": "^1.5.0", "buffers": "github:NuSkooler/node-buffers", "bunyan": "^1.8.12", "exiftool": "^0.0.3", "fs-extra": "8.1.0", - "glob": "7.1.4", - "graceful-fs": "4.2.0", - "hashids": "^1.1.1", - "hjson": "^3.1.2", + "glob": "^7.1.5", + "graceful-fs": "^4.2.3", + "hashids": "^2.0.1", + "hjson": "^3.2.1", "iconv-lite": "0.5.0", - "inquirer": "6.4.1", + "inquirer": "^7.0.0", "later": "1.2.0", - "lodash": "^4.17.10", + "lodash": "^4.17.15", "lru-cache": "^5.1.1", "mime-types": "2.1.24", - "minimist": "1.2.x", + "minimist": "1.2.0", "moment": "^2.24.0", "nntp-server": "^1.0.3", - "node-pty": "^0.8.1", - "nodemailer": "6.2.1", + "node-pty": "^0.9.0", + "nodemailer": "^6.3.1", "otplib": "11.0.1", - "qrcode-generator": "1.4.3", + "qrcode-generator": "^1.4.4", "rlogin": "^1.0.0", "sane": "4.1.0", - "sanitize-filename": "^1.6.1", - "sqlite3": "4.0.9", + "sanitize-filename": "^1.6.3", + "sqlite3": "^4.1.0", "sqlite3-trans": "^1.2.1", - "ssh2": "0.8.4", + "ssh2": "^0.8.5", "temptmp": "^1.1.0", - "uuid": "^3.2.1", + "uuid": "^3.3.3", "uuid-parse": "1.1.0", - "ws": "7.0.1", - "xxhash": "^0.2.4", + "ws": "^7.2.0", + "xxhash": "^0.3.0", "yazl": "^2.5.1" }, "devDependencies": {}, diff --git a/yarn.lock b/yarn.lock index d9aade26..eaf58605 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25,10 +25,12 @@ ajv@^5.3.0: fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.3.0" -ansi-escapes@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" - integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== +ansi-escapes@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.2.1.tgz#4dccdb846c3eee10f6d64dea66273eab90c37228" + integrity sha512-Cg3ymMAdN10wOk/VYfLV7KCQyv7EDirJ64500sU7n9UlmioEtDuU5Gd+hj73hXSU/ex7tHJSssmyftDdkMLO8Q== + dependencies: + type-fest "^0.5.2" ansi-regex@^2.0.0: version "2.1.1" @@ -177,10 +179,10 @@ bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2: dependencies: tweetnacl "^0.14.3" -binary-parser@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/binary-parser/-/binary-parser-1.4.0.tgz#5f32d9b28f2027968dc660b680699dc030a6ea92" - integrity sha512-z4TOFQFy5YzhEy50mM+WV6BRu3xo0Vpe5qEtVwnUZkhibuNrPKkBXTpzSs2xcSW6TMVeXas77fEsEVV+lj4/iw== +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== brace-expansion@^1.1.7: version "1.1.11" @@ -288,12 +290,12 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -cli-cursor@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" - integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== dependencies: - restore-cursor "^2.0.0" + restore-cursor "^3.1.0" cli-width@^2.0.0: version "2.2.0" @@ -485,6 +487,11 @@ ecc-jsbn@~0.1.1: 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" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + end-of-stream@^1.1.0, end-of-stream@^1.4.0: version "1.4.1" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" @@ -603,10 +610,10 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -figures@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" - integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= +figures@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.1.0.tgz#4b198dd07d8d71530642864af2d45dd9e459c4ec" + integrity sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg== dependencies: escape-string-regexp "^1.0.5" @@ -708,18 +715,6 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -glob@7.1.4: - version "7.1.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" - integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@^6.0.1: version "6.0.4" resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" @@ -743,6 +738,18 @@ glob@^7.0.3, glob@^7.0.5, glob@^7.1.2: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.5: + version "7.1.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.5.tgz#6714c69bee20f3c3e64c4dd905553e532b40cdc0" + integrity sha512-J9dlskqUXK1OeTOYBEn5s8aMukWMwWfs+rPTn/jn50Ux4MNXVhubL1wu/j2t+H4NVI+cXEcCaYellqaPVGXNqQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + globby@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" @@ -754,16 +761,21 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" -graceful-fs@4.2.0, graceful-fs@^4.2.0: - version "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.1.6: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" integrity sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg= +graceful-fs@^4.2.0: + version "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== + har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" @@ -818,15 +830,15 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" -hashids@^1.1.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/hashids/-/hashids-1.2.2.tgz#28635c7f2f7360ba463686078eee837479e8eafb" - integrity sha512-dEHCG2LraR6PNvSGxosZHIRgxF5sNLOIBFEHbj8lfP9WWmu/PWPMzsip1drdVSOFi51N2pU7gZavrgn7sbGFuw== +hashids@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/hashids/-/hashids-2.0.1.tgz#cdee93b6de1a6f941c955084fe82658c4fba28da" + integrity sha512-Hr1lPEGhCkZSniYj9jAj8gQtcTBKNhf/tF14R1tu3zvIcwCT89/ucHEyObSIUZ42Y30SlIfOt7Sq0Uw+rhs6iQ== -hjson@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/hjson/-/hjson-3.1.2.tgz#1ae8a3a897a1fab8d45180f98e9abf9b56f95b55" - integrity sha512-2ILrho8eRl2Bniy61mDFiXRAloYqH2T6OwWkoF/8y55DPFgG2RcqQGNXIfBLp432dnAbLOpBJ4pJs63W3X27EA== +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" @@ -876,22 +888,22 @@ ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== -inquirer@6.4.1: - version "6.4.1" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.4.1.tgz#7bd9e5ab0567cd23b41b0180b68e0cfa82fc3c0b" - integrity sha512-/Jw+qPZx4EDYsaT6uz7F4GJRNFMRdKNeUZw3ZnKV8lyuUgz/YWRCSUAJMZSVhSq4Ec0R2oYnyi6b3d4JXcL5Nw== +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== dependencies: - ansi-escapes "^3.2.0" + ansi-escapes "^4.2.1" chalk "^2.4.2" - cli-cursor "^2.1.0" + cli-cursor "^3.1.0" cli-width "^2.0.0" external-editor "^3.0.3" - figures "^2.0.0" - lodash "^4.17.11" - mute-stream "0.0.7" + figures "^3.0.0" + lodash "^4.17.15" + mute-stream "0.0.8" run-async "^2.2.0" rxjs "^6.4.0" - string-width "^2.1.0" + string-width "^4.1.0" strip-ansi "^5.1.0" through "^2.3.6" @@ -970,6 +982,11 @@ is-fullwidth-code-point@^2.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -1116,7 +1133,12 @@ later@1.2.0: resolved "https://registry.yarnpkg.com/later/-/later-1.2.0.tgz#f2cf6c4dd7956dd2f520adf0329836e9876bad0f" integrity sha1-8s9sTdeVbdL1IK3wMpg26YdrrQ8= -lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4: +lodash@^4.17.15: + version "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== @@ -1190,10 +1212,10 @@ mime-types@^2.1.12, mime-types@~2.1.19: dependencies: mime-db "~1.36.0" -mimic-fn@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" - integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== "minimatch@2 || 3", minimatch@^3.0.4: version "3.0.4" @@ -1207,7 +1229,7 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= -minimist@1.2.x, minimist@^1.1.1, minimist@^1.2.0: +minimist@1.2.0, 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= @@ -1262,10 +1284,10 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== -mute-stream@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" - integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== mv@~2: version "2.1.1" @@ -1276,17 +1298,12 @@ mv@~2: ncp "~2.0.0" rimraf "~2.4.0" -nan@2.12.1: - version "2.12.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552" - integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw== - -nan@^2.10.0, nan@^2.4.0: +nan@^2.10.0: version "2.11.1" resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.1.tgz#90e22bccb8ca57ea4cd37cc83d3819b52eea6766" integrity sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA== -nan@^2.12.1: +nan@^2.12.1, nan@^2.13.2, nan@^2.14.0: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== @@ -1364,17 +1381,17 @@ node-pre-gyp@^0.11.0: semver "^5.3.0" tar "^4" -node-pty@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.8.1.tgz#94b457bec013e7a09b8d9141f63b0787fa25c23f" - integrity sha512-j+/g0Q5dR+vkELclpJpz32HcS3O/3EdPSGPvDXJZVJQLCvgG0toEbfmymxAEyQyZEpaoKHAcoL+PvKM+4N9nlw== +node-pty@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.9.0.tgz#8f9bcc0d1c5b970a3184ffd533d862c7eb6590a6" + integrity sha512-MBnCQl83FTYOu7B4xWw10AW77AAh7ThCE1VXEv+JeWj8mSpGo+0bwgsV+b23ljBFwEM9OmsOv3kM27iUPPm84g== dependencies: - nan "2.12.1" + nan "^2.14.0" -nodemailer@6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.2.1.tgz#20d773925eb8f7a06166a0b62c751dc8290429f3" - integrity sha512-TagB7iuIi9uyNgHExo8lUDq3VK5/B0BpbkcjIgNvxbtVrjNqq0DwAOTuzALPVkK76kMhTSzIgHqg8X1uklVs6g== +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== nopt@^4.0.1: version "4.0.1" @@ -1466,12 +1483,12 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" -onetime@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" - integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= +onetime@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" + integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q== dependencies: - mimic-fn "^1.0.0" + mimic-fn "^2.1.0" os-homedir@^1.0.0: version "1.0.2" @@ -1583,10 +1600,10 @@ punycode@^1.4.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= -qrcode-generator@1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/qrcode-generator/-/qrcode-generator-1.4.3.tgz#4876e8f280e65b6c94615f4c19c484f6b964b199" - integrity sha512-++rVRvMRq5BlHfmAafl8a4ppUntzUxCCUTT2t0siUgqKwdnqRzY8IH6f6WSX5dZUhD2Ul5/MIKuTJddflwrGzw== +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" @@ -1679,12 +1696,12 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -restore-cursor@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" - integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== dependencies: - onetime "^2.0.0" + onetime "^5.1.0" signal-exit "^3.0.2" ret@~0.1.10: @@ -1767,10 +1784,10 @@ sane@4.1.0: minimist "^1.1.1" walker "~1.0.5" -sanitize-filename@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.1.tgz#612da1c96473fa02dccda92dcd5b4ab164a6772a" - integrity sha1-YS2hyWRz+gLczaktzVtKsWSmdyo= +sanitize-filename@^1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378" + integrity sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg== dependencies: truncate-utf8-bytes "^1.0.0" @@ -1908,10 +1925,10 @@ sqlite3-trans@^1.2.1: dependencies: lodash "^4.17.4" -sqlite3@4.0.9: - version "4.0.9" - resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-4.0.9.tgz#cff74550fa5a1159956815400bdef69245557640" - integrity sha512-IkvzjmsWQl9BuBiM4xKpl5X8WCR4w0AeJHRdobCdXZ8dT/lNc1XS6WqvY35N6+YzIIgzSBeY5prdFObID9F9tA== +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== dependencies: nan "^2.12.1" node-pre-gyp "^0.11.0" @@ -1926,10 +1943,10 @@ ssh2-streams@~0.4.4: bcrypt-pbkdf "^1.0.2" streamsearch "~0.1.2" -ssh2@0.8.4: - version "0.8.4" - resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-0.8.4.tgz#0a657d9371c1fe9f9e349bcff6144febee256aa6" - integrity sha512-qztb9t4b34wJSiWVpeTMVVN/5KCuBoyctBc2BcSe/Uq4NRnF0gB16Iu5p72ILhdYATcMNwB5WppzPIEs/3wB8Q== +ssh2@^0.8.5: + version "0.8.5" + resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-0.8.5.tgz#9144cdd6c104aa81b2b16ce647c109f4bd138b57" + integrity sha512-TkvzxSYYUSQ8jb//HbHnJVui4fVEW7yu/zwBxwro/QaK2EGYtwB+8gdEChwHHuj142c5+250poMC74aJiwApPw== dependencies: ssh2-streams "~0.4.4" @@ -1971,7 +1988,7 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2", string-width@^2.1.0: +"string-width@^1.0.2 || 2": version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== @@ -1979,6 +1996,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string-width@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.1.0.tgz#ba846d1daa97c3c596155308063e075ed1c99aff" + integrity sha512-NrX+1dVVh+6Y9dnQ19pR0pP4FiEIlUvdTGn8pw6CKTNq5sgib2nIhmUNT5TAmhWmvKr3WcxBcP3E8nWezuipuQ== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^5.2.0" + string_decoder@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" @@ -2007,7 +2033,7 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@^5.1.0: +strip-ansi@^5.1.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== @@ -2138,6 +2164,11 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= +type-fest@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.5.2.tgz#d6ef42a0356c6cd45f49485c3b6281fc148e48a2" + integrity sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw== + union-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" @@ -2186,11 +2217,16 @@ 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.2.1, uuid@^3.3.2: +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" @@ -2226,10 +2262,10 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -ws@7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.0.1.tgz#1a04e86cc3a57c03783f4910fdb090cf31b8e165" - integrity sha512-ILHfMbuqLJvnSgYXLgy4kMntroJpe8hT41dOVWM8bxRuw6TK4mgMp9VJUNsZTEc5Bh+Mbs0DJT4M0N+wBG9l9A== +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" @@ -2238,12 +2274,12 @@ xtend@~4.0.1: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= -xxhash@^0.2.4: - version "0.2.4" - resolved "https://registry.yarnpkg.com/xxhash/-/xxhash-0.2.4.tgz#8b8a48162cfccc21b920fa500261187d40216c39" - integrity sha1-i4pIFiz8zCG5IPpQAmEYfUAhbDk= +xxhash@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/xxhash/-/xxhash-0.3.0.tgz#d20893a62c5b0f7260597dd55859b12a1e02c559" + integrity sha512-1ud2yyPiR1DJhgyF1ZVMt+Ijrn0VNS/wzej1Z8eSFfkNfRPp8abVZNV2u9tYy9574II0ZayZYZgJm8KJoyGLCw== dependencies: - nan "^2.4.0" + nan "^2.13.2" yallist@^3.0.0, yallist@^3.0.2: version "3.0.2" From c9ff904b2bb5fbbe5db3ece50ec08648cf2d830c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 8 Nov 2019 19:20:14 -0700 Subject: [PATCH 136/140] Fix various minor mistakes --- core/connect.js | 1 - core/file_base_area.js | 4 ++-- core/message.js | 8 ++++---- core/servers/content/nntp.js | 2 +- core/string_util.js | 8 +++++--- core/upload.js | 10 ++++------ core/view_controller.js | 2 +- 7 files changed, 17 insertions(+), 18 deletions(-) diff --git a/core/connect.js b/core/connect.js index 5d45eaa4..52ff67a0 100644 --- a/core/connect.js +++ b/core/connect.js @@ -112,7 +112,6 @@ function ansiAttemptDetectUTF8(client, cb) { } } return cb(null); - } }; diff --git a/core/file_base_area.js b/core/file_base_area.js index ec36c7ec..e3887b84 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -401,7 +401,7 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { // // :TODO: This isn't really always the case - how to handle this? We could do a quick detection... const decodedData = iconv.decode(data, 'cp437'); - fileEntry[descType] = sliceAtSauceMarker(decodedData, 0x1a); + fileEntry[descType] = sliceAtSauceMarker(decodedData); fileEntry[`${descType}Src`] = 'descFile'; return next(null); }); @@ -575,7 +575,7 @@ function populateFileEntryInfoFromFile(fileEntry, filePath, cb) { `${_.upperFirst(descType)} description command failed` ); } else { - stdout = (stdout || '').trim(); + stdout = stdout.trim(); if(stdout.length > 0) { const key = 'short' === descType ? 'desc' : 'descLong'; if('desc' === key) { diff --git a/core/message.js b/core/message.js index 26a3bf21..5291b82a 100644 --- a/core/message.js +++ b/core/message.js @@ -329,8 +329,8 @@ module.exports = class Message { appendWhereClause(`m.area_tag = "${Message.WellKnownAreaTags.Private}"`); appendWhereClause( `m.message_id IN ( - SELECT message_id - FROM message_meta + SELECT message_id + FROM message_meta WHERE meta_category = "System" AND meta_name = "${Message.SystemMetaNames.LocalToUserID}" AND meta_value = ${filter.privateTagUserId} )`); } else { @@ -619,7 +619,7 @@ module.exports = class Message { } const metaStmt = transOrDb.prepare( - `INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value) + `INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value) VALUES (?, ?, ?, ?);`); if(!_.isArray(value)) { @@ -840,7 +840,7 @@ module.exports = class Message { } else { const QUOTE_RE = /^ ((?:[A-Za-z0-9]{2}> )+(?:[A-Za-z0-9]{2}>)*) */; const quoted = []; - const input = _.trimEnd(this.message).replace(/\b/g, ''); + const input = _.trimEnd(this.message).replace(/\x08/g, ''); // find *last* tearline let tearLinePos = this.getTearLinePosition(input); diff --git a/core/servers/content/nntp.js b/core/servers/content/nntp.js index 9f7c5a13..237c0994 100644 --- a/core/servers/content/nntp.js +++ b/core/servers/content/nntp.js @@ -275,7 +275,7 @@ class NNTPServer extends NNTPServerBase { // const remoteFrom = _.get(message.meta, [ 'System', Message.SystemMetaNames.RemoteFromUser ]); message.nntpHeaders['X-FTN-From'] = remoteFrom ? `${fromName} <${remoteFrom}>` : fromName; - const remoteTo = _.get(message.meta [ 'System', Message.SystemMetaNames.RemoteToUser ]); + const remoteTo = _.get(message.meta, [ 'System', Message.SystemMetaNames.RemoteToUser ]); message.nntpHeaders['X-FTN-To'] = remoteTo ? `${toName} <${remoteTo}>` : toName; if(!message.replyToMsgId) { diff --git a/core/string_util.js b/core/string_util.js index 4f7741ae..fa9a9097 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -29,9 +29,11 @@ exports.isAnsiLine = isAnsiLine; exports.isFormattedLine = isFormattedLine; exports.splitTextAtTerms = splitTextAtTerms; -// :TODO: create Unicode verison of this -const VOWELS = [ 'a', 'e', 'i', 'o', 'u' ]; -VOWELS.concat(VOWELS.map(l => l.toUpperCase())); +// :TODO: create Unicode version of this +const VOWELS = [ + 'a', 'e', 'i', 'o', 'u', + 'A', 'E', 'I', 'O', 'U', +]; const SIMPLE_ELITE_MAP = { 'a' : '4', diff --git a/core/upload.js b/core/upload.js index 6eaff2ab..b451ac9a 100644 --- a/core/upload.js +++ b/core/upload.js @@ -387,13 +387,11 @@ exports.getModule = class UploadModule extends MenuModule { self.client.log.error( 'Failed moving physical upload file', { error : err.message, fileName : newEntry.fileName, source : src, dest : dst } ); - - if(!err && dst !== finalPath) { - // name changed; ajust before persist - newEntry.fileName = paths.basename(finalPath); - } - return nextEntry(null); // still try next file + } else if(dst !== finalPath) + { + // name changed; adjust before persist + newEntry.fileName = paths.basename(finalPath); } self.client.log.debug('Moved upload to area', { path : finalPath } ); diff --git a/core/view_controller.js b/core/view_controller.js index 9c0ffe19..f51c307f 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -219,7 +219,7 @@ function ViewController(options) { break; default : - propValue = propValue = conf[propName]; + propValue = conf[propName]; break; } } else { From aa70a6b1aad01328277650e9e544cd1916e0da0b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 8 Nov 2019 22:12:29 -0700 Subject: [PATCH 137/140] Minor dep updates --- package.json | 4 ++-- yarn.lock | 42 +++++++++++++++++++++--------------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index f57d5ffa..8fde7f20 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "bunyan": "^1.8.12", "exiftool": "^0.0.3", "fs-extra": "8.1.0", - "glob": "^7.1.5", + "glob": "7.1.6", "graceful-fs": "^4.2.3", "hashids": "^2.0.1", "hjson": "^3.2.1", @@ -50,7 +50,7 @@ "sanitize-filename": "^1.6.3", "sqlite3": "^4.1.0", "sqlite3-trans": "^1.2.1", - "ssh2": "^0.8.5", + "ssh2": "0.8.6", "temptmp": "^1.1.0", "uuid": "^3.3.3", "uuid-parse": "1.1.0", diff --git a/yarn.lock b/yarn.lock index eaf58605..067bb3c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -715,6 +715,18 @@ getpass@^0.1.1: 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" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^6.0.1: version "6.0.4" resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" @@ -738,18 +750,6 @@ glob@^7.0.3, glob@^7.0.5, glob@^7.1.2: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.5: - version "7.1.5" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.5.tgz#6714c69bee20f3c3e64c4dd905553e532b40cdc0" - integrity sha512-J9dlskqUXK1OeTOYBEn5s8aMukWMwWfs+rPTn/jn50Ux4MNXVhubL1wu/j2t+H4NVI+cXEcCaYellqaPVGXNqQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - globby@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" @@ -1934,21 +1934,21 @@ sqlite3@^4.1.0: node-pre-gyp "^0.11.0" request "^2.87.0" -ssh2-streams@~0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/ssh2-streams/-/ssh2-streams-0.4.4.tgz#7f07464c4b19ee93324995ec7164f110c5a13658" - integrity sha512-yNfPZgJO/N69TvYkpDHZBkXAXQzTpfzRkOphQu3PeUpZnrjp9VNa8RKDZkZDpjsWItay+I4NMAbZZ7DqHRt0AQ== +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== dependencies: asn1 "~0.2.0" bcrypt-pbkdf "^1.0.2" streamsearch "~0.1.2" -ssh2@^0.8.5: - version "0.8.5" - resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-0.8.5.tgz#9144cdd6c104aa81b2b16ce647c109f4bd138b57" - integrity sha512-TkvzxSYYUSQ8jb//HbHnJVui4fVEW7yu/zwBxwro/QaK2EGYtwB+8gdEChwHHuj142c5+250poMC74aJiwApPw== +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== dependencies: - ssh2-streams "~0.4.4" + ssh2-streams "~0.4.7" sshpk@^1.7.0: version "1.14.2" From 0cae6e656ddec24fbbbb7c9e7cf99c154644f29b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 12 Nov 2019 19:18:47 -0700 Subject: [PATCH 138/140] Add some docs around write conf/area ACS --- docs/messageareas/configuring-a-message-area.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/messageareas/configuring-a-message-area.md b/docs/messageareas/configuring-a-message-area.md index e099c9e0..011e9497 100644 --- a/docs/messageareas/configuring-a-message-area.md +++ b/docs/messageareas/configuring-a-message-area.md @@ -21,7 +21,8 @@ Each conference is represented by a entry under `messageConferences`. Each entri ### ACS An optional standard [ACS](/docs/configuration/acs.md) block can be supplied with the following rules: -* `read`: ACS require to read (see) this conference. Defaults to `GM[users]`. +* `read`: ACS required to read (see) this conference. Defaults to `GM[users]`. +* `write`: ACS required to write (post) to this conference. Defaults to `GM[users]`. ### Example @@ -55,7 +56,8 @@ Message Areas are topic specific containers for messages that live within a part ### ACS An optional standard [ACS](/docs/configuration/acs.md) block can be supplied with the following rules: -* `read`: ACS require to read (see) this conference. Defaults to `GM[users]`. +* `read`: ACS required to read (see) this area. Defaults to `GM[users]`. +* `write`: ACS required to write (post) to this area. Defaults to `GM[users]`. ### Example @@ -64,13 +66,14 @@ messageConferences: { local: { // ... see above ... areas: { - enigma_dev: { // Area tag - required elsewhere! - name: ENiGMA 1/2 Development - desc: ENiGMA 1/2 discussion! - sort: 1 + enigma_dev: { // Area tag - required elsewhere! + name: ENiGMA 1/2 Development + desc: ENiGMA 1/2 development and discussion! + sort: 1 default: true acs: { read: GM[users] // default + write: GM[l33t] // super elite ENiGMA 1/2 users! } } } From 4397e943795dc98eba3a56ea1e661f8947ae374a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 18 Nov 2019 20:33:31 -0700 Subject: [PATCH 139/140] Add auto sig editor docs --- docs/_includes/nav.md | 19 ++++++++++--------- docs/modding/autosig-edit.md | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 docs/modding/autosig-edit.md diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index 5ecbe58e..a0e136ce 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -3,13 +3,13 @@ - [Install script]({{ site.baseurl }}{% link installation/install-script.md %}) - [Docker]({{ site.baseurl }}{% link installation/docker.md %}) - [Manual installation]({{ site.baseurl }}{% link installation/manual.md %}) - - [OS / Hardware Specific]({{ site.baseurl }}{% link installation/os-hardware.md %}) - - [Raspberry Pi]({{ site.baseurl }}{% link installation/rpi.md %}) - - [Windows]({{ site.baseurl }}{% link installation/windows.md %}) + - [OS / Hardware Specific]({{ site.baseurl }}{% link installation/os-hardware.md %}) + - [Raspberry Pi]({{ site.baseurl }}{% link installation/rpi.md %}) + - [Windows]({{ site.baseurl }}{% link installation/windows.md %}) - [Your Network Setup]({{ site.baseurl }}{% link installation/network.md %}) - [Testing Your Installation]({{ site.baseurl }}{% link installation/testing.md %}) - [Production Installation]({{ site.baseurl }}{% link installation/production.md %}) - + - Configuration - [Creating Config Files]({{ site.baseurl }}{% link configuration/creating-config.md %}) - [SysOp Setup]({{ site.baseurl }}{% link configuration/sysop-setup.md %}) @@ -37,13 +37,13 @@ - [TIC Support]({{ site.baseurl }}{% link filebase/tic-support.md %}) (Importing from FTN networks) - Tips and tricks - [Network mounts and symlinks]({{ site.baseurl }}{% link filebase/network-mounts-and-symlinks.md %}) - + - Message Areas - [Configuring a Message Area]({{ site.baseurl }}{% link messageareas/configuring-a-message-area.md %}) - [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 %}) - + - [Netmail]({{ site.baseurl }}{% link messageareas/netmail.md %}) + - Art - [General]({{ site.baseurl }}{% link art/general.md %}) - [Themes]({{ site.baseurl }}{% link art/themes.md %}) @@ -59,10 +59,10 @@ - [Web]({{ site.baseurl }}{% link servers/web-server.md %}) - [Gopher]({{ site.baseurl }}{% link servers/gopher.md %}) - [NNTP]({{ site.baseurl }}{% link servers/nntp.md %}) - + - Modding - [Local Doors]({{ site.baseurl }}{% link modding/local-doors.md %}) - - [Door Servers]({{ site.baseurl }}{% link modding/door-servers.md %}) + - [Door Servers]({{ site.baseurl }}{% link modding/door-servers.md %}) - DoorParty - BBSLink - Combatnet @@ -85,6 +85,7 @@ - [Node to Node Messaging]({{ site.baseurl }}{% link modding/node-msg.md %}) - [Top X]({{ site.baseurl }}{% link modding/top-x.md %}) - [2FA/OTP Config]({{ site.baseurl }}{% link modding/user-2fa-otp-config.md %}) + - [Auto Signature Editor]({{ site.baseurl }}{% link modding/autosig-edit.md %}) - Administration - [oputil]({{ site.baseurl }}{% link admin/oputil.md %}) diff --git a/docs/modding/autosig-edit.md b/docs/modding/autosig-edit.md new file mode 100644 index 00000000..f0da9b18 --- /dev/null +++ b/docs/modding/autosig-edit.md @@ -0,0 +1,22 @@ +--- +layout: page +title: Auto Signature Editor +--- +## The Auto Signature Editor +The built in `autosig_edit` module allows users to edit their auto signatures (AKA "autosig"). + +### Theming +The following MCI codes are available: +* MCI 1 (ie: `MT1`): Editor +* MCI 2 (ie: `BT2`): Save button + +### Disabling Auto Signatures +Auto Signature support can be disabled for a particular message area by setting `autoSignatures` to false in the area's configuration block. + +Example: +```hjson +my_area: { + name: My Area + autoSignatures: false +} +``` From d712c36171be5ca3f75255f99bc3d7ae340000fe Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 19 Nov 2019 20:03:35 -0700 Subject: [PATCH 140/140] Some documentation updates --- UPGRADE.md | 4 ++-- WHATSNEW.md | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index ef6fd95a..bd0a47b0 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -8,7 +8,7 @@ This document covers basic upgrade notes for major ENiGMA½ version updates. # General Notes ## Configuration File Updates -In general, look at the `menu_template.in.hjson`, and `config_template.in.hjson` as well as the defualt `luciano_blocktronics/theme.hjson` files when you update. These files may come with new sections you wish to merge into your system! +In general, look at the `menu_template.in.hjson`, and `config_template.in.hjson` as well as the default `luciano_blocktronics/theme.hjson` files when you update. These files may come with new sections you wish to merge into your system! ### menu.hjson Upgrades often come with changes to the default `menu_template.in.hjson`. It is wise to use a *different* file name for your BBS's version of this file and point to it via `config.hjson`. For example: @@ -81,7 +81,7 @@ ENiGMA 0.0.8-alpha comes with some structure changes: With the change to the `./mods` directory, `@systemModule` is now implied for `module` declarations in `menu.hjson`. To use a user module in `./mods` you must specify `@userModule`! With the above changes, you'll need to to at least: -* Move your `~/.config/enigma-bbs/config.hjson` to `./config/config.hjson` or utlize the `--config` option. +* Move your `~/.config/enigma-bbs/config.hjson` to `./config/config.hjson` or utlize the `--config` option. * Move your `prompt.hjson` and `menu.hjson` (e.g. `myboardname.hjson`) to `./config` * Move any non-theme art files, and theme directories to their appropriate locations mentioned above * Move any module directories such as `message_post_evt` to `./mods/` diff --git a/WHATSNEW.md b/WHATSNEW.md index b1ec8ce6..bc8cc9f8 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -42,6 +42,11 @@ submit: [ } ] ``` +* Added `read` (list/view) and `write` (post) ACS support to message conferences and areas. +* Many new built in modules adding support for things like auto signatures, listing "my" messages, top stats, etc. Take a look in the docs for setting them up! +* Built in MRC support! +* Added an customizable achievement system! + ## 0.0.9-alpha * Development is now against Node.js 10.x LTS. While other Node.js series may continue to work, you're own your own and YMMV! @@ -82,7 +87,7 @@ submit: [ * Added web (http://, https://) based download manager including batch downloads. Clickable links if using [VTXClient](https://github.com/codewar65/VTX_ClientServer)! * General VTX hyperlink support for web links * DEL vs Backspace key differences in FSE -* Correly parse oddball `INTL`, `TOPT`, `FMPT`, `Via`, etc. FTN kludge lines +* Correctly parse oddball `INTL`, `TOPT`, `FMPT`, `Via`, etc. FTN kludge lines * NetMail support! You can now send and receive NetMail. To send a NetMail address a external user using `Name

` format from your personal email menu. For example, `Foo Bar <123:123/123>`. The system also detects other formats such asa `Name @ address` (`Foo Bar@123:123/123`) * `oputil.js`: Added `mb areafix` command to quickly send AreaFix messages from the command line. You can manually send them from personal mail as well. * `oputil.js fb rm|remove|del|delete` functionality to remove file base entries.