Sync up with master

This commit is contained in:
Bryan Ashby
2023-10-12 20:25:01 -06:00
62 changed files with 511 additions and 167 deletions

View File

@@ -505,9 +505,9 @@ class Achievements {
getFormatObject(info) {
return {
userName: info.user.username,
userRealName: info.user.properties[UserProps.RealName],
userLocation: info.user.properties[UserProps.Location],
userAffils: info.user.properties[UserProps.Affiliations],
userRealName: info.user.realName(false) || 'N/A',
userLocation: info.user.properties[UserProps.Location] || 'N/A',
userAffils: info.user.properties[UserProps.Affiliations] || 'N/A',
nodeId: info.client.node,
title: info.details.title,
//text : info.global ? info.details.globalText : info.details.text,

View File

@@ -24,7 +24,7 @@ function ANSIEscapeParser(options) {
this.graphicRendition = {};
this.parseState = {
re: /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex
re: /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsutEFGST])/g, // eslint-disable-line no-control-regex
};
options = miscUtil.valueWithDefault(options, {
@@ -37,6 +37,12 @@ function ANSIEscapeParser(options) {
this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, '');
this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25);
this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80);
this.breakWidth = this.termWidth;
// toNumber takes care of null, undefined etc as well.
let artWidth = _.toNumber(options.artWidth);
if(!(_.isNaN(artWidth)) && artWidth > 0 && artWidth < this.breakWidth) {
this.breakWidth = options.artWidth;
}
this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default');
this.row = Math.min(options?.startRow ?? 1, this.termHeight);
@@ -90,8 +96,8 @@ function ANSIEscapeParser(options) {
switch (charCode) {
case CR:
self.emit('literal', text.slice(start, pos));
start = pos;
self.emit('literal', text.slice(start, pos + 1));
start = pos + 1;
self.column = 1;
@@ -105,8 +111,8 @@ function ANSIEscapeParser(options) {
self.column = 1;
}
self.emit('literal', text.slice(start, pos));
start = pos;
self.emit('literal', text.slice(start, pos + 1));
start = pos + 1;
self.row += 1;
@@ -114,13 +120,16 @@ function ANSIEscapeParser(options) {
break;
default:
if (self.column === self.termWidth) {
if (self.column === self.breakWidth) {
self.emit('literal', text.slice(start, pos + 1));
start = pos + 1;
// If we hit breakWidth before termWidth then we need to force the terminal to go to the next line.
if(self.column < self.termWidth) {
self.emit('literal', '\r\n');
}
self.column = 1;
self.row += 1;
self.positionUpdated();
} else {
self.column += 1;
@@ -135,7 +144,7 @@ function ANSIEscapeParser(options) {
//
// Finalize this chunk
//
if (self.column > self.termWidth) {
if (self.column > self.breakWidth) {
self.column = 1;
self.row += 1;
@@ -222,7 +231,7 @@ function ANSIEscapeParser(options) {
self.parseState = {
// ignore anything past EOF marker, if any
buffer: input.split(String.fromCharCode(0x1a), 1)[0],
re: /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex
re: /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsutEFGST])/g, // eslint-disable-line no-control-regex
stop: false,
};
};

View File

@@ -41,11 +41,21 @@ const SUPPORTED_ART_TYPES = {
};
function getFontNameFromSAUCE(sauce) {
if (sauce.Character) {
if (sauce && sauce.Character) {
return sauce.Character.fontName;
}
}
function getWidthFromSAUCE(sauce) {
if (sauce && sauce.Character) {
let sauceWidth = _.toNumber(sauce.Character.characterWidth);
if(!(_.isNaN(sauceWidth)) && sauceWidth > 0) {
return sauceWidth;
}
}
return null;
}
function sliceAtEOF(data, eofMarker) {
let eof = data.length;
const stopPos = Math.max(data.length - 256, 0); // 256 = 2 * sizeof(SAUCE)
@@ -274,6 +284,7 @@ function display(client, art, options, cb) {
mciReplaceChar: options.mciReplaceChar,
termHeight: client.term.termHeight,
termWidth: client.term.termWidth,
artWidth: getWidthFromSAUCE(options.sauce),
trailingLF: options.trailingLF,
startRow: options.startRow,
});

View File

@@ -87,7 +87,7 @@ function getActiveConnectionList(
//
entry.text = ac.user?.username || 'N/A';
entry.userName = ac.user?.username || 'N/A';
entry.realName = ac.user?.getProperty(UserProps.RealName) || 'N/A';
entry.realName = ac.user?.realName(false) || 'N/A';
entry.location = ac.user?.getProperty(UserProps.Location) || 'N/A';
entry.affils = entry.affiliation =
ac.user?.getProperty(UserProps.Affiliations) || 'N/A';

View File

@@ -188,22 +188,15 @@ module.exports = () => {
//
// 1 - Generate a Private Key (PK):
// Currently ENiGMA 1/2 requires a PKCS#1 PEM formatted PK.
// To generate a secure PK, issue the following command:
//
// > openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \
// -pkeyopt rsa_keygen_pubexp:65537 | openssl rsa \
// -out ./config/security/ssh_private_key.pem -aes128
//
// (The above is a more modern equivalent of the following):
// > openssl genrsa -aes128 -out ./config/security/ssh_private_key.pem 2048
// For information on generating a key, see:
// https://nuskooler.github.io/enigma-bbs/servers/loginservers/ssh.html#generate-a-ssh-private-key
//
// 2 - Set 'privateKeyPass' to the password you used in step #1
//
// 3 - Finally, set 'enabled' to 'true'
//
// Additional reading:
// - https://blog.sleeplessbeastie.eu/2017/12/28/how-to-generate-private-key/
// - https://gist.github.com/briansmith/2ee42439923d8e65a266994d0f70180b
// - https://nuskooler.github.io/enigma-bbs/servers/loginservers/ssh.html
//
privateKeyPem: paths.join(
__dirname,
@@ -222,14 +215,18 @@ module.exports = () => {
//
algorithms: {
kex: [
'curve25519-sha256',
'curve25519-sha256@libssh.org',
'ecdh-sha2-nistp256',
'ecdh-sha2-nistp384',
'ecdh-sha2-nistp521',
'diffie-hellman-group14-sha1',
'diffie-hellman-group1-sha1',
// Group exchange not currnetly supported
// 'diffie-hellman-group-exchange-sha256',
// 'diffie-hellman-group-exchange-sha1',
'curve25519-sha256',
'curve25519-sha256@libssh.org',
'ecdh-sha2-nistp256',
'ecdh-sha2-nistp384',
'ecdh-sha2-nistp521',
],
cipher: [
'aes128-ctr',
@@ -242,12 +239,7 @@ module.exports = () => {
'aes256-cbc',
'aes192-cbc',
'aes128-cbc',
'blowfish-cbc',
'3des-cbc',
'arcfour256',
'arcfour128',
'cast128-cbc',
'arcfour',
],
hmac: [
'hmac-sha2-256',

View File

@@ -57,8 +57,10 @@ module.exports = class Door {
run(exeInfo, cb) {
this.encoding = (exeInfo.encoding || 'cp437').toLowerCase();
if ('socket' === this.io && !this.sockServer) {
return cb(Errors.UnexpectedState('Socket server is not running'));
if ('socket' === this.io) {
if(!this.sockServer) {
return cb(Errors.UnexpectedState('Socket server is not running'));
}
} else if ('stdio' !== this.io) {
return cb(Errors.Invalid(`"${this.io}" is not a valid io type!`));
}

View File

@@ -167,6 +167,7 @@ exports.FullScreenEditorModule =
var newFocusViewId;
if (errMsgView) {
if (err) {
errMsgView.clearText();
errMsgView.setText(err.message);
if (MciViewIds.header.subject === err.view.getId()) {
@@ -183,6 +184,13 @@ exports.FullScreenEditorModule =
return cb(null);
},
editModeEscPressed: function (formData, extraArgs, cb) {
const errMsgView = self.viewControllers.header.getView(
MciViewIds.header.errorMsg
);
if (errMsgView) {
errMsgView.clearText();
}
self.footerMode =
'editor' === self.footerMode ? 'editorMenu' : 'editor';
@@ -982,11 +990,7 @@ exports.FullScreenEditorModule =
const area = getMessageAreaByTag(self.messageAreaTag);
if (fromView !== undefined) {
if (area && area.realNames) {
fromView.setText(
self.client.user.properties[
UserProps.RealName
] || self.client.user.username
);
fromView.setText(self.client.user.realName());
} else {
fromView.setText(self.client.user.username);
}
@@ -1054,7 +1058,7 @@ exports.FullScreenEditorModule =
posView.setText(
_.padStart(String(pos.row + 1), 2, '0') +
',' +
_.padEnd(String(pos.col + 1), 2, '0')
_.padStart(String(pos.col + 1), 2, '0')
);
this.client.term.rawWrite(ansi.restorePos());
}

View File

@@ -135,7 +135,7 @@ module.exports = class Address {
static fromString(addrStr) {
const m = FTN_ADDRESS_REGEXP.exec(addrStr);
if (m) {
if (m && m[2] && m[3]) {
// start with a 2D
let addr = {
net: parseInt(m[2]),

View File

@@ -763,6 +763,11 @@ module.exports = class Message {
}
persist(cb) {
const containsNonWhitespaceCharacterRegEx = /\S/;
if (!containsNonWhitespaceCharacterRegEx.test(this.message)) {
return cb(Errors.Invalid('Empty message'));
}
if (!this.isValid()) {
return cb(Errors.Invalid('Cannot persist invalid message!'));
}

View File

@@ -14,6 +14,39 @@ exports.moduleInfo = {
author: 'NuSkooler',
};
const MciViewIds = {
header: {
from: 1,
to: 2,
subject: 3,
errorMsg: 4,
modTimestamp: 5,
msgNum: 6,
msgTotal: 7,
customRangeStart: 10, // 10+ = customs
},
body: {
message: 1,
},
// :TODO: quote builder MCIs - remove all magic #'s
// :TODO: consolidate all footer MCI's - remove all magic #'s
ViewModeFooter: {
MsgNum: 6,
MsgTotal: 7,
// :TODO: Just use custom ranges
},
quoteBuilder: {
quotedMsg: 1,
// 2 NYI
quoteLines: 3,
},
};
exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
constructor(options) {
super(options);
@@ -42,19 +75,25 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
],
function complete(err) {
if (err) {
// :TODO:... sooooo now what?
} else {
// note: not logging 'from' here as it's part of client.log.xxxx()
self.client.log.info(
{
to: msg.toUserName,
subject: msg.subject,
uuid: msg.messageUuid,
},
`User "${self.client.user.username}" posted message to "${msg.toUserName}" (${msg.areaTag})`
const errMsgView = self.viewControllers.header.getView(
MciViewIds.header.errorMsg
);
if (errMsgView) {
errMsgView.setText(err.message);
}
return cb(err);
}
// note: not logging 'from' here as it's part of client.log.xxxx()
self.client.log.info(
{
to: msg.toUserName,
subject: msg.subject,
uuid: msg.messageUuid,
},
`User "${self.client.user.username}" posted message to "${msg.toUserName}" (${msg.areaTag})`
);
return self.nextMenu(cb);
}
);

View File

@@ -21,10 +21,7 @@ exports.getModule = class MyMessagesModule extends MenuModule {
initSequence() {
const filter = {
toUserName: [
this.client.user.username,
this.client.user.getProperty(UserProps.RealName),
],
toUserName: [this.client.user.username, this.client.user.realName()],
sort: 'modTimestamp',
resultType: 'messageList',
limit: 1024 * 16, // we want some sort of limit...

View File

@@ -13,6 +13,7 @@ const UserProps = require('./user_property.js');
// deps
const _ = require('lodash');
const moment = require('moment');
exports.moduleInfo = {
name: 'NUA',
@@ -95,15 +96,15 @@ exports.getModule = class NewUserAppModule extends MenuModule {
areaTag = areaTag || '';
newUser.properties = {
[UserProps.RealName]: formData.value.realName,
[UserProps.RealName]: formData.value.realName || '',
[UserProps.Birthdate]: getISOTimestampString(
formData.value.birthdate
formData.value.birthdate || moment()
),
[UserProps.Sex]: formData.value.sex,
[UserProps.Location]: formData.value.location,
[UserProps.Affiliations]: formData.value.affils,
[UserProps.EmailAddress]: formData.value.email,
[UserProps.WebAddress]: formData.value.web,
[UserProps.Sex]: formData.value.sex || '',
[UserProps.Location]: formData.value.location || '',
[UserProps.Affiliations]: formData.value.affils || '',
[UserProps.EmailAddress]: formData.value.email || '',
[UserProps.WebAddress]: formData.value.web || '',
[UserProps.AccountCreated]: getISOTimestampString(),
[UserProps.MessageConfTag]: confTag,

View File

@@ -21,8 +21,10 @@ exports.validateEmailAvail = validateEmailAvail;
exports.validateBirthdate = validateBirthdate;
exports.validatePasswordSpec = validatePasswordSpec;
const emptyFieldError = () => new Error('Field cannot be empty');
function validateNonEmpty(data, cb) {
return cb(data && data.length > 0 ? null : new Error('Field cannot be empty'));
return cb(data && data.length > 0 ? null : emptyFieldError);
}
function validateMessageSubject(data, cb) {
@@ -91,7 +93,11 @@ function validateGeneralMailAddressedTo(data, cb) {
// :TODO: remove hard-coded FTN check here. We need a decent way to register global supported flavors with modules.
const addressedToInfo = getAddressedToInfo(data);
if (Message.AddressFlavor.FTN === addressedToInfo.flavor) {
if (addressedToInfo.name.length === 0) {
return cb(emptyFieldError());
}
if (Message.AddressFlavor.Local !== addressedToInfo.flavor) {
return cb(null);
}

View File

@@ -179,6 +179,10 @@ TextView.prototype.setText = function (text, redraw) {
};
TextView.prototype.clearText = function () {
if (this.text) {
this.setText(this.fillChar.repeat(this.text.length));
}
this.setText('');
};

View File

@@ -124,12 +124,29 @@ module.exports = class User {
return isMember;
}
realName(withUsernameFallback = true) {
const realName = this.getProperty(UserProps.RealName);
if (realName) {
return realName;
}
if (withUsernameFallback) {
return this.username;
}
}
getSanitizedName(type = 'username') {
const name =
'real' === type ? this.getProperty(UserProps.RealName) : this.username;
const name = 'real' === type ? this.realName(true) : this.username;
return sanatizeFilename(name) || `user${this.userId.toString()}`;
}
emailAddress() {
const email = this.getProperty(UserProps.EmailAddress);
if (email) {
const realName = this.realName(false);
return realName ? `${realName} <${email}>` : email;
}
}
isAvailable() {
return (this.statusFlags & User.StatusFlags.NotAvailable) == 0;
}

View File

@@ -93,9 +93,7 @@ module.exports = class User2FA_OTPWebRegister {
}
const message = {
to: `${
user.getProperty(UserProps.RealName) || user.username
} <${user.getProperty(UserProps.EmailAddress)}>`,
to: user.emailAddress(),
// from will be filled in
subject: '2-Factor Authentication Registration',
text: textTemplate,

View File

@@ -115,15 +115,15 @@ exports.getModule = class UserConfigModule extends MenuModule {
formData = _.clone(formData);
const newProperties = {
[UserProps.RealName]: formData.value.realName,
[UserProps.RealName]: formData.value.realName || '',
[UserProps.Birthdate]: getISOTimestampString(
formData.value.birthdate
formData.value.birthdate || moment()
),
[UserProps.Sex]: formData.value.sex,
[UserProps.Location]: formData.value.location,
[UserProps.Affiliations]: formData.value.affils,
[UserProps.EmailAddress]: formData.value.email,
[UserProps.WebAddress]: formData.value.web,
[UserProps.Sex]: formData.value.sex || '',
[UserProps.Location]: formData.value.location || '',
[UserProps.Affiliations]: formData.value.affils || '',
[UserProps.EmailAddress]: formData.value.email || '',
[UserProps.WebAddress]: formData.value.web || '',
[UserProps.TermHeight]: formData.value.termHeight.toString(),
[UserProps.ThemeId]:
self.availThemeInfo[formData.value.theme].themeId,
@@ -233,11 +233,7 @@ exports.getModule = class UserConfigModule extends MenuModule {
function populateViews(callback) {
const user = self.client.user;
self.setViewText(
'menu',
MciCodeIds.RealName,
user.properties[UserProps.RealName]
);
self.setViewText('menu', MciCodeIds.RealName, user.realName(false) || '');
self.setViewText(
'menu',
MciCodeIds.BirthDate,

View File

@@ -143,9 +143,7 @@ class WebPasswordReset {
}
const message = {
to: `${user.properties[UserProps.RealName] || user.username} <${
user.properties[UserProps.EmailAddress]
}>`,
to: user.emailAddress(),
// from will be filled in
subject: 'Forgot Password',
text: textTemplate,

View File

@@ -506,9 +506,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule {
// Current
currentUserName: this.client.user.username,
currentUserRealName:
this.client.user.getProperty(UserProps.RealName) ||
this.client.user.username,
currentUserRealName: this.client.user.realName(false) || 'N/A',
availIndicator: availIndicator,
visIndicator: visIndicator,
lastLoginUserName: lastLoginStats.userName,