This commit is contained in:
Elijah 2024-06-23 18:32:40 -04:00
commit 5fd343c87b
5 changed files with 896 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
node_modules/
package-lock.json
yarn.lock
config.js

259
client.js Normal file
View file

@ -0,0 +1,259 @@
// Small single-file CollabVM client library, in semi-modern Javascript
import * as ws from 'ws';
const guacutils = {
parse: (string) => {
let pos = -1;
let sections = [];
for(;;) {
let len=string.indexOf('.', pos + 1);
if(len === -1)
break;
pos=parseInt(string.slice(pos + 1, len)) + len + 1
sections.push(string.slice(len + 1, pos)
.replace(/'/g, "'")
.replace(/"/g, '"')
.replace(///g, '/')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
);
if(string.slice(pos, pos + 1) === ';')
break;
}
return sections;
},
encode: (cypher) =>{
let command = '';
for(var i = 0; i < cypher.length; i++) {
let current = cypher[i];
command += current.length + '.' + current;
command += ( i < cypher.length - 1 ? ',' : ';');
}
return command;
}
};
const ConnectionState = Object.freeze({
CLOSED: 0,
CONNECTING: 1,
CONNECTED: 2
});
// System chat messages have a nil username.
function IsSystemChatInstruction(inst) {
return inst[1] == '';
}
class UserData {
#_name;
#_rank;
constructor(name, rank) {
this._name = name;
this._rank = rank;
}
GetName() { return this._name; }
GetRank() { return this._rank; }
UpdateRank(new_rank) { this._rank = new_rank; }
}
export default class CollabVMClient {
constructor() {
this._state = ConnectionState.CLOSED;
this._users = [];
}
GetState() { return this._state; }
Connect(uri) {
this._ws = new ws.WebSocket(uri, 'guacamole', {
origin: "https://computernewb.com"
});
this._ws.onopen = this.OnWebSocketOpen.bind(this);
this._ws.onclose = this.OnWebSocketClose.bind(this);
this._ws.onerror = this.OnWebSocketError.bind(this)
this._ws.onmessage = this.OnWebSocketMessage.bind(this);
this._state = ConnectionState.CONNECTING;
}
OnWebSocketOpen() {
this._state = ConnectionState.CONNECTED;
this.OnOpen();
}
OnWebSocketClose() {
this._state = ConnectionState.CLOSED;
this.OnClose(arguments);
}
OnWebSocketError() {
// fire the close handler
this._state = ConnectionState.CLOSED;
this.OnClose(arguments);
}
Close() {
this._state = ConnectionState.CLOSED;
this._ws.close();
}
OnWebSocketMessage(ev) {
// cvm server should never send binary data
if(typeof(ev.data) !== "string")
return;
let message = guacutils.parse(ev.data);
if(message.length === 0)
return;
// Hardcoded, we need to keep this to be alive
if(message[0] === "nop") {
this.SendGuacamoleMessage("nop");
return;
}
//if(message[0] === "chat") {
// console.log(`FUCK (${message.length}) ${message[1]} ${message[2]}`)
//}
if(message[0] === "chat" && message.length === 3 && !IsSystemChatInstruction(message)) {
if(message[1] != this._username) {
//console.log(`FUCK 2 (${message.length}) ${message[1]} ${message[2]}`)
this.OnChat(message[1], message[2]);
return;
}
}
if(message[0] === "adduser") {
this.OnAddUser(message.slice(2), parseInt(message[1]));
return;
}
if(message[0] === "remuser")
this.OnRemUser(message.slice(2), parseInt(message[1]));
// Handle renames
if(message[0] === "rename") {
if(message.length === 5) {
if(message[1] == '1') {
this._username = message[3];
}
}
}
this.OnGuacamoleMessage(message);
}
OnAddUser(users, count) {
//console.log(users);
for(var i = 0; i < count * 2; i += 2) {
let name = users[i];
let rank = users[i + 1];
//console.log(`[${this.GetVM()}] user ${name} rank ${rank}`)
let existingUser = this._users.find(elem => elem.GetName() == name);
if(existingUser === undefined) {
//console.log(`[${this.GetVM()}] New user ${name} rank ${rank}`)
this._users.push(new UserData(name, rank));
} else {
// Handle admin/mod rank update
if(existingUser.GetRank() != rank) {
//console.log(`[${this.GetVM()}] updating ${name} to rank ${rank}`)
existingUser.UpdateRank(rank);
}
}
}
this.OnAddUser_Bot(this.GetUserCount());
}
OnRemUser(users, count) {
for(var i = 0; i < count; i++) {
let saveUserTemp = this.GetUser(users[i]);
this._users = this._users.filter(user => user.GetName() != users[i]);
}
this.OnRemUser_Bot(this.GetUserCount());
}
// This subtracts bots, including ourselves
GetUserCount() {
const KnownBots = [
this.GetUsername(),
//"General Darian"
// logger bots
"Specialized Egg",
"Emperor Kevin"
];
var len = this._users.length;
// subtract known bots
for(var i = len - 1; i != 0; --i) {
// ?
if(this._users[i] === undefined)
return;
var name = this._users[i].GetName();
if(KnownBots.find(elem => name == elem) !== undefined) {
//console.log("found blacklisted username", name)
len--;
}
}
return len;
}
GetUserCountFull() {
return this._users.length;
}
GetUsers() {
this._users;
}
GetUser(username) {
let existingUser = this._users.find(elem => elem.GetName() == username);
// Apparently this can fail somehow..?
if(existingUser === undefined)
return null;
return existingUser;
}
GetUsername() { return this._username; }
Rename(name) {
this._username = name;
this.SendGuacamoleMessage("rename", name);
}
GetVM() { return this._vm; }
ConnectToVM(vm) {
this._vm = vm;
this.SendGuacamoleMessage("connect", vm);
}
SendGuacamoleMessage() {
if(this._state !== ConnectionState.CONNECTED)
return;
this._ws.send(guacutils.encode(Array.prototype.slice.call(arguments)));
}
}

38
collabvm-anyosbot.service Normal file
View file

@ -0,0 +1,38 @@
[Unit]
Description=CollabVM AnyOS bot
Wants=collabvmts@vm7.service
#Wants=collabvm@vm8.service
After=network.target
[Service]
User=collabvm
Group=collabvm
Type=simple
WorkingDirectory=/srv/collabvm/anyos-bot-new-new
ExecStart=/bin/node index.js --max-old-space-size=1024 --use-largepages=on
# Hardening
PrivateTmp=yes
NoNewPrivileges=true
RestrictNamespaces=uts ipc pid user cgroup
# bleh
CPUQuota=50%
MemoryHigh=512M
MemoryMax=1G
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
PrivateDevices=yes
RestrictSUIDSGID=true
# avoids funny business
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target

583
index.js Normal file
View file

@ -0,0 +1,583 @@
import CollabVMClient from './client.js';
import {BANNED_ISO, ISO_DIRECTORIES, INSTALLBOT_VMS, BOT_PREFIX, ADMIN_TOKEN, kGeneralLimitBaseSeconds, kRebootLimitBaseSeconds} from './config.js';
function Log() {
// console.log(`[AnyOSBot] [${new Date()}]`, [...arguments].join(' '))
console.log('[AnyOSBot]', [...arguments].join(' '))
}
// you people SUCK man (dynamic edition, with less bugs!)
// and it actually probably works without hanging or negative seconds this time.
class RateLimit {
constructor(time, factor, ident) {
this._ident = ident;
this._timeBase = time;
this._msUntil = 0;
this._n = 1;
this._factor = factor;
}
GetTime() {
return this._msUntil;
}
IsLimited() {
return this._limited;
}
GetMs() {
let ret = Math.floor(
((this._n * (1/5)) * this._timeBase) * this._factor
);
// Make sure it's at least time base
if(ret < 1000)
ret = this._timeBase;
// Clean out fractional milliseconds
if((ret % 1000) != 0)
ret -= (ret % 1000);
//Log(`Debug: Ratelimit returns ${ret} (where N = ${this._n})`);
return ret;
}
SetUserCount(count) {
if(count == 0)
count = 1;
this._n = count;
//Log(`Debug: Ratelimit \"${this._ident}\" count set to ${count}, algo says time will be: ${this.GetMs()} (${this.GetMs() / 1000} seconds)`);
}
StartLimit() {
// TODO: this might work for the dyna ratelimit?
if(this._limited)
return;
let self = this;
self._msUntil = this.GetMs();
Log(`Ratelimit \"${this._ident}\" started, will be done in ${self._msUntil} ms (${self._msUntil / 1000} seconds)`);
self._limited = true;
this._siHandle = setInterval(() => {
self._msUntil -= 1000;
if(self._msUntil <= 0) {
Log(`Ratelimit \"${self._ident}\" is done.`)
clearInterval(self._siHandle);
self._limited = false;
return;
}
}, 1000);
}
ToString() {
const time = Math.floor(this.GetTime() / 1000);
let second_or_seconds = () => {
if(time > 1 || time === 0)
return "seconds";
return "second";
};
return `${time} ${second_or_seconds()}`;
}
}
class HelperBot extends CollabVMClient {
constructor(wsUri, vmId, ide2, floppy) {
super();
this._wsUri = wsUri;
this._vmId = vmId;
this._ide2 = ide2;
this._hasFloppy = floppy;
}
DoConn() {
Log(`[${this._vmId}]`, `Connecting to ${this._wsUri}`);
this.Connect(this._wsUri);
}
OnOpen() {
Log(`[${this._vmId}]`, `Bot connected to CollabVM server`);
this.CreateRateLimits();
// This dummy rename is required for some dumb reason..
this.Rename("fucker google");
this.ConnectToVM(this._vmId);
}
OnClose(ev) {
//console.log(arguments)
// reconnect lol
/* the right way doesnt work thanks to something
Log(`[${this._vmId}]`, `Connection closed, reconnecting in 5 seconds`);
let self = this;
setTimeout(() => {
Log(`[${this._vmId}]`, `Reconnecting now`);
self.DoConn();
}, 1000 * 5)
*/
Log(`[${this._vmId}]`, `Connection closed, exiting process`);
process.exit(0);
// The bot should probably give up after some attempts
}
Chat(message) {
this.SendGuacamoleMessage("chat", message);
}
OnAddUser_Bot(count) {
this.GeneralCmdLimit.SetUserCount(count);
this.RebootLimit.SetUserCount(count);
}
OnRemUser_Bot(count) {
this.GeneralCmdLimit.SetUserCount(count);
this.RebootLimit.SetUserCount(count);
}
UserCanBypass(username) {
let existingUser = this.GetUser(username);
// Apparently this can fail somehow..?
if(existingUser == null)
return false;
let rank = existingUser.GetRank();
return rank == 2 || rank == 3;
}
SendMonitorCommand(cmd) {
this.SendGuacamoleMessage("admin", "5", this.GetVM(), cmd);
}
ConcatPath(isodir, otherPath) {
let isopath = ISO_DIRECTORIES[isodir];
if (isopath === undefined) {
// imo crashing and being restarted by systemd is better than not knowing why shit was coming out as undefined
// gotta love javascript
throw new Error(`Undefined iso directory ${isodir}`);
}
return `${isopath}/${otherPath}`;
}
// QEMU Abstractions
QemuEjectDevice(devname) {
this.SendMonitorCommand(`eject ${devname}`);
}
QemuChangeDevice(devname, source, opts) {
this.SendMonitorCommand(`change ${devname} "${source}" ${opts}`);
}
QemuEjectCd() {
if(this._ide2)
this.QemuEjectDevice("ide2-cd0");
else
this.QemuEjectDevice("vm.cd");
}
QemuEjectFloppy() {
if(this._hasFloppy)
this.QemuEjectDevice("vm.floppy");
}
QemuChangeCd(source, opts) {
if(this._ide2)
this.QemuChangeDevice("ide2-cd0", source, opts);
else
this.QemuChangeDevice("vm.cd", source, opts);
}
QemuChangeFloppy(source) {
if(this._hasFloppy)
this.QemuChangeDevice("vm.floppy" , source, "raw read-only");
}
OnChat(username, message) {
//console.log(`${username}> ${message}`);
if(username == this.GetUsername())
return;
if(message[0] === BOT_PREFIX) {
this.HandleCommands(username, message);
}
}
HandleCommands(username, message) {
{
let user = this.GetUser(username);
if(user == null) {
Log(`[${this._vmId}]`, `I don't know about user \"${username}\" `);
process.exit(0);
return;
}
// This should disallow unregistered users,
// please don't fuck the rank up to where I can't do this
if(!(user.GetRank() >= 1)) {
return;
}
}
// Little code fragment to make rate limiting
// more portable.
const DoLimit = (limit) => {
//console.log(`[AnyOSBot] [${this._vmId}] ${this.GetUserCount()} users online (${this.GetUserCountFull()} actual)`)
//if(this._vmId !== 'vm0b0t') {
//
if(limit.IsLimited() && !this.UserCanBypass(username)) {
this.Chat(`You may not use commands yet. Please wait ${limit.ToString()}.`);
return false;
}
//}
//console.log(`[AnyOSBot] [${this._vmId}] ${new Date()} ${username} executed command ${command}`)
//Log(`[${this._vmId}]`, `${username} executed \"!${command}\"`);
Log(`[${this._vmId}]`, `${username} executed \"${message}\"`);
if(!this.UserCanBypass(username)) {
limit.StartLimit();
}
return true;
}
// generate a help HTML string for the help
const generateHelp = (arr) => {
let str = "<h4>AnyOSInstallBot Help:</h4><ul>";
for(var cmd of arr) {
// remove commands which depend on the floppy on a VM without floppy
if(cmd.usesFloppy && !this._hasFloppy)
continue;
str += `<li><b>${BOT_PREFIX}${cmd.command}</b> - ${cmd.help}</li>`;
}
str += "</ul>";
return str;
}
const generateList = (title, arr) => {
let str = `<h4>${title}</h4><ul>`;
for(var cmd of arr) {
str += `<li>${cmd}</li>`;
}
str += "</ul>";
return str;
}
let command = '';
if(message.indexOf(' ') !== -1)
command = message.slice(1, message.indexOf(' '));
else
command = message.slice(1);
switch(command) {
case 'help':
if(!DoLimit(this.GeneralCmdLimit))
return;
this.SendGuacamoleMessage("admin", "21", generateHelp([
{ command: "certerror", help: "Provides information on the CollabNet SSL certificate" },
{ command: "network", help: "Provides network driver information" },
{ command: "cd [path]", help: "Change CD image to Computernewb ISO image (see computernewb.com/isos)" },
{ command: "lilycd [path]", help: "Change CD image to Lily ISO image (see computernewb.com/~lily/ISOs)" },
{ command: "crustycd [path]", help: "Change CD image to CrustyWindows ISO image (see crustywindo.ws/collection)" },
{ command: "flp [path]", help: "Change Floppy image to Dartz IMG/flp image", usesFloppy: true },
{ command: "lilyflp [path]", help: "Change Floppy image to Lily IMG/flp image", usesFloppy: true },
{ command: "httpcd [URL]", help: "Change CD image to HTTP server ISO file. Whitelisted domains only (see computernewb.com/CHOCOLATEMAN/domains.txt for a list)" },
{ command: "eject [cd/flp]", help: "Ejects media from the specified drive." },
{ command: "reboot", help: "Reboot the VM. Has a larger cooldown, so don't be a retard with it." },
{ command: "bootset [string of c,a,d,n]", help: "Change the VM's boot order" },
]));
return;
break;
case 'network':
if(!DoLimit(this.GeneralCmdLimit))
return;
switch(this._vmId) {
case "vm7":
this.SendGuacamoleMessage("admin", "21", generateList("VirtIO Network Setup Instructions (Windows):", [
"Run \"!cd driver/virtio-win-0.1.225.iso\" to insert the VirtIO driver CD into the VM.",
"Run \"devmgmt.msc\" in the VM, look for the \"Ethernet Controller\" device, and update its driver.",
"When asked for a path, put in D:\\NetKVM\\{OS}\\x86 (or on a 64-bit OS, D:\\NetKVM\\{OS}\\amd64)",
"After that, see the !certerror command."
]));
break;
case "vm8":
this.SendGuacamoleMessage("admin", "21", generateList("RTL8139 Network Setup Instructions (Windows):", [
"Run \"!cd Driver/VM8 Network Drivers.iso\" to insert the RTL8139 network driver CD into the VM.",
"Run \"devmgmt.msc\" in the VM, look for the \"Ethernet Controller\" device, and update its driver.",
"When asked for a path, put in D:\\(name of OS)\\. For example, on Windows NT 4, put in D:\\WINNT4\\ (click browse and select the OS if you can't figure it out)",
"After that, you should be online. This step should NOT be required on Windows 2000, XP, Vista, or 7. This step should not be required on ANY Linux distro unless its really shit."
]));
break;
default:
this.chat("This VM should already have internet.");
break;
}
break;
case 'certerror':
if(!DoLimit(this.GeneralCmdLimit))
return;
//if(this._vmId == 'vm0b0t') {
// this.SendGuacamoleMessage("admin", "21", "<h1>fuck off retard why are you trying to get collabnet on vm0</h1>");
// return;
//}
this.SendGuacamoleMessage("admin", "21", generateList("CollabNet Setup Instructions:", [
"Follow the instructions on http://192.168.1.1 to install the certificate."
]));
break;
// this is gigantic holy fuck
case 'cd':
case 'lilycd':
case 'crustycd':
case 'flp':
case 'lilyflp':
case 'httpcd':
case 'httpflp': {
if(!DoLimit(this.GeneralCmdLimit))
return;
let arg = message.slice(message.indexOf(' ')+1);
let ext = arg.slice(arg.lastIndexOf('.') + 1);
if(arg.indexOf('..') !== -1)
return;
for (var ii = 0; ii < BANNED_ISO.length; ii++) {
if (BANNED_ISO[ii].test(arg)) {
this.Chat("That ISO is currently blacklisted.");
return;
}
}
switch(command) {
case 'cd':
case 'lilycd':
case 'crustycd':
if(arg.indexOf('http://') !== -1 || arg.indexOf('https://') != -1) {
this.Chat("Use the http versions of these commands, if the iso is locally hosted you can try !command Path/iso.iso (case-sensitive)")
return;
}
if(ext.toLowerCase() === "iso") {
// repetitive but whatever it works
if(command === 'lilycd')
this.QemuChangeCd(this.ConcatPath('lily', arg), "");
else if (command == 'crustycd')
this.QemuChangeCd(this.ConcatPath('crustywin', arg), "");
else if (command == 'cd')
this.QemuChangeCd(this.ConcatPath('computernewb', arg), "");
//this.QemuChangeCd(this.ConcatPath(command === 'lilycd' && 'lily' || command === "crustycd" && 'crustywin' || 'computernewb', arg), "");
}
break;
case 'flp':
case 'lilyflp':
if(arg.indexOf('http://') !== -1 || arg.indexOf('https://') != -1) {
this.Chat("Use the http versions of these commands")
return;
}
if(!this._hasFloppy) {
this.Chat("This VM does not have a floppy drive.");
return;
}
if(ext.toLowerCase() === "iso") {
this.Chat("dumbass, use !cd or !lilycd");
return;
}
if(ext.toLowerCase() === "img" || ext.toLowerCase() === "ima") {
this.QemuChangeFloppy(this.ConcatPath(command === 'lilyflp' ? 'lily' : 'computernewb', arg));
}
break;
case 'httpcd':
//this.Chat("Disabled due to retards, sorry! Try !cd, !lilycd or !crustycd for some local isos.");
//return;
// whitelisted domains
const whitelist = ['http://kernel.org', 'https://kernel.org', 'http://distro.ibiblio.org',
'https://distro.ibiblio.org', 'https://dl.collabsysos.xyz', 'https://egg.l5.ca',
'https://download.manjaro.org', 'https://ubuntu.osuosl.org/', 'https://mirror.kku.ac.th/',
'https://cdimage.debian.org', 'https://archive.elijahr.dev/Games/pcgc.iso' ];
/*var is_founded = false; // this might not be elegant sorry - Hilda
for (var piss in whitelist) {
//console.log(`${arg.indexOf(whitelist[piss])}`);
if (arg.startsWith(whitelist[piss]) === true) { // no archive.org/cock.iso?ignore=validurl
is_founded = true;
break;
}
}*/
// Yeah I knew it, thanks modeco! - Hilda
let is_founded = (() => whitelist.find(e => arg.startsWith(e)) !== undefined)();
if(is_founded === false)
{
//this.Chat("This is sparta!");
return;
}
/*
if(arg.indexOf('~dartz/isos') !== -1 || arg.indexOf('~lily/ISOs') !== -1) {
this.Chat('Use the non-http versions of these commands for local images, please.');
return;
}
*/
/*if(arg.indexOf('crustywindo.ws') !== -1) { // make this an else if or bundle in previous if if latter is added back
this.Chat("Use the !crustycd command for the bootleg collection, please.");
return;
}*/ // wait im retarded whitelist
if(ext.toLowerCase() === "iso")
this.QemuChangeCd(arg, "");
break;
case 'httpflp':
//this.Chat("Disabled due to retards; sorry!");
//return;
if(arg.indexOf('~dartz/isos') !== -1 || arg.indexOf('~lily/ISOs') !== -1) {
this.Chat('Use the non-http versions of these commands for local images, please.');
return;
}
if(!this._hasFloppy) {
this.Chat("This VM does not have a floppy drive.");
return;
}
if(ext.toLowerCase() == "img" || ext.toLowerCase() == "ima")
this.QemuChangeFloppy(arg);
break;
}
// bleh
this.Chat("Tried to put media into specified device.");
} break;
case 'eject': {
if(!DoLimit(this.GeneralCmdLimit))
return;
let arg = message.slice(message.indexOf(' ')+1);
//this.Chat("sorry, severe autism not allowed right now");
//return;
switch(arg) {
case 'cd':
this.QemuEjectCd();
break;
case 'flp':
this.QemuEjectFloppy();
break;
}
} break;
case 'reboot':
if(!DoLimit(this.RebootLimit))
return;
// this.Chat("hold on fellas");
// return;
this.SendMonitorCommand("system_reset");
break;
case 'bootset':
if(!DoLimit(this.GeneralCmdLimit))
return;
//this.Chat("sorry, severe autism not allowed right now");
//return;
this.SendMonitorCommand(`boot_set ${message.slice(message.indexOf(' ')+1)}`);
break;
default:
this.Chat(`Unknown command ${command}. See !help?`);
break;
}
}
CreateRateLimits() {
// Instanciate rate limit classes
// TODO: Should these be shared? idk
//if (vmId == 'vm0b0t')
// this.GeneralCmdLimit = new RateLimit(25 * 1000);
//else
this.GeneralCmdLimit = new RateLimit(kGeneralLimitBaseSeconds * 1000, 2, `General commands/${this._vmId}`);
//this.EjectLimit = new RateLimit(30 * 1000);
this.RebootLimit = new RateLimit(kRebootLimitBaseSeconds * 1000, 3, `!reboot/${this._vmId}`);
}
OnGuacamoleMessage(message) {
if(message[0] == "connect") {
if(message[1] == '0') {
Log(`[${this._vmId}]`, `Failed to connect to VM`);
this.Close();
return;
}
//console.log(message);
Log(`[${this._vmId}]`, `Connected to VM`);
// I'm fucking lazy
this.SendGuacamoleMessage("login", ADMIN_TOKEN);
this.CreateRateLimits();
}
// 5.admin,1.2,44.unknown command: &#x27;aaaaa&#x27;&#13;&#10;;
if(message[0] == "admin" && message[1] == "2") {
if(message[2].indexOf("Could not open") !== -1) {
this.Chat("Could not open selected CD or floppy image. Please check the filename");
}
if(message[2].indexOf("boot device list now set to") !== -1) {
this.Chat("Successfully set boot order.");
}
//console.log(`Admin response: ${message[2]}`);
}
}
}
for(let vm of INSTALLBOT_VMS) {
// initalize this bot instance
new HelperBot(vm.uri, vm.id, vm.usesIde2, vm.hasFloppy)
.DoConn();
}

12
package.json Normal file
View file

@ -0,0 +1,12 @@
{
"name": "AnyOSInstallBot",
"version": "1.0.0",
"description": "Helper bot to insert media into AnyOS VMs",
"main": "index.js",
"author": "Computernewb",
"license": "ISC",
"type": "module",
"dependencies": {
"ws": "^8.3.0"
}
}