Compare commits
No commits in common. "master" and "bugfix/fix_art_length" have entirely different histories.
master
...
bugfix/fix
|
@ -7,7 +7,7 @@
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/python:1": {
|
"ghcr.io/devcontainers/features/python:1": {
|
||||||
"installTools": true,
|
"installTools": true,
|
||||||
"version": "3.11"
|
"version": "latest"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers-contrib/features/curl-apt-get:1": {},
|
"ghcr.io/devcontainers-contrib/features/curl-apt-get:1": {},
|
||||||
"ghcr.io/jungaretti/features/ripgrep:1": {},
|
"ghcr.io/jungaretti/features/ripgrep:1": {},
|
||||||
|
|
|
@ -11,14 +11,14 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
|
@ -31,5 +31,5 @@ jobs:
|
||||||
with:
|
with:
|
||||||
tags: enigmabbs/enigma-bbs:latest
|
tags: enigmabbs/enigma-bbs:latest
|
||||||
file: docker/Dockerfile
|
file: docker/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
|
@ -11,5 +11,3 @@ mail/
|
||||||
node_modules/
|
node_modules/
|
||||||
docs/_site/
|
docs/_site/
|
||||||
docs/.sass-cache/
|
docs/.sass-cache/
|
||||||
|
|
||||||
docs/.jekyll-cache/
|
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -35,10 +35,6 @@ npm install # or simply 'yarn'
|
||||||
|
|
||||||
## 0.0.13-beta to 0.0.14-beta
|
## 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
|
## 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).
|
* 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`
|
* :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`
|
||||||
|
|
|
@ -24,7 +24,7 @@ function ANSIEscapeParser(options) {
|
||||||
this.graphicRendition = {};
|
this.graphicRendition = {};
|
||||||
|
|
||||||
this.parseState = {
|
this.parseState = {
|
||||||
re: /(?:\x1b)(?:(?:\x5b([?=;0-9]*?)([ABCDEFGfHJKLmMsSTuUYZt@PXhlnpt]))|([78DEHM]))/g, // eslint-disable-line no-control-regex
|
re: /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsutEFGST])/g, // eslint-disable-line no-control-regex
|
||||||
};
|
};
|
||||||
|
|
||||||
options = miscUtil.valueWithDefault(options, {
|
options = miscUtil.valueWithDefault(options, {
|
||||||
|
@ -77,25 +77,10 @@ function ANSIEscapeParser(options) {
|
||||||
self.clearScreen = function () {
|
self.clearScreen = function () {
|
||||||
self.column = 1;
|
self.column = 1;
|
||||||
self.row = 1;
|
self.row = 1;
|
||||||
self.positionUpdated();
|
|
||||||
self.emit('clear screen');
|
self.emit('clear screen');
|
||||||
};
|
};
|
||||||
|
|
||||||
self.positionUpdated = function () {
|
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);
|
self.emit('position update', self.row, self.column);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -246,7 +231,7 @@ function ANSIEscapeParser(options) {
|
||||||
self.parseState = {
|
self.parseState = {
|
||||||
// ignore anything past EOF marker, if any
|
// ignore anything past EOF marker, if any
|
||||||
buffer: input.split(String.fromCharCode(0x1a), 1)[0],
|
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]*?)([ABCDHJKfhlmnpsutEFGST])/g, // eslint-disable-line no-control-regex
|
||||||
stop: false,
|
stop: false,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -286,47 +271,9 @@ function ANSIEscapeParser(options) {
|
||||||
opCode = match[2];
|
opCode = match[2];
|
||||||
args = match[1].split(';').map(v => parseInt(v, 10)); // convert to array of ints
|
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);
|
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);
|
self.emit('control', match[0], opCode, args);
|
||||||
}
|
}
|
||||||
} while (0 !== re.lastIndex);
|
} while (0 !== re.lastIndex);
|
||||||
|
@ -334,8 +281,8 @@ function ANSIEscapeParser(options) {
|
||||||
if (pos < buffer.length) {
|
if (pos < buffer.length) {
|
||||||
var lastBit = buffer.slice(pos);
|
var lastBit = buffer.slice(pos);
|
||||||
|
|
||||||
// handles either \r\n or \n
|
// :TODO: check for various ending LF's, not just DOS \r\n
|
||||||
if ('\n' === lastBit.slice(-1).toString()) {
|
if ('\r\n' === lastBit.slice(-2).toString()) {
|
||||||
switch (self.trailingLF) {
|
switch (self.trailingLF) {
|
||||||
case 'default':
|
case 'default':
|
||||||
//
|
//
|
||||||
|
@ -343,14 +290,14 @@ function ANSIEscapeParser(options) {
|
||||||
// if we're going to end on termHeight
|
// if we're going to end on termHeight
|
||||||
//
|
//
|
||||||
if (this.termHeight === self.row) {
|
if (this.termHeight === self.row) {
|
||||||
lastBit = lastBit.slice(0, -1);
|
lastBit = lastBit.slice(0, -2);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'omit':
|
case 'omit':
|
||||||
case 'no':
|
case 'no':
|
||||||
case false:
|
case false:
|
||||||
lastBit = lastBit.slice(0, -1);
|
lastBit = lastBit.slice(0, -2);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -361,6 +308,48 @@ function ANSIEscapeParser(options) {
|
||||||
self.emit('complete');
|
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) {
|
function escape(opCode, args) {
|
||||||
let arg;
|
let arg;
|
||||||
|
|
||||||
|
@ -393,37 +382,6 @@ function ANSIEscapeParser(options) {
|
||||||
self.moveCursor(-arg, 0);
|
self.moveCursor(-arg, 0);
|
||||||
break;
|
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 'f': // horiz & vertical
|
||||||
case 'H': // cursor position
|
case 'H': // cursor position
|
||||||
//self.row = args[0] || 1;
|
//self.row = args[0] || 1;
|
||||||
|
@ -434,37 +392,14 @@ function ANSIEscapeParser(options) {
|
||||||
self.positionUpdated();
|
self.positionUpdated();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// save position
|
||||||
// erase display/screen
|
case 's':
|
||||||
case 'J':
|
self.saveCursorPosition();
|
||||||
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();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// erase text in line
|
// restore position
|
||||||
case 'K':
|
case 'u':
|
||||||
if(isNaN(args[0]) || 0 === args[0]) {
|
self.restoreCursorPosition();
|
||||||
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);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// set graphic rendition
|
// set graphic rendition
|
||||||
|
@ -536,52 +471,15 @@ function ANSIEscapeParser(options) {
|
||||||
self.emit('sgr update', self.graphicRendition);
|
self.emit('sgr update', self.graphicRendition);
|
||||||
break; // m
|
break; // m
|
||||||
|
|
||||||
// save position
|
// :TODO: s, u, K
|
||||||
case 's':
|
|
||||||
self.saveCursorPosition();
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Scroll up
|
// erase display/screen
|
||||||
case 'S':
|
case 'J':
|
||||||
arg = isNaN(args[0]) ? 1 : args[0];
|
// :TODO: Handle other 'J' types!
|
||||||
self.emit('scroll', arg);
|
if (2 === args[0]) {
|
||||||
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':
|
|
||||||
self.clearScreen();
|
self.clearScreen();
|
||||||
|
}
|
||||||
break;
|
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;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -208,17 +208,17 @@ module.exports = class ArchiveUtil {
|
||||||
// pty.js doesn't currently give us a error when things fail,
|
// pty.js doesn't currently give us a error when things fail,
|
||||||
// so we have this horrible, horrible hack:
|
// so we have this horrible, horrible hack:
|
||||||
let err;
|
let err;
|
||||||
proc.onData(d => {
|
proc.once('data', d => {
|
||||||
if (_.isString(d) && d.startsWith('execvp(3) failed.')) {
|
if (_.isString(d) && d.startsWith('execvp(3) failed.')) {
|
||||||
err = Errors.ExternalProcess(`${action} failed: ${d.trim()}`);
|
err = Errors.ExternalProcess(`${action} failed: ${d.trim()}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
proc.onExit(exitEvent => {
|
proc.once('exit', exitCode => {
|
||||||
return cb(
|
return cb(
|
||||||
exitEvent.exitCode
|
exitCode
|
||||||
? Errors.ExternalProcess(
|
? Errors.ExternalProcess(
|
||||||
`${action} failed with exit code: ${exitEvent.exitCode}`
|
`${action} failed with exit code: ${exitCode}`
|
||||||
)
|
)
|
||||||
: err
|
: err
|
||||||
);
|
);
|
||||||
|
@ -358,10 +358,10 @@ module.exports = class ArchiveUtil {
|
||||||
output += data;
|
output += data;
|
||||||
});
|
});
|
||||||
|
|
||||||
proc.onExit(exitEvent => {
|
proc.once('exit', exitCode => {
|
||||||
if (exitEvent.exitCode) {
|
if (exitCode) {
|
||||||
return cb(
|
return cb(
|
||||||
Errors.ExternalProcess(`List failed with exit code: ${exitEvent.exitCode}`)
|
Errors.ExternalProcess(`List failed with exit code: ${exitCode}`)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
69
core/art.js
69
core/art.js
|
@ -316,75 +316,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('literal', literal => client.term.write(literal, false));
|
||||||
ansiParser.on('control', control => client.term.rawWrite(control));
|
ansiParser.on('control', control => client.term.rawWrite(control));
|
||||||
|
|
||||||
|
|
|
@ -188,15 +188,22 @@ module.exports = () => {
|
||||||
//
|
//
|
||||||
// 1 - Generate a Private Key (PK):
|
// 1 - Generate a Private Key (PK):
|
||||||
// Currently ENiGMA 1/2 requires a PKCS#1 PEM formatted PK.
|
// Currently ENiGMA 1/2 requires a PKCS#1 PEM formatted PK.
|
||||||
// For information on generating a key, see:
|
// To generate a secure PK, issue the following command:
|
||||||
// https://nuskooler.github.io/enigma-bbs/servers/loginservers/ssh.html#generate-a-ssh-private-key
|
//
|
||||||
|
// > 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
|
// 2 - Set 'privateKeyPass' to the password you used in step #1
|
||||||
//
|
//
|
||||||
// 3 - Finally, set 'enabled' to 'true'
|
// 3 - Finally, set 'enabled' to 'true'
|
||||||
//
|
//
|
||||||
// Additional reading:
|
// 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(
|
privateKeyPem: paths.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
|
@ -215,18 +222,14 @@ module.exports = () => {
|
||||||
//
|
//
|
||||||
algorithms: {
|
algorithms: {
|
||||||
kex: [
|
kex: [
|
||||||
'curve25519-sha256',
|
|
||||||
'curve25519-sha256@libssh.org',
|
|
||||||
'ecdh-sha2-nistp256',
|
'ecdh-sha2-nistp256',
|
||||||
'ecdh-sha2-nistp384',
|
'ecdh-sha2-nistp384',
|
||||||
'ecdh-sha2-nistp521',
|
'ecdh-sha2-nistp521',
|
||||||
'diffie-hellman-group14-sha1',
|
'diffie-hellman-group14-sha1',
|
||||||
'diffie-hellman-group1-sha1',
|
'diffie-hellman-group1-sha1',
|
||||||
'curve25519-sha256',
|
// Group exchange not currnetly supported
|
||||||
'curve25519-sha256@libssh.org',
|
// 'diffie-hellman-group-exchange-sha256',
|
||||||
'ecdh-sha2-nistp256',
|
// 'diffie-hellman-group-exchange-sha1',
|
||||||
'ecdh-sha2-nistp384',
|
|
||||||
'ecdh-sha2-nistp521',
|
|
||||||
],
|
],
|
||||||
cipher: [
|
cipher: [
|
||||||
'aes128-ctr',
|
'aes128-ctr',
|
||||||
|
@ -239,7 +242,12 @@ module.exports = () => {
|
||||||
'aes256-cbc',
|
'aes256-cbc',
|
||||||
'aes192-cbc',
|
'aes192-cbc',
|
||||||
'aes128-cbc',
|
'aes128-cbc',
|
||||||
|
'blowfish-cbc',
|
||||||
'3des-cbc',
|
'3des-cbc',
|
||||||
|
'arcfour256',
|
||||||
|
'arcfour128',
|
||||||
|
'cast128-cbc',
|
||||||
|
'arcfour',
|
||||||
],
|
],
|
||||||
hmac: [
|
hmac: [
|
||||||
'hmac-sha2-256',
|
'hmac-sha2-256',
|
||||||
|
|
16
core/door.js
16
core/door.js
|
@ -57,10 +57,8 @@ module.exports = class Door {
|
||||||
run(exeInfo, cb) {
|
run(exeInfo, cb) {
|
||||||
this.encoding = (exeInfo.encoding || 'cp437').toLowerCase();
|
this.encoding = (exeInfo.encoding || 'cp437').toLowerCase();
|
||||||
|
|
||||||
if ('socket' === this.io) {
|
if ('socket' === this.io && !this.sockServer) {
|
||||||
if(!this.sockServer) {
|
|
||||||
return cb(Errors.UnexpectedState('Socket server is not running'));
|
return cb(Errors.UnexpectedState('Socket server is not running'));
|
||||||
}
|
|
||||||
} else if ('stdio' !== this.io) {
|
} else if ('stdio' !== this.io) {
|
||||||
return cb(Errors.Invalid(`"${this.io}" is not a valid io type!`));
|
return cb(Errors.Invalid(`"${this.io}" is not a valid io type!`));
|
||||||
}
|
}
|
||||||
|
@ -115,10 +113,9 @@ module.exports = class Door {
|
||||||
spawnOptions
|
spawnOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
prePty.onExit(exitEvent => {
|
prePty.once('exit', exitCode => {
|
||||||
const {exitCode, signal} = exitEvent;
|
|
||||||
this.client.log.info(
|
this.client.log.info(
|
||||||
{ exitCode, signal },
|
{ exitCode: exitCode },
|
||||||
'Door pre-command exited'
|
'Door pre-command exited'
|
||||||
);
|
);
|
||||||
return callback(null);
|
return callback(null);
|
||||||
|
@ -168,7 +165,7 @@ module.exports = class Door {
|
||||||
|
|
||||||
this.doorPty.onData(this.doorDataHandler.bind(this));
|
this.doorPty.onData(this.doorDataHandler.bind(this));
|
||||||
|
|
||||||
this.doorPty.onExit( (/*exitEvent*/) => {
|
this.doorPty.once('close', () => {
|
||||||
return this.restoreIo(this.doorPty);
|
return this.restoreIo(this.doorPty);
|
||||||
});
|
});
|
||||||
} else if ('socket' === this.io) {
|
} else if ('socket' === this.io) {
|
||||||
|
@ -181,9 +178,8 @@ module.exports = class Door {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.doorPty.onExit(exitEvent => {
|
this.doorPty.once('exit', exitCode => {
|
||||||
const {exitCode, signal} = exitEvent;
|
this.client.log.info({ exitCode: exitCode }, 'Door exited');
|
||||||
this.client.log.info({ exitCode, signal }, 'Door exited');
|
|
||||||
|
|
||||||
if (this.sockServer) {
|
if (this.sockServer) {
|
||||||
this.sockServer.close();
|
this.sockServer.close();
|
||||||
|
|
|
@ -167,17 +167,17 @@ class ScheduledEvent {
|
||||||
return cb(e);
|
return cb(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
proc.onExit(exitEvent => {
|
proc.once('exit', exitCode => {
|
||||||
if (exitEvent.exitCode) {
|
if (exitCode) {
|
||||||
Log.warn(
|
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'
|
'Bad exit code while performing scheduled event action'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return cb(
|
return cb(
|
||||||
exitEvent.exitCode
|
exitCode
|
||||||
? Errors.ExternalProcess(
|
? Errors.ExternalProcess(
|
||||||
`Bad exit code while performing scheduled event action: ${exitEvent.exitCode}`
|
`Bad exit code while performing scheduled event action: ${exitCode}`
|
||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
|
@ -485,10 +485,13 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
externalProc.onExit(exitEvent => {
|
externalProc.once('close', () => {
|
||||||
const {exitCode, signal} = exitEvent;
|
return this.restorePipeAfterExternalProc();
|
||||||
|
});
|
||||||
|
|
||||||
|
externalProc.once('exit', exitCode => {
|
||||||
this.client.log.debug(
|
this.client.log.debug(
|
||||||
{ cmd: cmd, args: args, exitCode, signal },
|
{ cmd: cmd, args: args, exitCode: exitCode },
|
||||||
'Process exited'
|
'Process exited'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
10
core/fse.js
10
core/fse.js
|
@ -167,7 +167,6 @@ exports.FullScreenEditorModule =
|
||||||
var newFocusViewId;
|
var newFocusViewId;
|
||||||
if (errMsgView) {
|
if (errMsgView) {
|
||||||
if (err) {
|
if (err) {
|
||||||
errMsgView.clearText();
|
|
||||||
errMsgView.setText(err.message);
|
errMsgView.setText(err.message);
|
||||||
|
|
||||||
if (MciViewIds.header.subject === err.view.getId()) {
|
if (MciViewIds.header.subject === err.view.getId()) {
|
||||||
|
@ -184,13 +183,6 @@ exports.FullScreenEditorModule =
|
||||||
return cb(null);
|
return cb(null);
|
||||||
},
|
},
|
||||||
editModeEscPressed: function (formData, extraArgs, cb) {
|
editModeEscPressed: function (formData, extraArgs, cb) {
|
||||||
const errMsgView = self.viewControllers.header.getView(
|
|
||||||
MciViewIds.header.errorMsg
|
|
||||||
);
|
|
||||||
if (errMsgView) {
|
|
||||||
errMsgView.clearText();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.footerMode =
|
self.footerMode =
|
||||||
'editor' === self.footerMode ? 'editorMenu' : 'editor';
|
'editor' === self.footerMode ? 'editorMenu' : 'editor';
|
||||||
|
|
||||||
|
@ -1058,7 +1050,7 @@ exports.FullScreenEditorModule =
|
||||||
posView.setText(
|
posView.setText(
|
||||||
_.padStart(String(pos.row + 1), 2, '0') +
|
_.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());
|
this.client.term.rawWrite(ansi.restorePos());
|
||||||
}
|
}
|
||||||
|
|
|
@ -135,7 +135,7 @@ module.exports = class Address {
|
||||||
static fromString(addrStr) {
|
static fromString(addrStr) {
|
||||||
const m = FTN_ADDRESS_REGEXP.exec(addrStr);
|
const m = FTN_ADDRESS_REGEXP.exec(addrStr);
|
||||||
|
|
||||||
if (m && m[2] && m[3]) {
|
if (m) {
|
||||||
// start with a 2D
|
// start with a 2D
|
||||||
let addr = {
|
let addr = {
|
||||||
net: parseInt(m[2]),
|
net: parseInt(m[2]),
|
||||||
|
|
|
@ -763,11 +763,6 @@ module.exports = class Message {
|
||||||
}
|
}
|
||||||
|
|
||||||
persist(cb) {
|
persist(cb) {
|
||||||
const containsNonWhitespaceCharacterRegEx = /\S/;
|
|
||||||
if (!containsNonWhitespaceCharacterRegEx.test(this.message)) {
|
|
||||||
return cb(Errors.Invalid('Empty message'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isValid()) {
|
if (!this.isValid()) {
|
||||||
return cb(Errors.Invalid('Cannot persist invalid message!'));
|
return cb(Errors.Invalid('Cannot persist invalid message!'));
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,39 +14,6 @@ exports.moduleInfo = {
|
||||||
author: 'NuSkooler',
|
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 {
|
exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
@ -75,15 +42,8 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
|
||||||
],
|
],
|
||||||
function complete(err) {
|
function complete(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
const errMsgView = self.viewControllers.header.getView(
|
// :TODO:... sooooo now what?
|
||||||
MciViewIds.header.errorMsg
|
} else {
|
||||||
);
|
|
||||||
if (errMsgView) {
|
|
||||||
errMsgView.setText(err.message);
|
|
||||||
}
|
|
||||||
return cb(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// note: not logging 'from' here as it's part of client.log.xxxx()
|
// note: not logging 'from' here as it's part of client.log.xxxx()
|
||||||
self.client.log.info(
|
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})`
|
`User "${self.client.user.username}" posted message to "${msg.toUserName}" (${msg.areaTag})`
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return self.nextMenu(cb);
|
return self.nextMenu(cb);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,10 +21,8 @@ exports.validateEmailAvail = validateEmailAvail;
|
||||||
exports.validateBirthdate = validateBirthdate;
|
exports.validateBirthdate = validateBirthdate;
|
||||||
exports.validatePasswordSpec = validatePasswordSpec;
|
exports.validatePasswordSpec = validatePasswordSpec;
|
||||||
|
|
||||||
const emptyFieldError = () => new Error('Field cannot be empty');
|
|
||||||
|
|
||||||
function validateNonEmpty(data, cb) {
|
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) {
|
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.
|
// :TODO: remove hard-coded FTN check here. We need a decent way to register global supported flavors with modules.
|
||||||
const addressedToInfo = getAddressedToInfo(data);
|
const addressedToInfo = getAddressedToInfo(data);
|
||||||
|
|
||||||
if (addressedToInfo.name.length === 0) {
|
if (Message.AddressFlavor.FTN === addressedToInfo.flavor) {
|
||||||
return cb(emptyFieldError());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Message.AddressFlavor.Local !== addressedToInfo.flavor) {
|
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -179,10 +179,6 @@ TextView.prototype.setText = function (text, redraw) {
|
||||||
};
|
};
|
||||||
|
|
||||||
TextView.prototype.clearText = function () {
|
TextView.prototype.clearText = function () {
|
||||||
if (this.text) {
|
|
||||||
this.setText(this.fillChar.repeat(this.text.length));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setText('');
|
this.setText('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,19 @@
|
||||||
FROM --platform=${BUILDPLATFORM:-linux/amd64} node:20-bookworm-slim
|
FROM node:18-buster-slim
|
||||||
|
|
||||||
ARG TARGETPLATFORM
|
|
||||||
ARG BUILDPLATFORM
|
|
||||||
ARG TARGETOS
|
|
||||||
ARG TARGETBRANCH
|
|
||||||
|
|
||||||
LABEL maintainer="dave@force9.org"
|
LABEL maintainer="dave@force9.org"
|
||||||
|
|
||||||
ENV NVM_DIR /root/.nvm
|
ENV NVM_DIR /root/.nvm
|
||||||
ENV DEBIAN_FRONTEND noninteractive
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
|
COPY . /enigma-bbs
|
||||||
|
|
||||||
|
# Do some installing! (and alot of cleaning up) keeping it in one step for less docker layers
|
||||||
# Just copy the package.json so it only needs to build once
|
# - if you need to debug i recommend to break the steps with individual RUNs)
|
||||||
COPY package.json /enigma-bbs/
|
|
||||||
|
|
||||||
# Install APT and NPM packages
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
git \
|
git \
|
||||||
curl \
|
curl \
|
||||||
build-essential \
|
build-essential \
|
||||||
|
python \
|
||||||
python3 \
|
python3 \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
lrzsz \
|
lrzsz \
|
||||||
|
@ -28,21 +22,9 @@ RUN apt-get update \
|
||||||
unrar-free \
|
unrar-free \
|
||||||
p7zip-full \
|
p7zip-full \
|
||||||
dos2unix \
|
dos2unix \
|
||||||
&& npm set progress=false && npm config set depth 0 \
|
|
||||||
&& npm install -g npm@latest \
|
&& npm install -g npm@latest \
|
||||||
&& npm install -g pm2 \
|
&& npm install -g pm2 \
|
||||||
&& cd /enigma-bbs && npm install
|
&& 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 \
|
|
||||||
&& pm2 start main.js \
|
&& pm2 start main.js \
|
||||||
&& mkdir -p /enigma-bbs-pre/art \
|
&& mkdir -p /enigma-bbs-pre/art \
|
||||||
&& mkdir /enigma-bbs-pre/mods \
|
&& 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 art/* ../enigma-bbs-pre/art/ \
|
||||||
&& cp -rp mods/* ../enigma-bbs-pre/mods/ \
|
&& cp -rp mods/* ../enigma-bbs-pre/mods/ \
|
||||||
&& cp -rp config/* ../enigma-bbs-pre/config/ \
|
&& 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 \
|
&& apt-get autoremove -y \
|
||||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
|
||||||
&& apt-get clean
|
&& 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
|
# enigma storage mounts
|
||||||
VOLUME /enigma-bbs/art
|
VOLUME /enigma-bbs/art
|
||||||
VOLUME /enigma-bbs/config
|
VOLUME /enigma-bbs/config
|
||||||
|
|
|
@ -77,8 +77,6 @@ GEM
|
||||||
rb-inotify (~> 0.9, >= 0.9.10)
|
rb-inotify (~> 0.9, >= 0.9.10)
|
||||||
mercenary (0.4.0)
|
mercenary (0.4.0)
|
||||||
minitest (5.19.0)
|
minitest (5.19.0)
|
||||||
nokogiri (1.15.4-aarch64-linux)
|
|
||||||
racc (~> 1.4)
|
|
||||||
nokogiri (1.15.4-x86_64-linux)
|
nokogiri (1.15.4-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
pathutil (0.16.2)
|
pathutil (0.16.2)
|
||||||
|
@ -103,7 +101,6 @@ GEM
|
||||||
webrick (1.8.1)
|
webrick (1.8.1)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
aarch64-linux
|
|
||||||
x86_64-linux
|
x86_64-linux
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
|
|
|
@ -131,5 +131,4 @@ collections:
|
||||||
- admin/oputil.md
|
- admin/oputil.md
|
||||||
- admin/updating.md
|
- admin/updating.md
|
||||||
- troubleshooting/monitoring-logs.md
|
- troubleshooting/monitoring-logs.md
|
||||||
- troubleshooting/ssh-troubleshooting.md
|
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,9 @@ layout: page
|
||||||
title: SSH Server
|
title: SSH Server
|
||||||
---
|
---
|
||||||
## SSH Login Server
|
## SSH Login Server
|
||||||
|
|
||||||
The ENiGMA½ SSH *login server* allows secure user logins over SSH (ssh://).
|
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
|
## Configuration
|
||||||
|
|
||||||
Entries available under `config.loginServers.ssh`:
|
Entries available under `config.loginServers.ssh`:
|
||||||
|
|
||||||
| Item | Required | Description |
|
| 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`.
|
| `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.
|
| `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!
|
* *IMPORTANT* With the `privateKeyPass` option set, make sure that you verify that the config file is not readable by other users!
|
||||||
|
|
||||||
|
|
||||||
### Example Configuration
|
### Example Configuration
|
||||||
|
|
||||||
```hjson
|
```hjson
|
||||||
|
@ -42,93 +40,42 @@ Entries available under `config.loginServers.ssh`:
|
||||||
```
|
```
|
||||||
|
|
||||||
## Generate a SSH Private Key
|
## 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:
|
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
|
```bash
|
||||||
|
ssh-keygen -m PEM -h -f config/ssh_private_key.pem
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 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 descriptions:
|
||||||
|
|
||||||
| Option | Description |
|
| 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 |
|
| `-m PEM` | Set the output format to `PEM`, compatible with the `ssh2` library |
|
||||||
| `-h` | Generate a host key |
|
| `-h` | Generate a host key |
|
||||||
| `-f config/ssh_private_key.pem` | Filename for the private key. Used in the `privateKeyPem` option in the configuration |
|
| `-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.
|
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
|
### 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
|
```bash
|
||||||
|
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -pkeyopt rsa_keygen_pubexp:65537 | openssl rsa -out ./config/ssh_private_key.pem -aes128
|
||||||
```powershell
|
|
||||||
winget install -e --id ShiningLight.OpenSSL
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 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:
|
```bash
|
||||||
|
|
||||||
#### 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
|
|
||||||
openssl genrsa -aes128 -out ./config/ssh_private_key.pem 2048
|
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
|
## Prompt
|
||||||
|
|
||||||
|
|
|
@ -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`
|
|
|
@ -69,7 +69,7 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
actionKeys: @reference:common.escToPrev
|
actionKeys: @reference: common.escToPrev
|
||||||
}
|
}
|
||||||
1: {
|
1: {
|
||||||
mci: {
|
mci: {
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
"minimist": "^1.2.6",
|
"minimist": "^1.2.6",
|
||||||
"moment": "2.29.4",
|
"moment": "2.29.4",
|
||||||
"nntp-server": "3.1.0",
|
"nntp-server": "3.1.0",
|
||||||
"node-pty": "1.0.0",
|
"node-pty": "0.10.1",
|
||||||
"nodemailer": "6.7.7",
|
"nodemailer": "6.7.7",
|
||||||
"otplib": "11.0.1",
|
"otplib": "11.0.1",
|
||||||
"qrcode-generator": "^1.4.4",
|
"qrcode-generator": "^1.4.4",
|
||||||
|
@ -59,8 +59,8 @@
|
||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
"sqlite3": "5.1.6",
|
"sqlite3": "5.1.6",
|
||||||
"sqlite3-trans": "1.3.0",
|
"sqlite3-trans": "1.3.0",
|
||||||
"ssh2": "1.14.0",
|
"ssh2": "1.11.0",
|
||||||
"systeminformation": "5.21.7",
|
"systeminformation": "5.12.3",
|
||||||
"telnet-socket": "0.2.4",
|
"telnet-socket": "0.2.4",
|
||||||
"temptmp": "^1.1.0",
|
"temptmp": "^1.1.0",
|
||||||
"uuid": "8.3.2",
|
"uuid": "8.3.2",
|
||||||
|
|
Loading…
Reference in New Issue