AnyOSInstallBotJS/src/index.ts

651 lines
19 KiB
TypeScript

import * as fs from "fs";
import * as path from "path";
import * as child_process from "child_process";
import CollabVMClient from "./client.js";
import Config from "./config.js";
let config: Config = JSON.parse(fs.readFileSync("config.json", "utf-8"));
if (!config.ADMIN_PASSWORD && !config.ADMIN_TOKEN) {
console.error("Either ADMIN_PASSWORD or ADMIN_TOKEN must be defined in config.json");
process.exit(1);
}
const blankflp = Buffer.alloc(1440000);
if (!fs.existsSync("media/flp/"))
fs.mkdirSync("media/flp/", { recursive: true });
if (!fs.existsSync("media/img/"))
fs.mkdirSync("media/img/", { recursive: true });
function Log(...args: string[]) {
console.log("[AnyOSBot]", args.join(" "));
}
interface HelpCommand {
command: string;
help: string;
usesFloppy?: boolean | undefined;
}
// you people SUCK man (dynamic edition, with less bugs!)
// and it actually probably works without hanging or negative seconds this time.
class RateLimit {
private _ident: string;
private _timeBase: number;
private _factor: number;
private _msUntil: number;
private _n: number;
private _limited: boolean;
private _siHandle: NodeJS.Timeout | null;
constructor(time: number, factor: number, ident: string) {
this._ident = ident;
this._timeBase = time;
this._msUntil = 0;
this._n = 1;
this._factor = factor;
this._limited = false;
this._siHandle = null;
}
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;
return ret;
}
SetUserCount(count: number) {
if (count == 0) count = 1;
this._n = count;
}
StartLimit() {
// TODO: this might work for the dyna ratelimit?
if (this._limited) return;
this._msUntil = this.GetMs();
Log(
`Ratelimit \"${this._ident}\" started, will be done in ${this._msUntil} ms (${this._msUntil / 1000} seconds)`,
);
this._limited = true;
this._siHandle = setInterval(() => {
this._msUntil -= 1000;
if (this._msUntil <= 0) {
Log(`Ratelimit \"${this._ident}\" is done.`);
if (this._siHandle !== null) clearInterval(this._siHandle);
this._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 {
private _wsUri: string;
private _vmId: string;
private _ide2: boolean;
private _hasFloppy: boolean;
private _hasUsb: boolean;
private GeneralCmdLimit: RateLimit | undefined;
private RebootLimit: RateLimit | undefined;
constructor(
wsUri: string,
vmId: string,
ide2: boolean,
floppy: boolean,
usb: boolean,
) {
super();
this._wsUri = wsUri;
this._vmId = vmId;
this._ide2 = ide2;
this._hasFloppy = floppy;
this._hasUsb = usb;
}
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() {
Log(`[${this._vmId}]`, `Connection closed, exiting process`);
process.exit(0);
}
Chat(message: string) {
this.SendGuacamoleMessage("chat", message);
}
OnAddUser_Bot(count: number) {
this.GeneralCmdLimit!.SetUserCount(count);
this.RebootLimit!.SetUserCount(count);
}
OnRemUser_Bot(count: number) {
this.GeneralCmdLimit!.SetUserCount(count);
this.RebootLimit!.SetUserCount(count);
}
UserCanBypass(username: string) {
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: string) {
this.SendGuacamoleMessage("admin", "5", this.GetVM()!, cmd);
}
ConcatPath(isodir: string, otherPath: string) {
let isopath = config.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: string) {
this.SendMonitorCommand(`eject ${devname}`);
}
QemuChangeDevice(devname: string, source: string, opts: string) {
console.log(`change ${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");
}
QemuRemoveUSB() {
if (this._hasUsb) this.QemuEjectDevice("vm.usbstorage");
}
QemuChangeCd(source: string, opts: string) {
if (this._ide2) this.QemuChangeDevice("ide2-cd0", source, opts);
else this.QemuChangeDevice("vm.cd", source, opts);
}
QemuChangeFloppy(source: string, readOnly: boolean = true) {
let opts = "raw";
if (readOnly) opts += " read-only";
else opts += " read-write";
if (this._hasFloppy) this.QemuChangeDevice("vm.floppy", source, opts);
}
QemuChangeUSB(source: string) {
this.QemuChangeDevice("vm.usbstorage", source, "");
}
OnChat(username: string, message: string) {
if (username == this.GetUsername()) return;
if (message[0] === config.BOT_PREFIX) {
this.HandleCommands(username, message.replaceAll("\\", "\\\\")); // thanks js
}
}
HandleCommands(username: string, message: string) {
{
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
// hack lol
if (config.ADMIN_TOKEN && user.GetRank() === 0) {
return;
}
}
// Little code fragment to make rate limiting
// more portable.
const DoLimit = (limit: RateLimit) => {
if (limit.IsLimited() && !this.UserCanBypass(username)) {
this.Chat(
`You may not use commands yet. Please wait ${limit.ToString()}.`,
);
return false;
}
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: HelpCommand[]) => {
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>${config.BOT_PREFIX}${cmd.command}</b> - ${cmd.help}</li>`;
}
str += "</ul>";
return str;
};
const generateList = (title: string, arr: string[]) => {
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: "myfloppy",
help: "Insert and/or create your personal floppy disk",
usesFloppy: true,
},
{
command: "myusb",
help: "Insert and/or create your personal 100MB USB Drive",
},
{
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/usb]",
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;
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;
if (config.BANNED_ISO.some((b) => b.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), "");
}
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":
// 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",
];
let is_founded = (() =>
whitelist.find((e) => arg.startsWith(e)) !== undefined)();
if (is_founded === false) {
return;
}
if (ext.toLowerCase() === "iso") this.QemuChangeCd(arg, "");
break;
case "httpflp":
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);
switch (arg) {
case "cd":
this.QemuEjectCd();
break;
case "flp":
this.QemuEjectFloppy();
break;
case "usb":
this.QemuRemoveUSB();
break;
}
}
break;
case "reboot":
if (!DoLimit(this.RebootLimit!)) return;
this.SendMonitorCommand("system_reset");
break;
case "bootset":
if (!DoLimit(this.GeneralCmdLimit!)) return;
this.SendMonitorCommand(
`boot_set ${message.slice(message.indexOf(" ") + 1)}`,
);
break;
case "myfloppy": {
if (!DoLimit(this.GeneralCmdLimit!)) return;
if (!this._hasFloppy) {
this.Chat("This VM does not have a floppy drive.");
return;
}
let flppath = path.resolve(`media/flp/${username}.img`);
if (!fs.existsSync(flppath)) {
fs.writeFileSync(flppath, blankflp);
}
this.QemuChangeFloppy(flppath, false);
this.Chat("Tried to put media into specified device.");
break;
}
case "myusb": {
if (!DoLimit(this.GeneralCmdLimit!)) return;
if (!this._hasUsb) {
this.Chat("This VM does not have a USB drive.");
return;
}
let imgpath = path.resolve(`media/img/${username}.qcow2`);
if (!fs.existsSync(imgpath)) {
child_process.execSync(`qemu-img create -f qcow2 "${imgpath}" 100M`);
}
this.QemuChangeUSB(imgpath);
this.Chat("Tried to put media into specified device.");
break;
}
default:
this.Chat(`Unknown command ${command}. See !help?`);
break;
}
}
CreateRateLimits() {
// Instanciate rate limit classes
// TODO: Should these be shared? idk
this.GeneralCmdLimit = new RateLimit(
config.kGeneralLimitBaseSeconds * 1000,
2,
`General commands/${this._vmId}`,
);
this.RebootLimit = new RateLimit(
config.kRebootLimitBaseSeconds * 1000,
3,
`!reboot/${this._vmId}`,
);
}
OnGuacamoleMessage(message: string[]) {
if (message[0] == "connect") {
if (message[1] == "0") {
Log(`[${this._vmId}]`, `Failed to connect to VM`);
this.Close();
return;
}
Log(`[${this._vmId}]`, `Connected to VM`);
if (config.ADMIN_PASSWORD) {
this.SendGuacamoleMessage("admin", "2", config.ADMIN_PASSWORD);
} else if (config.ADMIN_TOKEN) {
this.SendGuacamoleMessage("login", config.ADMIN_TOKEN);
}
this.CreateRateLimits();
}
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.");
}
}
}
}
for (let vm of config.INSTALLBOT_VMS) {
// initalize this bot instance
new HelperBot(vm.uri, vm.id, vm.usesIde2, vm.hasFloppy, vm.hasUsb).DoConn();
}