Giant refactoring (or at least the start)

In short:
- cvmts is now bundled/built via parcel and inside of a npm/yarn workspace with multiple nodejs projects
- cvmts now uses the crusttest QEMU management and RFB library (or a fork, if you so prefer).
- cvmts does NOT use node-canvas anymore, instead we opt for the same route crusttest took and just encode jpegs ourselves from the RFB provoded framebuffer via jpeg-turbo. this means funnily enough sharp is back for more for thumbnails, but actually seems to WORK this time
- IPData is now managed in a very similar way to the original cvm 1.2 implementation where a central manager and reference count exist. tbh it wouldn't be that hard to implement multinode either, but for now, I'm not going to take much time on doing that.

this refactor is still incomplete. please do not treat it as generally available while it's not on the default branch. if you want to use it (and report bugs or send fixes) feel free to, but while it may "just work" in certain situations it may be very broken in others.

(yes, I know windows support is partially totaled by this; it's something that can and will be fixed)
This commit is contained in:
modeco80
2024-04-23 09:57:02 -04:00
parent 28dddfc363
commit cb297e15c4
46 changed files with 5661 additions and 1011 deletions

161
cvmts/src/User.ts Normal file
View File

@@ -0,0 +1,161 @@
import * as Utilities from './Utilities.js';
import * as guacutils from './guacutils.js';
import { WebSocket } from 'ws';
import { IPData } from './IPData.js';
import IConfig from './IConfig.js';
import RateLimiter from './RateLimiter.js';
import { execa, execaCommand, ExecaSyncError } from 'execa';
import { Logger } from '@cvmts/shared';
export class User {
socket: WebSocket;
nopSendInterval: NodeJS.Timeout;
msgRecieveInterval: NodeJS.Timeout;
nopRecieveTimeout?: NodeJS.Timeout;
username?: string;
connectedToNode: boolean;
viewMode: number;
rank: Rank;
msgsSent: number;
Config: IConfig;
IP: IPData;
// Rate limiters
ChatRateLimit: RateLimiter;
LoginRateLimit: RateLimiter;
RenameRateLimit: RateLimiter;
TurnRateLimit: RateLimiter;
VoteRateLimit: RateLimiter;
private logger = new Logger("CVMTS.User");
constructor(ws: WebSocket, ip: IPData, config: IConfig, username?: string, node?: string) {
this.IP = ip;
this.connectedToNode = false;
this.viewMode = -1;
this.Config = config;
this.socket = ws;
this.msgsSent = 0;
this.socket.on('close', () => {
clearInterval(this.nopSendInterval);
});
this.socket.on('message', (e) => {
clearTimeout(this.nopRecieveTimeout);
clearInterval(this.msgRecieveInterval);
this.msgRecieveInterval = setInterval(() => this.onNoMsg(), 10000);
});
this.nopSendInterval = setInterval(() => this.sendNop(), 5000);
this.msgRecieveInterval = setInterval(() => this.onNoMsg(), 10000);
this.sendNop();
if (username) this.username = username;
this.rank = 0;
this.ChatRateLimit = new RateLimiter(this.Config.collabvm.automute.messages, this.Config.collabvm.automute.seconds);
this.ChatRateLimit.on('limit', () => this.mute(false));
this.RenameRateLimit = new RateLimiter(3, 60);
this.RenameRateLimit.on('limit', () => this.closeConnection());
this.LoginRateLimit = new RateLimiter(4, 3);
this.LoginRateLimit.on('limit', () => this.closeConnection());
this.TurnRateLimit = new RateLimiter(5, 3);
this.TurnRateLimit.on('limit', () => this.closeConnection());
this.VoteRateLimit = new RateLimiter(3, 3);
this.VoteRateLimit.on('limit', () => this.closeConnection());
}
assignGuestName(existingUsers: string[]): string {
var username;
do {
username = 'guest' + Utilities.Randint(10000, 99999);
} while (existingUsers.indexOf(username) !== -1);
this.username = username;
return username;
}
sendNop() {
this.socket.send('3.nop;');
}
sendMsg(msg: string | Buffer) {
if (this.socket.readyState !== this.socket.OPEN) return;
clearInterval(this.nopSendInterval);
this.nopSendInterval = setInterval(() => this.sendNop(), 5000);
this.socket.send(msg);
}
private onNoMsg() {
this.sendNop();
this.nopRecieveTimeout = setTimeout(() => {
this.closeConnection();
}, 3000);
}
closeConnection() {
this.socket.send(guacutils.encode('disconnect'));
this.socket.close();
}
onMsgSent() {
if (!this.Config.collabvm.automute.enabled) return;
// rate limit guest and unregistered chat messages, but not staff ones
switch (this.rank) {
case Rank.Moderator:
case Rank.Admin:
break;
default:
this.ChatRateLimit.request();
break;
}
}
mute(permanent: boolean) {
this.IP.muted = true;
this.sendMsg(guacutils.encode('chat', '', `You have been muted${permanent ? '' : ` for ${this.Config.collabvm.tempMuteTime} seconds`}.`));
if (!permanent) {
clearTimeout(this.IP.tempMuteExpireTimeout);
this.IP.tempMuteExpireTimeout = setTimeout(() => this.unmute(), this.Config.collabvm.tempMuteTime * 1000);
}
}
unmute() {
clearTimeout(this.IP.tempMuteExpireTimeout);
this.IP.muted = false;
this.sendMsg(guacutils.encode('chat', '', 'You are no longer muted.'));
}
private banCmdArgs(arg: string): string {
return arg.replace(/\$IP/g, this.IP.address).replace(/\$NAME/g, this.username || '');
}
async ban() {
// Prevent the user from taking turns or chatting, in case the ban command takes a while
this.IP.muted = true;
try {
if (Array.isArray(this.Config.collabvm.bancmd)) {
let args: string[] = this.Config.collabvm.bancmd.map((a: string) => this.banCmdArgs(a));
if (args.length || args[0].length) {
await execa(args.shift()!, args, { stdout: process.stdout, stderr: process.stderr });
this.kick();
} else {
this.logger.Error(`Failed to ban ${this.IP.address} (${this.username}): Empty command`);
}
} else if (typeof this.Config.collabvm.bancmd == 'string') {
let cmd: string = this.banCmdArgs(this.Config.collabvm.bancmd);
if (cmd.length) {
await execaCommand(cmd, { stdout: process.stdout, stderr: process.stderr });
this.kick();
} else {
this.logger.Error(`Failed to ban ${this.IP.address} (${this.username}): Empty command`);
}
}
} catch (e) {
this.logger.Error(`Failed to ban ${this.IP.address} (${this.username}): ${(e as ExecaSyncError).shortMessage}`);
}
}
async kick() {
this.sendMsg('10.disconnect;');
this.socket.close();
}
}
export enum Rank {
Unregistered = 0,
// After all these years
Registered = 1,
Admin = 2,
Moderator = 3,
// Giving a good gap between server only internal ranks just in case
Turn = 10
}