Compare commits

..

No commits in common. "master" and "feature/docker_dev" have entirely different histories.

58 changed files with 988 additions and 1230 deletions

View File

@ -7,7 +7,7 @@
"features": {
"ghcr.io/devcontainers/features/python:1": {
"installTools": true,
"version": "3.11"
"version": "latest"
},
"ghcr.io/devcontainers-contrib/features/curl-apt-get:1": {},
"ghcr.io/jungaretti/features/ripgrep:1": {},

View File

@ -11,25 +11,25 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v2
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
uses: docker/build-push-action@v2
with:
tags: enigmabbs/enigma-bbs:latest
file: docker/Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7
platforms: linux/amd64,linux/arm64
push: true

2
.gitignore vendored
View File

@ -11,5 +11,3 @@ mail/
node_modules/
docs/_site/
docs/.sass-cache/
docs/.jekyll-cache/

17
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,17 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/main.js"
}
]
}

View File

@ -35,10 +35,6 @@ npm install # or simply 'yarn'
## 0.0.13-beta to 0.0.14-beta
* Due to changes to supported algorithms in newer versions of openssl, the default list of supported algorithms for the ssh login server has changed. There are both removed ciphers as well as optional new kex algorithms available now. ***NOTE:*** Changes to supported algorithms are only needed to support keys generated with new versions of openssl, if you already have a ssl key in use you should not have to make any changes to your config.
* Removed ciphers: 'blowfish-cbc', 'arcfour256', 'arcfour128', and 'cast128-cbc'
* Added kex: 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'ecdh-sha2-nistp256', 'ecdh-sha2-nistp384', 'ecdh-sha2-nistp521'
## 0.0.12-beta to 0.0.13-beta
* To enable the new Waiting for Caller (WFC) support, please see [WFC](docs/modding/wfc.md).
* :exclamation: The SSH server's `ssh2` module has gone through a major upgrade. Existing users will need to comment out two SSH KEX algorithms from their `config.hjson` if present else clients such as NetRunner will not be able to connect over SSH. Comment out `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1`

View File

@ -9,8 +9,6 @@ This document attempts to track **major** changes and additions in ENiGMA½. For
* Finally, the system will search for `index.html` and `index.htm` in that order, if another suitable route cannot be established.
* CombatNet has shut down, so the module (`combatnet.js`) has been removed.
* The Menu Flag `popParent` has been removed and `noHistory` has been updated to work as expected. In general things should "Just Work", but check your `menu.hjson` entries if you see menu stack issues.
* Various New User Application (NUA) properties are now optional. If you would like to reduce the information users are required, remove optional fields from NUA artwork and collect less. These properties will be stored as "" (empty). Optional properties are as follows: Real name, Birth date, Sex, Location, Affiliations (Affils), Email, and Web address.
* Art handling has been changed to respect the art width contained in SAUCE when present in the case where the terminal width is greater than the art width. This fixes art files that assume wrapping at 80 columns on wide (mostly new utf8) terminals.
## 0.0.13-beta
* **Note for contributors**: ENiGMA has switched to [Prettier](https://prettier.io) for formatting/style. Please see [CONTRIBUTING](CONTRIBUTING.md) and the Prettier website for more information.

View File

@ -187,7 +187,7 @@
}
mci: {
VM1: {
height: 14
height: 15,
width: 50
itemFormat: "|00|11{userName:<17.17}|03{affils:<21.21}|11{location:<19.19}|03{lastLoginTs}"
focusItemFormat: "|00|19|15{userName:<17.17}{affils:<21.21}{location:<19.19}{lastLoginTs}"
@ -343,7 +343,7 @@
}
mci: {
VM1: {
height: 13
height: 14
width: 70
itemFormat: "|00|15{msgNum:>4} |03{subject:<28.27} |11{fromUserName:<20.20} |03{ts:<15.16} |15{newIndicator}"
focusItemFormat: "|00|19|15{msgNum:>4} {subject:<28.27} {fromUserName:<20.20} {ts:<15.16} {newIndicator}"
@ -416,7 +416,7 @@
}
mci: {
VM1: {
height: 12
height: 14
width: 70
itemFormat: "|00|15{msgNum:>4} |03{subject:<28.27} |11{fromUserName:<20.20} |03{ts} |15{newIndicator}"
focusItemFormat: "|00|19|15{msgNum:>4} {subject:<28.27} {fromUserName:<20.20} {ts} {newIndicator}"
@ -618,7 +618,7 @@
}
mci: {
VM1: {
height: 12
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}"
@ -783,7 +783,7 @@
}
mci: {
VM1: {
height: 12
height: 14
width: 70
itemFormat: "|00|15 {msgNum:<5.5}|03{subject:<28.27} |15{fromUserName:<20.20} {ts}"
focusItemFormat: "|00|19> |15{msgNum:<5.5}{subject:<28.27} {fromUserName:<20.20} {ts}"
@ -1214,7 +1214,7 @@
2: {
mci: {
MT1: {
height: 13
height: 14
width: 45
}

View File

@ -505,9 +505,9 @@ class Achievements {
getFormatObject(info) {
return {
userName: info.user.username,
userRealName: info.user.realName(false) || 'N/A',
userLocation: info.user.properties[UserProps.Location] || 'N/A',
userAffils: info.user.properties[UserProps.Affiliations] || 'N/A',
userRealName: info.user.properties[UserProps.RealName],
userLocation: info.user.properties[UserProps.Location],
userAffils: info.user.properties[UserProps.Affiliations],
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]*?)([ABCDEFGfHJKLmMsSTuUYZt@PXhlnpt]))|([78DEHM]))/g, // eslint-disable-line no-control-regex
re: /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex
};
options = miscUtil.valueWithDefault(options, {
@ -37,12 +37,6 @@ 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);
@ -77,25 +71,10 @@ function ANSIEscapeParser(options) {
self.clearScreen = function () {
self.column = 1;
self.row = 1;
self.positionUpdated();
self.emit('clear screen');
};
self.positionUpdated = function () {
if(self.row > self.termHeight) {
if(this.savedPosition) {
this.savedPosition.row -= self.row - self.termHeight;
}
self.emit('scroll', self.row - self.termHeight);
self.row = self.termHeight;
}
else if(self.row < 1) {
if(this.savedPosition) {
this.savedPosition.row -= self.row - 1;
}
self.emit('scroll', -(self.row - 1));
self.row = 1;
}
self.emit('position update', self.row, self.column);
};
@ -111,8 +90,8 @@ function ANSIEscapeParser(options) {
switch (charCode) {
case CR:
self.emit('literal', text.slice(start, pos + 1));
start = pos + 1;
self.emit('literal', text.slice(start, pos));
start = pos;
self.column = 1;
@ -126,8 +105,8 @@ function ANSIEscapeParser(options) {
self.column = 1;
}
self.emit('literal', text.slice(start, pos + 1));
start = pos + 1;
self.emit('literal', text.slice(start, pos));
start = pos;
self.row += 1;
@ -135,16 +114,13 @@ function ANSIEscapeParser(options) {
break;
default:
if (self.column === self.breakWidth) {
if (self.column === self.termWidth) {
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;
@ -159,7 +135,7 @@ function ANSIEscapeParser(options) {
//
// Finalize this chunk
//
if (self.column > self.breakWidth) {
if (self.column > self.termWidth) {
self.column = 1;
self.row += 1;
@ -246,7 +222,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]*?)([ABCDEFGfHJKLmMsSTuUYZt@PXhlnpt]))|([78DEHM]))/g, // eslint-disable-line no-control-regex
re: /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex
stop: false,
};
};
@ -286,47 +262,9 @@ function ANSIEscapeParser(options) {
opCode = match[2];
args = match[1].split(';').map(v => parseInt(v, 10)); // convert to array of ints
// Handle the case where there is no bracket
if(!(_.isNil(match[3]))) {
opCode = match[3];
args = [];
// no bracket
switch(opCode) {
// save cursor position
case '7':
escape('s', args);
break;
// restore cursor position
case '8':
escape('u', args);
break;
// scroll up
case 'D':
escape('S', args);
break;
// move to next line
case 'E':
// functonality is the same as ESC [ E
escape(opCode, args);
break;
// create a tab at current cursor position
case 'H':
literal('\t');
break;
// scroll down
case 'M':
escape('T', args);
break;
}
}
else {
escape(opCode, args);
}
//self.emit('chunk', match[0]);
self.emit('control', match[0], opCode, args);
}
} while (0 !== re.lastIndex);
@ -334,8 +272,8 @@ function ANSIEscapeParser(options) {
if (pos < buffer.length) {
var lastBit = buffer.slice(pos);
// handles either \r\n or \n
if ('\n' === lastBit.slice(-1).toString()) {
// :TODO: check for various ending LF's, not just DOS \r\n
if ('\r\n' === lastBit.slice(-2).toString()) {
switch (self.trailingLF) {
case 'default':
//
@ -343,14 +281,14 @@ function ANSIEscapeParser(options) {
// if we're going to end on termHeight
//
if (this.termHeight === self.row) {
lastBit = lastBit.slice(0, -1);
lastBit = lastBit.slice(0, -2);
}
break;
case 'omit':
case 'no':
case false:
lastBit = lastBit.slice(0, -1);
lastBit = lastBit.slice(0, -2);
break;
}
}
@ -361,6 +299,48 @@ function ANSIEscapeParser(options) {
self.emit('complete');
};
/*
self.parse = function(buffer, savedRe) {
// :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc.
// :TODO: move this to "constants" section @ top
var re = /(?:\x1b\x5b)([\?=;0-9]*?)([ABCDHJKfhlmnpsu])/g;
var pos = 0;
var match;
var opCode;
var args;
// ignore anything past EOF marker, if any
buffer = buffer.split(String.fromCharCode(0x1a), 1)[0];
do {
pos = re.lastIndex;
match = re.exec(buffer);
if(null !== match) {
if(match.index > pos) {
parseMCI(buffer.slice(pos, match.index));
}
opCode = match[2];
args = getArgArray(match[1].split(';'));
escape(opCode, args);
self.emit('chunk', match[0]);
}
} while(0 !== re.lastIndex);
if(pos < buffer.length) {
parseMCI(buffer.slice(pos));
}
self.emit('complete');
};
*/
function escape(opCode, args) {
let arg;
@ -393,37 +373,6 @@ function ANSIEscapeParser(options) {
self.moveCursor(-arg, 0);
break;
// line feed
case 'E':
arg = isNaN(args[0]) ? 1 : args[0];
if(this.row + arg > this.termHeight) {
this.emit('scroll', arg - (this.termHeight - this.row));
self.moveCursor(0, this.termHeight);
}
else {
self.moveCursor(0, arg);
}
break;
// reverse line feed
case 'F':
arg = isNaN(args[0]) ? 1 : args[0];
if(this.row - arg < 1) {
this.emit('scroll', -(arg - this.row));
self.moveCursor(0, 1 - this.row);
}
else {
self.moveCursor(0, -arg);
}
break;
// absolute horizontal cursor position
case 'G':
arg = isNaN(args[0]) ? 1 : args[0];
self.column = Math.max(1, arg);
self.positionUpdated();
break;
case 'f': // horiz & vertical
case 'H': // cursor position
//self.row = args[0] || 1;
@ -434,37 +383,14 @@ function ANSIEscapeParser(options) {
self.positionUpdated();
break;
// erase display/screen
case 'J':
if(isNaN(args[0]) || 0 === args[0]) {
self.emit('erase rows', self.row, self.termHeight);
}
else if (1 === args[0]) {
self.emit('erase rows', 1, self.row);
}
else if (2 === args[0]) {
self.clearScreen();
}
// save position
case 's':
self.saveCursorPosition();
break;
// erase text in line
case 'K':
if(isNaN(args[0]) || 0 === args[0]) {
self.emit('erase columns', self.row, self.column, self.termWidth);
}
else if (1 === args[0]) {
self.emit('erase columns', self.row, 1, self.column);
}
else if (2 === args[0]) {
self.emit('erase columns', self.row, 1, self.termWidth);
}
break;
// insert line
case 'L':
arg = isNaN(args[0]) ? 1 : args[0];
self.emit('insert line', self.row, arg);
// restore position
case 'u':
self.restoreCursorPosition();
break;
// set graphic rendition
@ -536,52 +462,15 @@ function ANSIEscapeParser(options) {
self.emit('sgr update', self.graphicRendition);
break; // m
// save position
case 's':
self.saveCursorPosition();
break;
// :TODO: s, u, K
// Scroll up
case 'S':
arg = isNaN(args[0]) ? 1 : args[0];
self.emit('scroll', arg);
break;
// Scroll down
case 'T':
arg = isNaN(args[0]) ? 1 : args[0];
self.emit('scroll', -arg);
break;
// restore position
case 'u':
self.restoreCursorPosition();
break;
// clear
case 'U':
// erase display/screen
case 'J':
// :TODO: Handle other 'J' types!
if (2 === args[0]) {
self.clearScreen();
}
break;
// delete line
// TODO: how should we handle 'M'?
case 'Y':
arg = isNaN(args[0]) ? 1 : args[0];
self.emit('delete line', self.row, arg);
break;
// back tab
case 'Z':
// calculate previous tabstop
self.column = Math.max( 1, self.column - (self.column % 8 || 8) );
self.positionUpdated();
break;
case '@':
// insert column(s)
arg = isNaN(args[0]) ? 1 : args[0];
self.emit('insert columns', self.row, self.column, arg);
break;
}
}
}

View File

@ -208,17 +208,17 @@ module.exports = class ArchiveUtil {
// pty.js doesn't currently give us a error when things fail,
// so we have this horrible, horrible hack:
let err;
proc.onData(d => {
proc.once('data', d => {
if (_.isString(d) && d.startsWith('execvp(3) failed.')) {
err = Errors.ExternalProcess(`${action} failed: ${d.trim()}`);
}
});
proc.onExit(exitEvent => {
proc.once('exit', exitCode => {
return cb(
exitEvent.exitCode
exitCode
? Errors.ExternalProcess(
`${action} failed with exit code: ${exitEvent.exitCode}`
`${action} failed with exit code: ${exitCode}`
)
: err
);
@ -358,10 +358,10 @@ module.exports = class ArchiveUtil {
output += data;
});
proc.onExit(exitEvent => {
if (exitEvent.exitCode) {
proc.once('exit', exitCode => {
if (exitCode) {
return cb(
Errors.ExternalProcess(`List failed with exit code: ${exitEvent.exitCode}`)
Errors.ExternalProcess(`List failed with exit code: ${exitCode}`)
);
}

View File

@ -41,21 +41,11 @@ const SUPPORTED_ART_TYPES = {
};
function getFontNameFromSAUCE(sauce) {
if (sauce && sauce.Character) {
if (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)
@ -284,7 +274,6 @@ 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,
});
@ -316,75 +305,6 @@ function display(client, art, options, cb) {
}
});
// Remove any MCI's that are in erased rows
ansiParser.on('erase row', (startRow, endRow) => {
_.forEach(mciMap, (mciInfo, mapKey) => {
if (mciInfo.position[0] >= startRow && mciInfo.position[0] <= endRow) {
delete mciMap[mapKey];
}
});
});
// Remove any MCI's that are in erased columns
ansiParser.on('erase columns', (row, startCol, endCol) => {
_.forEach(mciMap, (mciInfo, mapKey) => {
if (
mciInfo.position[0] === row &&
mciInfo.position[1] >= startCol &&
mciInfo.position[1] <= endCol
) {
delete mciMap[mapKey];
}
});
});
ansiParser.on('insert columns', (row, startCol, numCols) => {
_.forEach(mciMap, (mciInfo, mapKey) => {
if (mciInfo.position[0] === row && mciInfo.position[1] >= startCol) {
mciInfo.position[1] += numCols;
if(mciInfo.position[1] > client.term.termWidth) {
delete mciMap[mapKey];
}
}
});
});
// Clear the screen, removing any MCI's
ansiParser.on('clear screen', () => {
_.forEach(mciMap, (mciInfo, mapKey) => {
delete mciMap[mapKey];
});
});
ansiParser.on('scroll', (scrollY) => {
_.forEach(mciMap, (mciInfo) => {
mciInfo.position[0] -= scrollY;
});
});
ansiParser.on('insert line', (row, numLines) => {
_.forEach(mciMap, (mciInfo) => {
if (mciInfo.position[0] >= row) {
mciInfo.position[0] += numLines;
}
});
});
ansiParser.on('delete line', (row, numLines) => {
_.forEach(mciMap, (mciInfo, mapKey) => {
if (mciInfo.position[0] >= row) {
if(mciInfo.position[0] < row + numLines) {
// unlike scrolling, the rows are actually gone,
// so we need to delete any MCI's that are in them
delete mciMap[mapKey];
}
else {
mciInfo.position[0] -= numLines;
}
}
});
});
ansiParser.on('literal', literal => client.term.write(literal, false));
ansiParser.on('control', control => client.term.rawWrite(control));

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?.realName(false) || 'N/A';
entry.realName = ac.user?.getProperty(UserProps.RealName) || '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,15 +188,22 @@ module.exports = () => {
//
// 1 - Generate a Private Key (PK):
// Currently ENiGMA 1/2 requires a PKCS#1 PEM formatted PK.
// For information on generating a key, see:
// https://nuskooler.github.io/enigma-bbs/servers/loginservers/ssh.html#generate-a-ssh-private-key
// 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
//
// 2 - Set 'privateKeyPass' to the password you used in step #1
//
// 3 - Finally, set 'enabled' to 'true'
//
// Additional reading:
// - https://nuskooler.github.io/enigma-bbs/servers/loginservers/ssh.html
// - https://blog.sleeplessbeastie.eu/2017/12/28/how-to-generate-private-key/
// - https://gist.github.com/briansmith/2ee42439923d8e65a266994d0f70180b
//
privateKeyPem: paths.join(
__dirname,
@ -215,18 +222,14 @@ 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',
'curve25519-sha256',
'curve25519-sha256@libssh.org',
'ecdh-sha2-nistp256',
'ecdh-sha2-nistp384',
'ecdh-sha2-nistp521',
// Group exchange not currnetly supported
// 'diffie-hellman-group-exchange-sha256',
// 'diffie-hellman-group-exchange-sha1',
],
cipher: [
'aes128-ctr',
@ -239,7 +242,12 @@ module.exports = () => {
'aes256-cbc',
'aes192-cbc',
'aes128-cbc',
'blowfish-cbc',
'3des-cbc',
'arcfour256',
'arcfour128',
'cast128-cbc',
'arcfour',
],
hmac: [
'hmac-sha2-256',

View File

@ -57,10 +57,8 @@ module.exports = class Door {
run(exeInfo, cb) {
this.encoding = (exeInfo.encoding || 'cp437').toLowerCase();
if ('socket' === this.io) {
if(!this.sockServer) {
if ('socket' === this.io && !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!`));
}
@ -115,10 +113,9 @@ module.exports = class Door {
spawnOptions
);
prePty.onExit(exitEvent => {
const {exitCode, signal} = exitEvent;
prePty.once('exit', exitCode => {
this.client.log.info(
{ exitCode, signal },
{ exitCode: exitCode },
'Door pre-command exited'
);
return callback(null);
@ -168,7 +165,7 @@ module.exports = class Door {
this.doorPty.onData(this.doorDataHandler.bind(this));
this.doorPty.onExit( (/*exitEvent*/) => {
this.doorPty.once('close', () => {
return this.restoreIo(this.doorPty);
});
} else if ('socket' === this.io) {
@ -181,9 +178,8 @@ module.exports = class Door {
);
}
this.doorPty.onExit(exitEvent => {
const {exitCode, signal} = exitEvent;
this.client.log.info({ exitCode, signal }, 'Door exited');
this.doorPty.once('exit', exitCode => {
this.client.log.info({ exitCode: exitCode }, 'Door exited');
if (this.sockServer) {
this.sockServer.close();

View File

@ -167,17 +167,17 @@ class ScheduledEvent {
return cb(e);
}
proc.onExit(exitEvent => {
if (exitEvent.exitCode) {
proc.once('exit', exitCode => {
if (exitCode) {
Log.warn(
{ eventName: this.name, action: this.action, exitCode: exitEvent.exitCode },
{ eventName: this.name, action: this.action, exitCode: exitCode },
'Bad exit code while performing scheduled event action'
);
}
return cb(
exitEvent.exitCode
exitCode
? Errors.ExternalProcess(
`Bad exit code while performing scheduled event action: ${exitEvent.exitCode}`
`Bad exit code while performing scheduled event action: ${exitCode}`
)
: null
);

View File

@ -485,10 +485,13 @@ exports.getModule = class TransferFileModule extends MenuModule {
}
});
externalProc.onExit(exitEvent => {
const {exitCode, signal} = exitEvent;
externalProc.once('close', () => {
return this.restorePipeAfterExternalProc();
});
externalProc.once('exit', exitCode => {
this.client.log.debug(
{ cmd: cmd, args: args, exitCode, signal },
{ cmd: cmd, args: args, exitCode: exitCode },
'Process exited'
);

View File

@ -167,7 +167,6 @@ exports.FullScreenEditorModule =
var newFocusViewId;
if (errMsgView) {
if (err) {
errMsgView.clearText();
errMsgView.setText(err.message);
if (MciViewIds.header.subject === err.view.getId()) {
@ -184,13 +183,6 @@ 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';
@ -990,7 +982,11 @@ exports.FullScreenEditorModule =
const area = getMessageAreaByTag(self.messageAreaTag);
if (fromView !== undefined) {
if (area && area.realNames) {
fromView.setText(self.client.user.realName());
fromView.setText(
self.client.user.properties[
UserProps.RealName
] || self.client.user.username
);
} else {
fromView.setText(self.client.user.username);
}
@ -1058,7 +1054,7 @@ exports.FullScreenEditorModule =
posView.setText(
_.padStart(String(pos.row + 1), 2, '0') +
',' +
_.padStart(String(pos.col + 1), 2, '0')
_.padEnd(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 && m[2] && m[3]) {
if (m) {
// start with a 2D
let addr = {
net: parseInt(m[2]),

View File

@ -763,11 +763,6 @@ 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,39 +14,6 @@ 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);
@ -75,15 +42,8 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
],
function complete(err) {
if (err) {
const errMsgView = self.viewControllers.header.getView(
MciViewIds.header.errorMsg
);
if (errMsgView) {
errMsgView.setText(err.message);
}
return cb(err);
}
// :TODO:... sooooo now what?
} else {
// note: not logging 'from' here as it's part of client.log.xxxx()
self.client.log.info(
{
@ -93,6 +53,7 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
},
`User "${self.client.user.username}" posted message to "${msg.toUserName}" (${msg.areaTag})`
);
}
return self.nextMenu(cb);
}

View File

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

View File

@ -13,7 +13,6 @@ const UserProps = require('./user_property.js');
// deps
const _ = require('lodash');
const moment = require('moment');
exports.moduleInfo = {
name: 'NUA',
@ -96,15 +95,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 || moment()
formData.value.birthdate
),
[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,10 +21,8 @@ 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 : emptyFieldError);
return cb(data && data.length > 0 ? null : new Error('Field cannot be empty'));
}
function validateMessageSubject(data, cb) {
@ -93,11 +91,7 @@ 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 (addressedToInfo.name.length === 0) {
return cb(emptyFieldError());
}
if (Message.AddressFlavor.Local !== addressedToInfo.flavor) {
if (Message.AddressFlavor.FTN === addressedToInfo.flavor) {
return cb(null);
}

View File

@ -179,10 +179,6 @@ 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,29 +124,12 @@ 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.realName(true) : this.username;
const name =
'real' === type ? this.getProperty(UserProps.RealName) : 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,7 +93,9 @@ module.exports = class User2FA_OTPWebRegister {
}
const message = {
to: user.emailAddress(),
to: `${
user.getProperty(UserProps.RealName) || user.username
} <${user.getProperty(UserProps.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 || moment()
formData.value.birthdate
),
[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,7 +233,11 @@ exports.getModule = class UserConfigModule extends MenuModule {
function populateViews(callback) {
const user = self.client.user;
self.setViewText('menu', MciCodeIds.RealName, user.realName(false) || '');
self.setViewText(
'menu',
MciCodeIds.RealName,
user.properties[UserProps.RealName]
);
self.setViewText(
'menu',
MciCodeIds.BirthDate,

View File

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

View File

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

View File

@ -1,25 +1,19 @@
FROM --platform=${BUILDPLATFORM:-linux/amd64} node:20-bookworm-slim
ARG TARGETPLATFORM
ARG BUILDPLATFORM
ARG TARGETOS
ARG TARGETBRANCH
FROM node:18-buster-slim
LABEL maintainer="dave@force9.org"
ENV NVM_DIR /root/.nvm
ENV DEBIAN_FRONTEND noninteractive
COPY . /enigma-bbs
# Just copy the package.json so it only needs to build once
COPY package.json /enigma-bbs/
# Install APT and NPM packages
# Do some installing! (and alot of cleaning up) keeping it in one step for less docker layers
# - if you need to debug i recommend to break the steps with individual RUNs)
RUN apt-get update \
&& apt-get install -y \
git \
curl \
build-essential \
python \
python3 \
libssl-dev \
lrzsz \
@ -28,21 +22,9 @@ RUN apt-get update \
unrar-free \
p7zip-full \
dos2unix \
&& npm set progress=false && npm config set depth 0 \
&& npm install -g npm@latest \
&& npm install -g pm2 \
&& cd /enigma-bbs && npm install
# Do this after npm install to avoid cache-miss on every code change
COPY . /enigma-bbs
# Then run post source copy steps that have to happen every time
RUN dos2unix /enigma-bbs/docker/bin/docker-entrypoint.sh \
&& apt-get remove dos2unix -y \
&& chmod +x /enigma-bbs/docker/bin/docker-entrypoint.sh \
&& cp -f /enigma-bbs/docker/bin/sexyz /usr/local/bin \
&& cd /enigma-bbs \
&& cd /enigma-bbs && npm install \
&& pm2 start main.js \
&& mkdir -p /enigma-bbs-pre/art \
&& mkdir /enigma-bbs-pre/mods \
@ -50,11 +32,16 @@ RUN dos2unix /enigma-bbs/docker/bin/docker-entrypoint.sh \
&& cp -rp art/* ../enigma-bbs-pre/art/ \
&& cp -rp mods/* ../enigma-bbs-pre/mods/ \
&& cp -rp config/* ../enigma-bbs-pre/config/ \
&& apt-get remove build-essential python3 libssl-dev git curl -y \
&& apt-get remove build-essential python python3 libssl-dev git curl -y \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
&& apt-get clean
# sexyz
COPY docker/bin/sexyz /usr/local/bin
RUN dos2unix /enigma-bbs/docker/bin/docker-entrypoint.sh && apt-get remove dos2unix -y
RUN chmod +x /enigma-bbs/docker/bin/docker-entrypoint.sh
# enigma storage mounts
VOLUME /enigma-bbs/art
VOLUME /enigma-bbs/config

View File

@ -6,8 +6,8 @@ GEM
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
addressable (2.8.5)
public_suffix (>= 2.0.2, < 6.0)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
colorator (1.1.0)
concurrent-ruby (1.2.2)
cssminify2 (2.0.1)
@ -19,14 +19,14 @@ GEM
ffi (1.15.5)
forwardable-extended (2.6.0)
gemoji (3.0.1)
html-pipeline (2.14.3)
html-pipeline (2.14.0)
activesupport (>= 2)
nokogiri (>= 1.4)
htmlcompressor (0.4.0)
http_parser.rb (0.8.0)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
jekyll (4.2.2)
jekyll (4.2.1)
addressable (~> 2.4)
colorator (~> 1.0)
em-websocket (~> 0.5)
@ -49,7 +49,7 @@ GEM
uglifier (~> 4.1)
jekyll-relative-links (0.6.1)
jekyll (>= 3.3, < 5.0)
jekyll-sass-converter (2.2.0)
jekyll-sass-converter (2.1.0)
sassc (> 2.0.1, < 3.0)
jekyll-seo-tag (2.7.1)
jekyll (>= 3.8, < 5.0)
@ -64,32 +64,30 @@ GEM
gemoji (~> 3.0)
html-pipeline (~> 2.2)
jekyll (>= 3.0, < 5.0)
json (2.6.3)
json (2.6.1)
json-minify (0.0.3)
json (> 0)
kramdown (2.4.0)
kramdown (2.3.1)
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
liquid (4.0.4)
listen (3.8.0)
liquid (4.0.3)
listen (3.7.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
mercenary (0.4.0)
minitest (5.19.0)
nokogiri (1.15.4-aarch64-linux)
racc (~> 1.4)
nokogiri (1.15.4-x86_64-linux)
nokogiri (1.14.3-x86_64-linux)
racc (~> 1.4)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
public_suffix (5.0.3)
racc (1.7.1)
rb-fsevent (0.11.2)
public_suffix (4.0.6)
racc (1.6.2)
rb-fsevent (0.11.0)
rb-inotify (0.10.1)
ffi (~> 1.0)
rexml (3.2.6)
rouge (3.30.0)
rexml (3.2.5)
rouge (3.28.0)
safe_yaml (1.0.5)
sassc (2.4.0)
ffi (~> 1.9)
@ -100,10 +98,8 @@ GEM
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
unicode-display_width (1.8.0)
webrick (1.8.1)
PLATFORMS
aarch64-linux
x86_64-linux
DEPENDENCIES
@ -115,7 +111,6 @@ DEPENDENCIES
jekyll-theme-hacker (~> 0.2.0)
jemoji (~> 0.12.0)
tzinfo-data
webrick
BUNDLED WITH
2.4.19
2.3.5

View File

@ -131,5 +131,4 @@ collections:
- admin/oputil.md
- admin/updating.md
- troubleshooting/monitoring-logs.md
- troubleshooting/ssh-troubleshooting.md

View File

@ -3,7 +3,7 @@ layout: page
title: TIC Support
---
## TIC Support
ENiGMA½ supports FidoNet-Style TIC file attachments by mapping external TIC area tags to local file areas.
ENiGMA½ supports FidoNet-Style TIC file attachments by mapping TIC areas to local file areas.
Under a given node defined in the `ftn_bso` config section in `config.hjson` (see
[BSO Import/Export](../messageareas/bso-import-export.md)), TIC configuration may be supplied:
@ -17,9 +17,9 @@ Under a given node defined in the `ftn_bso` config section in `config.hjson` (se
packetPassword: mypass
encoding: cp437
archiveType: zip
tic: { // <--- General TIC config for 46:*
tic: {
password: TESTY-TEST
uploadBy: AgoraNet TIC
uploadBy: Agoranet TIC
allowReplace: true
}
}
@ -29,15 +29,7 @@ Under a given node defined in the `ftn_bso` config section in `config.hjson` (se
}
```
Valid `tic` members:
| Item | Required | Description |
|--------|---------------|------------------|
| `password` | :-1: | TIC packet password, if required |
| `uploadedBy` | :-1: | Sets the "uploaded by" field for TIC attachments, for example "AgoraNet TIC" |
| `allowReplace` | :-1: | Set to `true` to allow TIC attachments to replace each other. This is especially handy for things like weekly node list attachments |
Next, we need to configure the mapping between TIC areas you want to carry, and the file base area (and, optionally, specific storage tag) for them to be tossed to. You can also add hashtags to the tossed files to assist users in searching for files:
You then need to configure the mapping between TIC areas you want to carry, and the file base area and storage tag for them to be tossed to. Optionally you can also add hashtags to the tossed files to assist users in searching for files:
```hjson
ticAreas: {
@ -49,22 +41,10 @@ ticAreas: {
}
```
> :information_source: Note that in the example above `agn_node` represents the **external** network area tag, usually represented in all caps. In this case, `AGN_NODE`.
Valid `ticAreas` members under a given node mapping are as follows:
| Item | Required | Description |
|--------|---------------|------------------|
| `areaTag` | :+1: | Specifies the local areaTag in which to place TIC attachments |
| `storageTag` | :-1: | Optionally, set a specific storageTag. If not set, the default for this area will be used. |
| `hashTags` | :-1: | One or more optional hash tags to assign TIC attachments in this area. |
💡 Multiple TIC areas can be mapped to a single file base area.
Multiple TIC areas can be mapped to a single file base area.
### Example Configuration
Example configuration fragments mapping file base areas, FTN BSO node configuration and TIC area configuration.
An example configuration linking file base areas, FTN BSO node configuration and TIC area configuration.
```hjson
fileBase: {
@ -99,9 +79,11 @@ scannerTossers: {
}
}
}
}
}
ticAreas: {
// here we map AgoraNet AGN_NODE -> local msgNetworks file area
ticAreas: {
agn_node: {
areaTag: msgNetworks
storageTag: msg_network
@ -112,11 +94,7 @@ scannerTossers: {
storageTag: msg_network
hashTags: agoranet,infopack
}
}
}
}
```
## See Also

View File

@ -3,13 +3,9 @@ layout: page
title: SSH Server
---
## SSH Login Server
The ENiGMA½ SSH *login server* allows secure user logins over SSH (ssh://).
*Note:* If you run into any troubles during SSH setup, please see [Troubleshooting SSH](../../troubleshooting/ssh-troubleshooting.md)
## Configuration
Entries available under `config.loginServers.ssh`:
| Item | Required | Description |
@ -24,8 +20,10 @@ Entries available under `config.loginServers.ssh`:
| `algorithms` | :-1: | Configuration block for SSH algorithms. Includes keys of `kex`, `cipher`, `hmac`, and `compress`. See the algorithms section in the [ssh2-streams](https://github.com/mscdex/ssh2-streams#ssh2stream-methods) documentation for details. For defaults set by ENiGMA½, see `core/config_default.js`.
| `traceConnections` | :-1: | Set to `true` to enable full trace-level information on SSH connections.
* *IMPORTANT* With the `privateKeyPass` option set, make sure that you verify that the config file is not readable by other users!
### Example Configuration
```hjson
@ -42,93 +40,42 @@ Entries available under `config.loginServers.ssh`:
```
## Generate a SSH Private Key
To utilize the SSH server, an SSH Private Key (PK) will need generated. OpenSSH or (with some versions) OpenSSL can be used for this task:
### OpenSSH (Preferred)
### OpenSSH
#### OpenSSH Install - Linux / Mac
If it is not already available, install OpenSSH using the package manager of your choice (should be pre-installed on most distributions.)
#### Running OpenSSH - Linux / Mac
From the root directory of the Enigma BBS, run the following:
```shell
mkdir -p config/security
ssh-keygen -t rsa -m PEM -h -f config/security/ssh_private_key.pem
```bash
ssh-keygen -m PEM -h -f config/ssh_private_key.pem
```
#### Windows Install - OpenSSH
OpenSSH may already be installed, try running `ssh-keygen.exe`. If not, see this page: [Install OpenSSH for Windows](https://learn.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse?tabs=gui)
#### Running OpenSSH - Windows
After installation, go to the root directory of your enigma project and run:
```powershell
mkdir .\config\security -ErrorAction SilentlyContinue
ssh-keygen.exe -t rsa -m PEM -h -f .\config\security\ssh_private_key.pem
```
#### ssh-keygen options
Option descriptions:
| Option | Description |
|------|-------------|
| `-t rsa` | Use the RSA algorithm needed for the `ssh2` library |
| `-m PEM` | Set the output format to `PEM`, compatible with the `ssh2` library |
| `-h` | Generate a host key |
| `-f config/ssh_private_key.pem` | Filename for the private key. Used in the `privateKeyPem` option in the configuration |
When you execute the `ssh-keygen` command it will ask for a passphrase (and a confirmation.) This should then be used as the value for `privateKeyPass` in the configuration.
### OpenSSL
#### Open SSL Install - Linux / Mac
If you do not have OpenSSH installed or if you have trouble with the above OpenSSH commands, using some versions for OpenSSL (before version 3) the following commands may work as well:
If not already installed, install via the `openssl` package on most package managers.
#### Open SSL Install - Windows
```powershell
winget install -e --id ShiningLight.OpenSSL
```bash
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -pkeyopt rsa_keygen_pubexp:65537 | openssl rsa -out ./config/ssh_private_key.pem -aes128
```
#### Running OpenSSL
Or for even older OpenSSL versions:
*Note:* Using `ssh-keygen` from OpenSSL is recommended where possible. If you have trouble with the above OpenSSH commands, using some versions for OpenSSL (before version 3) the following commands may work as well:
#### Running OpenSSL - Linux / Mac
Run the following from the root directory of Enigma
```shell
mkdir -p config/security
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -pkeyopt rsa_keygen_pubexp:65537 | openssl rsa -out ./config/security/ssh_private_key.pem -aes128
```
#### Running OpenSSL - Windows
Run the following from the root directory of Enigma (note: you may need to specify the full path to openssl.exe if it isn't in your system path, on my system it was `C:\Program Files\OpenSSL-Win64\bin\openssl.exe`):
```powershell
mkdir .\config\security -ErrorAction SilentlyContinue
openssl.exe genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -pkeyopt rsa_keygen_pubexp:65537 | openssl.exe rsa -out ./config/security/ssh_private_key.pem -aes128
```
#### Running Older OpenSSL
For older OpenSSL versions, the following command has been known to work:
```shell
```bash
openssl genrsa -aes128 -out ./config/ssh_private_key.pem 2048
```
*Note:* that you may need `-3des` for very old implementations or SSH clients!
Note that you may need `-3des` for very old implementations or SSH clients!
## Prompt

View File

@ -1,45 +0,0 @@
---
layout: page
title: Troubleshooting SSH
---
Stuck with errors trying to get your SSH setup configured? See below for some common problems. Or as always, reach out to us by creating an [Issue](https://github.com/NuSkooler/enigma-bbs/issues) or start a [Discussion](https://github.com/NuSkooler/enigma-bbs/discussions)
## No Such File or Directory
***Symptom:***
BBS not starting with an error similar to the following:
```shell
Error initializing: Error: ENOENT: no such file or directory, open '<path>/config/security/ssh_private_key.pem'
```
***Solution:***
Several things can cause this:
1. `ssh_private_key.pem` was installed to the wrong location. Make sure that it is in the `config/security` directory and has the name matching the error message. You can also change your `config.hjson` if you prefer to point to the location of the key file.
2. `ssh_private_key.pem` has the wrong file permissions. Verify that the file will be readable by the user that the BBS is running as. Because it is a cryptographic key however, we do recommend that access is restricted only to that user.
## Error With Netrunner
***Symptom:***
Some ssh clients connect, but Netrunner (and other older clients) get a connection failed message and the following is in the log:
```shell
"level":40,"error":"Handshake failed","code":2,"msg":"SSH connection error"
```
***Solution:***
The key was most likely not generated with the `-t rsa` option, and is using a newer algorithm that is not supported by Netrunner and similar clients. Regenerate the certificate with the `-t rsa` option.
***Symptom:***
Some ssh clients connect, but Netrunner (and other older clients) get a connection failed message and the following is in the log:
```shell
"level":40,"error":"Group exchange not implemented for server","msg":"SSH connection error"
```
***Solution:***
Remove the following encryption protocols from your `config.hjson`: `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1`

View File

@ -69,7 +69,7 @@
}
]
}
actionKeys: @reference:common.escToPrev
actionKeys: @reference: common.escToPrev
}
1: {
mci: {

View File

@ -50,7 +50,7 @@
"minimist": "^1.2.6",
"moment": "2.29.4",
"nntp-server": "3.1.0",
"node-pty": "1.0.0",
"node-pty": "0.10.1",
"nodemailer": "6.7.7",
"otplib": "11.0.1",
"qrcode-generator": "^1.4.4",
@ -59,8 +59,8 @@
"sanitize-filename": "^1.6.3",
"sqlite3": "5.1.6",
"sqlite3-trans": "1.3.0",
"ssh2": "1.14.0",
"systeminformation": "5.21.7",
"ssh2": "1.11.0",
"systeminformation": "5.12.3",
"telnet-socket": "0.2.4",
"temptmp": "^1.1.0",
"uuid": "8.3.2",

1309
yarn.lock

File diff suppressed because it is too large Load Diff