cvmts: Use npm versions of superqemu/nodejs-rfb.
We publish them now, so let's use them in cvmts! Additionally, this removes the 'shared' module entirely, since it has little purpose anymore. The logger is replaced with pino (because superqemu uses pino for logging itself).
This commit is contained in:
@@ -11,12 +11,14 @@
|
||||
"author": "Elijah R, modeco80",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@computernewb/nodejs-rfb": "^0.3.0",
|
||||
"@computernewb/superqemu": "^0.1.0",
|
||||
"@cvmts/cvm-rs": "*",
|
||||
"@cvmts/qemu": "*",
|
||||
"@maxmind/geoip2-node": "^5.0.0",
|
||||
"execa": "^8.0.1",
|
||||
"mnemonist": "^0.39.5",
|
||||
"msgpackr": "^1.10.2",
|
||||
"pino": "^9.3.1",
|
||||
"sharp": "^0.33.3",
|
||||
"toml": "^3.0.0",
|
||||
"ws": "^8.14.1"
|
||||
@@ -24,6 +26,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.12.5",
|
||||
"@types/ws": "^8.5.5",
|
||||
"pino-pretty": "^11.2.1",
|
||||
"prettier": "^3.2.5",
|
||||
"typescript": "^5.4.4"
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { Logger } from '@cvmts/shared';
|
||||
import { Rank, User } from './User.js';
|
||||
|
||||
export default class AuthManager {
|
||||
apiEndpoint: string;
|
||||
secretKey: string;
|
||||
|
||||
private logger = new Logger('CVMTS.AuthMan');
|
||||
|
||||
constructor(apiEndpoint: string, secretKey: string) {
|
||||
this.apiEndpoint = apiEndpoint;
|
||||
this.secretKey = secretKey;
|
||||
|
||||
@@ -6,18 +6,20 @@ import * as cvm from '@cvmts/cvm-rs';
|
||||
import CircularBuffer from 'mnemonist/circular-buffer.js';
|
||||
import Queue from 'mnemonist/queue.js';
|
||||
import { createHash } from 'crypto';
|
||||
import { VMState, QemuVM, QemuVmDefinition } from '@cvmts/qemu';
|
||||
import { VMState, QemuVM, QemuVmDefinition } from '@computernewb/superqemu';
|
||||
import { IPDataManager } from './IPData.js';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import AuthManager from './AuthManager.js';
|
||||
import { Size, Rect, Logger } from '@cvmts/shared';
|
||||
import { JPEGEncoder } from './JPEGEncoder.js';
|
||||
import VM from './VM.js';
|
||||
import { ReaderModel } from '@maxmind/geoip2-node';
|
||||
import * as msgpack from 'msgpackr';
|
||||
import { CollabVMProtocolMessage, CollabVMProtocolMessageType } from '@cvmts/collab-vm-1.2-binary-protocol';
|
||||
|
||||
import { Size, Rect } from './VMDisplay.js';
|
||||
import pino from 'pino';
|
||||
|
||||
// Instead of strange hacks we can just use nodejs provided
|
||||
// import.meta properties, which have existed since LTS if not before
|
||||
const __dirname = import.meta.dirname;
|
||||
@@ -36,6 +38,7 @@ type VoteTally = {
|
||||
no: number;
|
||||
};
|
||||
|
||||
|
||||
export default class CollabVMServer {
|
||||
private Config: IConfig;
|
||||
|
||||
@@ -87,7 +90,7 @@ export default class CollabVMServer {
|
||||
// Geoip
|
||||
private geoipReader: ReaderModel | null;
|
||||
|
||||
private logger = new Logger('CVMTS.Server');
|
||||
private logger = pino({ name: 'CVMTS.Server' });
|
||||
|
||||
constructor(config: IConfig, vm: VM, auth: AuthManager | null, geoipReader: ReaderModel | null) {
|
||||
this.Config = config;
|
||||
@@ -121,7 +124,7 @@ export default class CollabVMServer {
|
||||
if (config.vm.type == 'qemu') {
|
||||
(vm as QemuVM).on('statechange', (newState: VMState) => {
|
||||
if (newState == VMState.Started) {
|
||||
self.logger.Info('VM started');
|
||||
self.logger.info('VM started');
|
||||
// well aware this sucks but whatever
|
||||
self.VM.GetDisplay().on('resize', (size: Size) => self.OnDisplayResized(size));
|
||||
self.VM.GetDisplay().on('rect', (rect: Rect) => self.OnDisplayRectangle(rect));
|
||||
@@ -129,7 +132,7 @@ export default class CollabVMServer {
|
||||
|
||||
if (newState == VMState.Stopped) {
|
||||
setTimeout(async () => {
|
||||
self.logger.Info('restarting VM');
|
||||
self.logger.info('restarting VM');
|
||||
await self.VM.Start();
|
||||
}, kRestartTimeout);
|
||||
}
|
||||
@@ -154,7 +157,7 @@ export default class CollabVMServer {
|
||||
try {
|
||||
user.countryCode = this.geoipReader!.country(user.IP.address).country!.isoCode;
|
||||
} catch (error) {
|
||||
this.logger.Warning(`Failed to get country code for ${user.IP.address}: ${(error as Error).message}`);
|
||||
this.logger.warn(`Failed to get country code for ${user.IP.address}: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
user.socket.on('msg', (msg: string) => this.onMessage(user, msg));
|
||||
@@ -179,7 +182,7 @@ export default class CollabVMServer {
|
||||
|
||||
this.clients.splice(clientIndex, 1);
|
||||
|
||||
this.logger.Info(`Disconnect From ${user.IP.address}${user.username ? ` with username ${user.username}` : ''}`);
|
||||
this.logger.info(`Disconnect From ${user.IP.address}${user.username ? ` with username ${user.username}` : ''}`);
|
||||
if (!user.username) return;
|
||||
if (this.TurnQueue.toArray().indexOf(user) !== -1) {
|
||||
var hadturn = this.TurnQueue.peek() === user;
|
||||
@@ -204,7 +207,7 @@ export default class CollabVMServer {
|
||||
try {
|
||||
let res = await this.auth!.Authenticate(msgArr[1], client);
|
||||
if (res.clientSuccess) {
|
||||
this.logger.Info(`${client.IP.address} logged in as ${res.username}`);
|
||||
this.logger.info(`${client.IP.address} logged in as ${res.username}`);
|
||||
client.sendMsg(cvm.guacEncode('login', '1'));
|
||||
let old = this.clients.find((c) => c.username === res.username);
|
||||
if (old) {
|
||||
@@ -236,7 +239,7 @@ export default class CollabVMServer {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.Error(`Error authenticating client ${client.IP.address}: ${(err as Error).message}`);
|
||||
this.logger.error(`Error authenticating client ${client.IP.address}: ${(err as Error).message}`);
|
||||
// for now?
|
||||
client.sendMsg(cvm.guacEncode('login', '0', 'There was an internal error while authenticating. Please let a staff member know as soon as possible'));
|
||||
}
|
||||
@@ -679,7 +682,7 @@ export default class CollabVMServer {
|
||||
}
|
||||
} catch (err) {
|
||||
// No
|
||||
this.logger.Error(`User ${user?.IP.address} ${user?.username ? `with username ${user?.username}` : ''} sent broken Guacamole: ${err as Error}`);
|
||||
this.logger.error(`User ${user?.IP.address} ${user?.username ? `with username ${user?.username}` : ''} sent broken Guacamole: ${err as Error}`);
|
||||
user?.kick();
|
||||
}
|
||||
}
|
||||
@@ -721,10 +724,10 @@ export default class CollabVMServer {
|
||||
|
||||
client.sendMsg(cvm.guacEncode('rename', '0', status, client.username!, client.rank.toString()));
|
||||
if (hadName) {
|
||||
this.logger.Info(`Rename ${client.IP.address} from ${oldname} to ${client.username}`);
|
||||
this.logger.info(`Rename ${client.IP.address} from ${oldname} to ${client.username}`);
|
||||
if (announce) this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('rename', '1', oldname, client.username!, client.rank.toString())));
|
||||
} else {
|
||||
this.logger.Info(`Rename ${client.IP.address} to ${client.username}`);
|
||||
this.logger.info(`Rename ${client.IP.address} to ${client.username}`);
|
||||
if (announce)
|
||||
this.clients.forEach((c) => {
|
||||
c.sendMsg(cvm.guacEncode('adduser', '1', client.username!, client.rank.toString()));
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Logger } from '@cvmts/shared';
|
||||
import { Reader, ReaderModel } from '@maxmind/geoip2-node';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'node:path';
|
||||
@@ -6,102 +5,101 @@ import { Readable } from 'node:stream';
|
||||
import { ReadableStream } from 'node:stream/web';
|
||||
import { finished } from 'node:stream/promises';
|
||||
import { execa } from 'execa';
|
||||
import pino from 'pino';
|
||||
|
||||
export default class GeoIPDownloader {
|
||||
private directory: string;
|
||||
private accountID: string;
|
||||
private licenseKey: string;
|
||||
private logger: Logger
|
||||
constructor(filename: string, accountID: string, licenseKey: string) {
|
||||
this.directory = filename;
|
||||
if (!this.directory.endsWith('/')) this.directory += '/';
|
||||
this.accountID = accountID;
|
||||
this.licenseKey = licenseKey;
|
||||
this.logger = new Logger('CVMTS.GeoIPDownloader');
|
||||
}
|
||||
private directory: string;
|
||||
private accountID: string;
|
||||
private licenseKey: string;
|
||||
private logger = pino({ name: 'CVMTS.GeoIPDownloader' });
|
||||
constructor(filename: string, accountID: string, licenseKey: string) {
|
||||
this.directory = filename;
|
||||
if (!this.directory.endsWith('/')) this.directory += '/';
|
||||
this.accountID = accountID;
|
||||
this.licenseKey = licenseKey;
|
||||
}
|
||||
|
||||
private genAuthHeader(): string {
|
||||
return `Basic ${Buffer.from(`${this.accountID}:${this.licenseKey}`).toString('base64')}`;
|
||||
}
|
||||
private genAuthHeader(): string {
|
||||
return `Basic ${Buffer.from(`${this.accountID}:${this.licenseKey}`).toString('base64')}`;
|
||||
}
|
||||
|
||||
private async ensureDirectoryExists(): Promise<void> {
|
||||
let stat;
|
||||
try {
|
||||
stat = await fs.stat(this.directory);
|
||||
}
|
||||
catch (e) {
|
||||
var error = e as NodeJS.ErrnoException;
|
||||
if (error.code === 'ENOTDIR') {
|
||||
this.logger.Warning('File exists at GeoIP directory path, unlinking...');
|
||||
await fs.unlink(this.directory.substring(0, this.directory.length - 1));
|
||||
} else if (error.code !== 'ENOENT') {
|
||||
this.logger.Error('Failed to access GeoIP directory: {0}', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
this.logger.Info('Creating GeoIP directory: {0}', this.directory);
|
||||
await fs.mkdir(this.directory, { recursive: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
private async ensureDirectoryExists(): Promise<void> {
|
||||
let stat;
|
||||
try {
|
||||
stat = await fs.stat(this.directory);
|
||||
} catch (e) {
|
||||
var error = e as NodeJS.ErrnoException;
|
||||
if (error.code === 'ENOTDIR') {
|
||||
this.logger.warn('File exists at GeoIP directory path, unlinking...');
|
||||
await fs.unlink(this.directory.substring(0, this.directory.length - 1));
|
||||
} else if (error.code !== 'ENOENT') {
|
||||
this.logger.error('Failed to access GeoIP directory: {0}', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
this.logger.info('Creating GeoIP directory: {0}', this.directory);
|
||||
await fs.mkdir(this.directory, { recursive: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async getGeoIPReader(): Promise<ReaderModel> {
|
||||
await this.ensureDirectoryExists();
|
||||
let dbpath = path.join(this.directory, (await this.getLatestVersion()).replace('.tar.gz', ''), 'GeoLite2-Country.mmdb');
|
||||
try {
|
||||
await fs.access(dbpath, fs.constants.F_OK | fs.constants.R_OK);
|
||||
this.logger.Info('Loading cached GeoIP database: {0}', dbpath);
|
||||
} catch (ex) {
|
||||
var error = ex as NodeJS.ErrnoException;
|
||||
if (error.code === 'ENOENT') {
|
||||
await this.downloadLatestDatabase();
|
||||
} else {
|
||||
this.logger.Error('Failed to access GeoIP database: {0}', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
return await Reader.open(dbpath);
|
||||
}
|
||||
async getGeoIPReader(): Promise<ReaderModel> {
|
||||
await this.ensureDirectoryExists();
|
||||
let dbpath = path.join(this.directory, (await this.getLatestVersion()).replace('.tar.gz', ''), 'GeoLite2-Country.mmdb');
|
||||
try {
|
||||
await fs.access(dbpath, fs.constants.F_OK | fs.constants.R_OK);
|
||||
this.logger.info('Loading cached GeoIP database: {0}', dbpath);
|
||||
} catch (ex) {
|
||||
var error = ex as NodeJS.ErrnoException;
|
||||
if (error.code === 'ENOENT') {
|
||||
await this.downloadLatestDatabase();
|
||||
} else {
|
||||
this.logger.error('Failed to access GeoIP database: {0}', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
return await Reader.open(dbpath);
|
||||
}
|
||||
|
||||
async getLatestVersion(): Promise<string> {
|
||||
let res = await fetch('https://download.maxmind.com/geoip/databases/GeoLite2-Country/download?suffix=tar.gz', {
|
||||
redirect: 'follow',
|
||||
method: "HEAD",
|
||||
headers: {
|
||||
"Authorization": this.genAuthHeader()
|
||||
}
|
||||
});
|
||||
let disposition = res.headers.get('Content-Disposition');
|
||||
if (!disposition) {
|
||||
this.logger.Error('Failed to get latest version of GeoIP database: No Content-Disposition header');
|
||||
process.exit(1);
|
||||
}
|
||||
let filename = disposition.match(/filename=(.*)$/);
|
||||
if (!filename) {
|
||||
this.logger.Error('Failed to get latest version of GeoIP database: Could not parse version from Content-Disposition header');
|
||||
process.exit(1);
|
||||
}
|
||||
return filename[1];
|
||||
}
|
||||
async getLatestVersion(): Promise<string> {
|
||||
let res = await fetch('https://download.maxmind.com/geoip/databases/GeoLite2-Country/download?suffix=tar.gz', {
|
||||
redirect: 'follow',
|
||||
method: 'HEAD',
|
||||
headers: {
|
||||
Authorization: this.genAuthHeader()
|
||||
}
|
||||
});
|
||||
let disposition = res.headers.get('Content-Disposition');
|
||||
if (!disposition) {
|
||||
this.logger.error('Failed to get latest version of GeoIP database: No Content-Disposition header');
|
||||
process.exit(1);
|
||||
}
|
||||
let filename = disposition.match(/filename=(.*)$/);
|
||||
if (!filename) {
|
||||
this.logger.error('Failed to get latest version of GeoIP database: Could not parse version from Content-Disposition header');
|
||||
process.exit(1);
|
||||
}
|
||||
return filename[1];
|
||||
}
|
||||
|
||||
async downloadLatestDatabase(): Promise<void> {
|
||||
let filename = await this.getLatestVersion();
|
||||
this.logger.Info('Downloading latest GeoIP database: {0}', filename);
|
||||
let dbpath = path.join(this.directory, filename);
|
||||
let file = await fs.open(dbpath, fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_WRONLY);
|
||||
let stream = file.createWriteStream();
|
||||
let res = await fetch('https://download.maxmind.com/geoip/databases/GeoLite2-Country/download?suffix=tar.gz', {
|
||||
redirect: 'follow',
|
||||
headers: {
|
||||
"Authorization": this.genAuthHeader()
|
||||
}
|
||||
});
|
||||
await finished(Readable.fromWeb(res.body as ReadableStream<any>).pipe(stream));
|
||||
await file.close();
|
||||
this.logger.Info('Finished downloading latest GeoIP database: {0}', filename);
|
||||
this.logger.Info('Extracting GeoIP database: {0}', filename);
|
||||
// yeah whatever
|
||||
await execa('tar', ['xzf', filename], {cwd: this.directory});
|
||||
this.logger.Info('Unlinking GeoIP tarball');
|
||||
await fs.unlink(dbpath);
|
||||
}
|
||||
}
|
||||
async downloadLatestDatabase(): Promise<void> {
|
||||
let filename = await this.getLatestVersion();
|
||||
this.logger.info('Downloading latest GeoIP database: {0}', filename);
|
||||
let dbpath = path.join(this.directory, filename);
|
||||
let file = await fs.open(dbpath, fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_WRONLY);
|
||||
let stream = file.createWriteStream();
|
||||
let res = await fetch('https://download.maxmind.com/geoip/databases/GeoLite2-Country/download?suffix=tar.gz', {
|
||||
redirect: 'follow',
|
||||
headers: {
|
||||
Authorization: this.genAuthHeader()
|
||||
}
|
||||
});
|
||||
await finished(Readable.fromWeb(res.body as ReadableStream<any>).pipe(stream));
|
||||
await file.close();
|
||||
this.logger.info('Finished downloading latest GeoIP database: {0}', filename);
|
||||
this.logger.info('Extracting GeoIP database: {0}', filename);
|
||||
// yeah whatever
|
||||
await execa('tar', ['xzf', filename], { cwd: this.directory });
|
||||
this.logger.info('Unlinking GeoIP tarball');
|
||||
await fs.unlink(dbpath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Logger } from '@cvmts/shared';
|
||||
import pino from 'pino';
|
||||
|
||||
export class IPData {
|
||||
tempMuteExpireTimeout?: NodeJS.Timeout;
|
||||
@@ -22,7 +22,7 @@ export class IPData {
|
||||
|
||||
export class IPDataManager {
|
||||
static ipDatas = new Map<string, IPData>();
|
||||
static logger = new Logger('CVMTS.IPDataManager');
|
||||
static logger = pino({ name: 'CVMTS.IPDataManager' });
|
||||
|
||||
static GetIPData(address: string) {
|
||||
if (IPDataManager.ipDatas.has(address)) {
|
||||
@@ -64,7 +64,7 @@ export class IPDataManager {
|
||||
setInterval(() => {
|
||||
for (let tuple of IPDataManager.ipDatas) {
|
||||
if (tuple[1].refCount == 0) {
|
||||
IPDataManager.logger.Info('Deleted IPData for IP {0}', tuple[0]);
|
||||
IPDataManager.logger.info(`Deleted IPData for IP ${tuple[0]}`);
|
||||
IPDataManager.ipDatas.delete(tuple[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Size, Rect } from '@cvmts/shared';
|
||||
import { Size, Rect } from './VMDisplay.js';
|
||||
import sharp from 'sharp';
|
||||
import * as cvm from '@cvmts/cvm-rs';
|
||||
|
||||
|
||||
@@ -2,20 +2,19 @@ import EventEmitter from 'events';
|
||||
import NetworkServer from '../NetworkServer.js';
|
||||
import { Server, Socket } from 'net';
|
||||
import IConfig from '../IConfig.js';
|
||||
import { Logger } from '@cvmts/shared';
|
||||
import TCPClient from './TCPClient.js';
|
||||
import { IPDataManager } from '../IPData.js';
|
||||
import { User } from '../User.js';
|
||||
import pino from 'pino';
|
||||
|
||||
export default class TCPServer extends EventEmitter implements NetworkServer {
|
||||
listener: Server;
|
||||
Config: IConfig;
|
||||
logger: Logger;
|
||||
logger = pino({name: 'CVMTS.TCPServer'});
|
||||
clients: TCPClient[];
|
||||
|
||||
constructor(config: IConfig) {
|
||||
super();
|
||||
this.logger = new Logger('CVMTS.TCPServer');
|
||||
this.Config = config;
|
||||
this.listener = new Server();
|
||||
this.clients = [];
|
||||
@@ -23,7 +22,7 @@ export default class TCPServer extends EventEmitter implements NetworkServer {
|
||||
}
|
||||
|
||||
private onConnection(socket: Socket) {
|
||||
this.logger.Info(`New TCP connection from ${socket.remoteAddress}`);
|
||||
this.logger.info(`New TCP connection from ${socket.remoteAddress}`);
|
||||
var client = new TCPClient(socket);
|
||||
this.clients.push(client);
|
||||
this.emit('connect', new User(client, IPDataManager.GetIPData(client.getIP()), this.Config));
|
||||
@@ -31,7 +30,7 @@ export default class TCPServer extends EventEmitter implements NetworkServer {
|
||||
|
||||
start(): void {
|
||||
this.listener.listen(this.Config.tcp.port, this.Config.tcp.host, () => {
|
||||
this.logger.Info(`TCP server listening on ${this.Config.tcp.host}:${this.Config.tcp.port}`);
|
||||
this.logger.info(`TCP server listening on ${this.Config.tcp.host}:${this.Config.tcp.port}`);
|
||||
});
|
||||
}
|
||||
stop(): void {
|
||||
|
||||
@@ -4,9 +4,9 @@ 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';
|
||||
import NetworkClient from './NetworkClient.js';
|
||||
import { CollabVMCapabilities } from '@cvmts/collab-vm-1.2-binary-protocol';
|
||||
import pino from 'pino';
|
||||
|
||||
export class User {
|
||||
socket: NetworkClient;
|
||||
@@ -31,7 +31,7 @@ export class User {
|
||||
TurnRateLimit: RateLimiter;
|
||||
VoteRateLimit: RateLimiter;
|
||||
|
||||
private logger = new Logger('CVMTS.User');
|
||||
private logger = pino({ name: 'CVMTS.User' });
|
||||
|
||||
constructor(socket: NetworkClient, ip: IPData, config: IConfig, username?: string, node?: string) {
|
||||
this.IP = ip;
|
||||
@@ -148,7 +148,7 @@ export class User {
|
||||
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`);
|
||||
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);
|
||||
@@ -156,11 +156,11 @@ export class User {
|
||||
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`);
|
||||
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}`);
|
||||
this.logger.error(`Failed to ban ${this.IP.address} (${this.username}): ${(e as ExecaSyncError).shortMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { VMState } from '@cvmts/qemu';
|
||||
import VMDisplay from './VMDisplay.js';
|
||||
import { VMState } from '@computernewb/superqemu';
|
||||
import { VMDisplay } from './VMDisplay.js';
|
||||
|
||||
export default interface VM {
|
||||
Start(): Promise<void>;
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
import { Size } from '@cvmts/shared';
|
||||
import EventEmitter from 'node:events';
|
||||
|
||||
export default interface VMDisplay extends EventEmitter {
|
||||
// not great but whatever
|
||||
// nodejs-rfb COULD probably export them though.
|
||||
export type Size = {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type Rect = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export interface VMDisplay extends EventEmitter {
|
||||
Connect(): void;
|
||||
Disconnect(): void;
|
||||
Connected(): boolean;
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import EventEmitter from 'events';
|
||||
import VNCVMDef from './VNCVMDef';
|
||||
import VM from '../VM';
|
||||
import VMDisplay from '../VMDisplay';
|
||||
import { Clamp, Logger, Rect, Size, Sleep } from '@cvmts/shared';
|
||||
import { Size, Rect, VMDisplay } from '../VMDisplay';
|
||||
import { VncClient } from '@computernewb/nodejs-rfb';
|
||||
import { BatchRects, VMState } from '@cvmts/qemu';
|
||||
import { BatchRects, VMState } from '@computernewb/superqemu';
|
||||
import { execaCommand } from 'execa';
|
||||
import pino from 'pino';
|
||||
|
||||
function Clamp(input: number, min: number, max: number) {
|
||||
return Math.min(Math.max(input, min), max);
|
||||
}
|
||||
|
||||
async function Sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export default class VNCVM extends EventEmitter implements VM, VMDisplay {
|
||||
def: VNCVMDef;
|
||||
logger: Logger;
|
||||
logger;
|
||||
private displayVnc = new VncClient({
|
||||
debug: false,
|
||||
fps: 60,
|
||||
@@ -20,7 +28,8 @@ export default class VNCVM extends EventEmitter implements VM, VMDisplay {
|
||||
constructor(def: VNCVMDef) {
|
||||
super();
|
||||
this.def = def;
|
||||
this.logger = new Logger(`CVMTS.VNCVM/${this.def.vncHost}:${this.def.vncPort}`);
|
||||
// TODO: Now that we're using an actual structured logger can we please
|
||||
this.logger = pino({ name: `CVMTS.VNCVM/${this.def.vncHost}:${this.def.vncPort}` });
|
||||
|
||||
this.displayVnc.on('connectTimeout', () => {
|
||||
this.Reconnect();
|
||||
@@ -31,7 +40,7 @@ export default class VNCVM extends EventEmitter implements VM, VMDisplay {
|
||||
});
|
||||
|
||||
this.displayVnc.on('disconnect', () => {
|
||||
this.logger.Info('Disconnected');
|
||||
this.logger.info('Disconnected');
|
||||
this.Reconnect();
|
||||
});
|
||||
|
||||
@@ -40,7 +49,7 @@ export default class VNCVM extends EventEmitter implements VM, VMDisplay {
|
||||
});
|
||||
|
||||
this.displayVnc.on('firstFrameUpdate', () => {
|
||||
this.logger.Info('Connected');
|
||||
this.logger.info('Connected');
|
||||
// apparently this library is this good.
|
||||
// at least it's better than the two others which exist.
|
||||
this.displayVnc.changeFps(60);
|
||||
@@ -101,13 +110,13 @@ export default class VNCVM extends EventEmitter implements VM, VMDisplay {
|
||||
}
|
||||
|
||||
async Start(): Promise<void> {
|
||||
this.logger.Info('Connecting');
|
||||
this.logger.info('Connecting');
|
||||
if (this.def.startCmd) await execaCommand(this.def.startCmd, { shell: true });
|
||||
this.Connect();
|
||||
}
|
||||
|
||||
async Stop(): Promise<void> {
|
||||
this.logger.Info('Disconnecting');
|
||||
this.logger.info('Disconnecting');
|
||||
this.Disconnect();
|
||||
if (this.def.stopCmd) await execaCommand(this.def.stopCmd, { shell: true });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { WebSocket } from 'ws';
|
||||
import NetworkClient from '../NetworkClient.js';
|
||||
import EventEmitter from 'events';
|
||||
import { Logger } from '@cvmts/shared';
|
||||
|
||||
export default class WSClient extends EventEmitter implements NetworkClient {
|
||||
socket: WebSocket;
|
||||
|
||||
@@ -8,20 +8,19 @@ import { isIP } from 'net';
|
||||
import { IPDataManager } from '../IPData.js';
|
||||
import WSClient from './WSClient.js';
|
||||
import { User } from '../User.js';
|
||||
import { Logger } from '@cvmts/shared';
|
||||
import pino from 'pino';
|
||||
|
||||
export default class WSServer extends EventEmitter implements NetworkServer {
|
||||
private httpServer: http.Server;
|
||||
private wsServer: WebSocketServer;
|
||||
private clients: WSClient[];
|
||||
private Config: IConfig;
|
||||
private logger: Logger;
|
||||
private logger = pino({ name: 'CVMTS.WSServer' });
|
||||
|
||||
constructor(config: IConfig) {
|
||||
super();
|
||||
this.Config = config;
|
||||
this.clients = [];
|
||||
this.logger = new Logger('CVMTS.WSServer');
|
||||
this.httpServer = http.createServer();
|
||||
this.wsServer = new WebSocketServer({ noServer: true });
|
||||
this.httpServer.on('upgrade', (req: http.IncomingMessage, socket: internal.Duplex, head: Buffer) => this.httpOnUpgrade(req, socket, head));
|
||||
@@ -34,7 +33,7 @@ export default class WSServer extends EventEmitter implements NetworkServer {
|
||||
|
||||
start(): void {
|
||||
this.httpServer.listen(this.Config.http.port, this.Config.http.host, () => {
|
||||
this.logger.Info(`WebSocket server listening on ${this.Config.http.host}:${this.Config.http.port}`);
|
||||
this.logger.info(`WebSocket server listening on ${this.Config.http.host}:${this.Config.http.port}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -94,7 +93,7 @@ export default class WSServer extends EventEmitter implements NetworkServer {
|
||||
// Make sure x-forwarded-for is set
|
||||
if (req.headers['x-forwarded-for'] === undefined) {
|
||||
killConnection();
|
||||
this.logger.Error('X-Forwarded-For header not set. This is most likely a misconfiguration of your reverse proxy.');
|
||||
this.logger.error('X-Forwarded-For header not set. This is most likely a misconfiguration of your reverse proxy.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -102,7 +101,7 @@ export default class WSServer extends EventEmitter implements NetworkServer {
|
||||
ip = req.headers['x-forwarded-for']?.toString().replace(/\ /g, '').split(',')[0];
|
||||
} catch {
|
||||
// If we can't get the IP, kill the connection
|
||||
this.logger.Error('Invalid X-Forwarded-For header. This is most likely a misconfiguration of your reverse proxy.');
|
||||
this.logger.error('Invalid X-Forwarded-For header. This is most likely a misconfiguration of your reverse proxy.');
|
||||
killConnection();
|
||||
return;
|
||||
}
|
||||
@@ -135,10 +134,10 @@ export default class WSServer extends EventEmitter implements NetworkServer {
|
||||
this.emit('connect', user);
|
||||
|
||||
ws.on('error', (e) => {
|
||||
this.logger.Error(`${e} (caused by connection ${ip})`);
|
||||
this.logger.error(`${e} (caused by connection ${ip})`);
|
||||
ws.close();
|
||||
});
|
||||
|
||||
this.logger.Info(`New WebSocket connection from ${user.IP.address}`);
|
||||
this.logger.info(`New WebSocket connection from ${user.IP.address}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@ import IConfig from './IConfig.js';
|
||||
import * as fs from 'fs';
|
||||
import CollabVMServer from './CollabVMServer.js';
|
||||
|
||||
import { QemuVM, QemuVmDefinition } from '@cvmts/qemu';
|
||||
import { QemuVM, QemuVmDefinition } from '@computernewb/superqemu';
|
||||
|
||||
import * as Shared from '@cvmts/shared';
|
||||
import AuthManager from './AuthManager.js';
|
||||
import WSServer from './WebSocket/WSServer.js';
|
||||
import { User } from './User.js';
|
||||
@@ -13,24 +12,25 @@ import TCPServer from './TCP/TCPServer.js';
|
||||
import VM from './VM.js';
|
||||
import VNCVM from './VNCVM/VNCVM.js';
|
||||
import GeoIPDownloader from './GeoIPDownloader.js';
|
||||
import pino from 'pino';
|
||||
|
||||
let logger = new Shared.Logger('CVMTS.Init');
|
||||
let logger = pino();
|
||||
|
||||
logger.Info('CollabVM Server starting up');
|
||||
logger.info('CollabVM Server starting up');
|
||||
|
||||
// Parse the config file
|
||||
|
||||
let Config: IConfig;
|
||||
|
||||
if (!fs.existsSync('config.toml')) {
|
||||
logger.Error('Fatal error: Config.toml not found. Please copy config.example.toml and fill out fields');
|
||||
logger.error('Fatal error: Config.toml not found. Please copy config.example.toml and fill out fields');
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
var configRaw = fs.readFileSync('config.toml').toString();
|
||||
Config = toml.parse(configRaw);
|
||||
} catch (e) {
|
||||
logger.Error('Fatal error: Failed to read or parse the config file: {0}', (e as Error).message);
|
||||
logger.error('Fatal error: Failed to read or parse the config file: {0}', (e as Error).message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -54,15 +54,6 @@ async function start() {
|
||||
let auth = Config.auth.enabled ? new AuthManager(Config.auth.apiEndpoint, Config.auth.secretKey) : null;
|
||||
switch (Config.vm.type) {
|
||||
case 'qemu': {
|
||||
// Print a warning if qmpSockDir is set
|
||||
// and the host OS is Windows, as this
|
||||
// configuration will very likely not work.
|
||||
if (process.platform === 'win32' && Config.qemu.qmpSockDir !== null) {
|
||||
logger.Warning("You appear to have the option 'qmpSockDir' enabled in the config.");
|
||||
logger.Warning('This is not supported on Windows, and you will likely run into issues.');
|
||||
logger.Warning('To remove this warning, use the qmpHost and qmpPort options instead.');
|
||||
}
|
||||
|
||||
// Fire up the VM
|
||||
let def: QemuVmDefinition = {
|
||||
id: Config.collabvm.node,
|
||||
@@ -78,7 +69,7 @@ async function start() {
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
logger.Error('Invalid VM type in config: {0}', Config.vm.type);
|
||||
logger.error(`Invalid VM type in config: ${Config.vm.type}`);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user