Compare commits

..

No commits in common. "master" and "bugfix/ssh_doc_updates" have entirely different histories.

12 changed files with 732 additions and 876 deletions

View File

@ -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": {},

View File

@ -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

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

@ -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 escape(opCode, args);
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); 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]) {
self.clearScreen();
}
break; 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();
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, // 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}`)
); );
} }

View File

@ -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));

View File

@ -115,10 +115,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 +167,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 +180,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();

View File

@ -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
); );

View File

@ -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'
); );

View File

@ -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

View File

@ -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",

1200
yarn.lock

File diff suppressed because it is too large Load Diff