mirror of
https://github.com/computernewb/collabvm-1.2.ts.git
synced 2025-01-22 10:42:00 -05:00
Implement account authentication (server)
This commit is contained in:
parent
b815bf8874
commit
f89492cfcd
6 changed files with 133 additions and 2 deletions
|
@ -13,6 +13,15 @@ originAllowedDomains = ["computernewb.com"]
|
|||
# Maximum amount of active connections allowed from the same IP.
|
||||
maxConnections = 3
|
||||
|
||||
[auth]
|
||||
enabled = false
|
||||
apiEndpoint = ""
|
||||
secretKey = "hunter2"
|
||||
|
||||
[auth.guestPermissions]
|
||||
chat = true
|
||||
turn = false
|
||||
|
||||
[vm]
|
||||
qemuArgs = "qemu-system-x86_64"
|
||||
vncPort = 5900
|
||||
|
|
41
src/AuthManager.ts
Normal file
41
src/AuthManager.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { Rank, User } from "./User.js";
|
||||
import log from "./log.js";
|
||||
|
||||
export default class AuthManager {
|
||||
apiEndpoint : string;
|
||||
secretKey : string;
|
||||
constructor(apiEndpoint : string, secretKey : string) {
|
||||
this.apiEndpoint = apiEndpoint;
|
||||
this.secretKey = secretKey;
|
||||
}
|
||||
|
||||
Authenticate(token : string, user : User) {
|
||||
return new Promise<JoinResponse>(async res => {
|
||||
var response = await fetch(this.apiEndpoint + "/api/v1/join", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
secretKey: this.secretKey,
|
||||
sessionToken: token,
|
||||
ip: user.IP.address
|
||||
})
|
||||
});
|
||||
var json = await response.json() as JoinResponse;
|
||||
if (!json.success) {
|
||||
log("FATAL", `Failed to query auth server: ${json.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
res(json);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface JoinResponse {
|
||||
success : boolean;
|
||||
clientSuccess : boolean;
|
||||
error : string | undefined;
|
||||
username : string | undefined;
|
||||
rank : Rank;
|
||||
}
|
|
@ -8,6 +8,15 @@ export default interface IConfig {
|
|||
originAllowedDomains : string[];
|
||||
maxConnections: number;
|
||||
};
|
||||
auth : {
|
||||
enabled : boolean;
|
||||
apiEndpoint : string;
|
||||
secretKey : string;
|
||||
guestPermissions : {
|
||||
chat : boolean;
|
||||
turn : boolean;
|
||||
}
|
||||
}
|
||||
vm : {
|
||||
qemuArgs : string;
|
||||
vncPort : number;
|
||||
|
|
|
@ -141,6 +141,8 @@ export class User {
|
|||
|
||||
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
|
||||
|
|
|
@ -18,6 +18,7 @@ import log from './log.js';
|
|||
import VM from './VM.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from 'path';
|
||||
import AuthManager from './AuthManager.js';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
|
@ -54,7 +55,11 @@ export default class WSServer {
|
|||
private indefiniteTurn : User | null;
|
||||
private ModPerms : number;
|
||||
private VM : VM;
|
||||
constructor(config : IConfig, vm : VM) {
|
||||
|
||||
// Authentication manager
|
||||
private auth : AuthManager | null;
|
||||
|
||||
constructor(config : IConfig, vm : VM, auth : AuthManager | null) {
|
||||
this.Config = config;
|
||||
this.ChatHistory = new CircularBuffer<{user:string,msg:string}>(Array, this.Config.collabvm.maxChatHistoryLength);
|
||||
this.TurnQueue = new Queue<User>();
|
||||
|
@ -84,6 +89,8 @@ export default class WSServer {
|
|||
this.VM = vm;
|
||||
this.VM.on("dirtyrect", (j, x, y) => this.newrect(j, x, y));
|
||||
this.VM.on("size", (s) => this.newsize(s));
|
||||
// authentication manager
|
||||
this.auth = auth;
|
||||
}
|
||||
|
||||
listen() {
|
||||
|
@ -216,6 +223,7 @@ export default class WSServer {
|
|||
};
|
||||
|
||||
private connectionClosed(user : User) {
|
||||
if (this.clients.indexOf(user) === -1) return;
|
||||
if(user.IP.vote != null) {
|
||||
user.IP.vote = null;
|
||||
this.sendVoteUpdate();
|
||||
|
@ -236,6 +244,36 @@ export default class WSServer {
|
|||
var msgArr = guacutils.decode(message);
|
||||
if (msgArr.length < 1) return;
|
||||
switch (msgArr[0]) {
|
||||
case "login":
|
||||
if (msgArr.length !== 2 || !this.Config.auth.enabled) return;
|
||||
var res = await this.auth!.Authenticate(msgArr[1], client);
|
||||
if (res.clientSuccess) {
|
||||
log("INFO", `${client.IP.address} logged in as ${res.username}`);
|
||||
client.sendMsg(guacutils.encode("login", "1"));
|
||||
var old = this.clients.find(c=>c.username === res.username);
|
||||
if (old) {
|
||||
// kick() doesnt wait until the user is actually removed from the list and itd be anal to make it do that
|
||||
// so we call connectionClosed manually here. When it gets called on kick(), it will return because the user isn't in the list
|
||||
this.connectionClosed(old);
|
||||
await old.kick();
|
||||
}
|
||||
// Set username
|
||||
this.renameUser(client, res.username);
|
||||
// Set rank
|
||||
client.rank = res.rank;
|
||||
if (client.rank === Rank.Admin) {
|
||||
client.sendMsg(guacutils.encode("admin", "0", "1"));
|
||||
} else if (client.rank === Rank.Moderator) {
|
||||
client.sendMsg(guacutils.encode("admin", "0", "3", this.ModPerms.toString()));
|
||||
}
|
||||
this.clients.forEach((c) => c.sendMsg(guacutils.encode("adduser", "1", client.username!, client.rank.toString())));
|
||||
} else {
|
||||
client.sendMsg(guacutils.encode("login", "0", res.error!));
|
||||
if (res.error === "You are banned") {
|
||||
client.kick();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "list":
|
||||
client.sendMsg(guacutils.encode("list", this.Config.collabvm.node, this.Config.collabvm.displayname, this.screenHidden ? this.screenHiddenThumb : await this.getThumbnail()));
|
||||
break;
|
||||
|
@ -246,6 +284,9 @@ export default class WSServer {
|
|||
}
|
||||
client.connectedToNode = true;
|
||||
client.sendMsg(guacutils.encode("connect", "1", "1", this.Config.vm.snapshots ? "1" : "0", "0"));
|
||||
if (this.Config.auth.enabled) {
|
||||
client.sendMsg(guacutils.encode("auth", this.Config.auth.apiEndpoint));
|
||||
}
|
||||
if (this.ChatHistory.size !== 0) client.sendMsg(this.getChatHistoryMsg());
|
||||
if (this.Config.collabvm.motd) client.sendMsg(guacutils.encode("chat", "", this.Config.collabvm.motd));
|
||||
if (this.screenHidden) {
|
||||
|
@ -304,12 +345,26 @@ export default class WSServer {
|
|||
case "rename":
|
||||
if (!client.RenameRateLimit.request()) return;
|
||||
if (client.connectedToNode && client.IP.muted) return;
|
||||
if (this.Config.auth.enabled && client.rank !== Rank.Unregistered) {
|
||||
client.sendMsg(guacutils.encode("chat", "", "Go to your account settings to change your username."));
|
||||
return;
|
||||
}
|
||||
if (this.Config.auth.enabled && msgArr[1] !== undefined) {
|
||||
client.sendMsg(guacutils.encode("chat", "", "You need to log in to do that."));
|
||||
if (client.rank !== Rank.Unregistered) return;
|
||||
this.renameUser(client, undefined);
|
||||
return;
|
||||
}
|
||||
this.renameUser(client, msgArr[1]);
|
||||
break;
|
||||
case "chat":
|
||||
if (!client.username) return;
|
||||
if (client.IP.muted) return;
|
||||
if (msgArr.length !== 2) return;
|
||||
if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.chat) {
|
||||
client.sendMsg(guacutils.encode("chat", "", "You need to login to do that."));
|
||||
return;
|
||||
}
|
||||
var msg = Utilities.HTMLSanitize(msgArr[1]);
|
||||
// One of the things I hated most about the old server is it completely discarded your message if it was too long
|
||||
if (msg.length > this.Config.collabvm.maxChatLength) msg = msg.substring(0, this.Config.collabvm.maxChatLength);
|
||||
|
@ -321,6 +376,10 @@ export default class WSServer {
|
|||
break;
|
||||
case "turn":
|
||||
if ((!this.turnsAllowed || this.Config.collabvm.turnwhitelist) && client.rank !== Rank.Admin && client.rank !== Rank.Moderator && client.rank !== Rank.Turn) return;
|
||||
if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.turn) {
|
||||
client.sendMsg(guacutils.encode("chat", "", "You need to login to do that."));
|
||||
return;
|
||||
}
|
||||
if (!client.TurnRateLimit.request()) return;
|
||||
if (!client.connectedToNode) return;
|
||||
if (msgArr.length > 2) return;
|
||||
|
@ -413,6 +472,10 @@ export default class WSServer {
|
|||
switch (msgArr[1]) {
|
||||
case "2":
|
||||
// Login
|
||||
if (this.Config.auth.enabled) {
|
||||
client.sendMsg(guacutils.encode("chat", "", "This server does not support staff passwords. Please log in to become staff."));
|
||||
return;
|
||||
}
|
||||
if (!client.LoginRateLimit.request() || !client.username) return;
|
||||
if (msgArr.length !== 3) return;
|
||||
var sha256 = createHash("sha256");
|
||||
|
@ -527,6 +590,9 @@ export default class WSServer {
|
|||
case "18":
|
||||
// Rename user
|
||||
if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.rename)) return;
|
||||
if (this.Config.auth.enabled) {
|
||||
client.sendMsg(guacutils.encode("chat", "", "Cannot rename users on a server that uses authentication."));
|
||||
}
|
||||
if (msgArr.length !== 4) return;
|
||||
var user = this.clients.find(c => c.username === msgArr[2]);
|
||||
if (!user) return;
|
||||
|
|
|
@ -4,6 +4,7 @@ import * as fs from "fs";
|
|||
import WSServer from './WSServer.js';
|
||||
import QEMUVM from './QEMUVM.js';
|
||||
import log from './log.js';
|
||||
import AuthManager from './AuthManager.js';
|
||||
|
||||
log("INFO", "CollabVM Server starting up");
|
||||
|
||||
|
@ -34,12 +35,15 @@ async function start() {
|
|||
log("WARN", "To remove this warning, use the qmpHost and qmpPort options instead.");
|
||||
}
|
||||
|
||||
// Init the auth manager if enabled
|
||||
var auth = Config.auth.enabled ? new AuthManager(Config.auth.apiEndpoint, Config.auth.secretKey) : null;
|
||||
|
||||
// Fire up the VM
|
||||
var VM = new QEMUVM(Config);
|
||||
await VM.Start();
|
||||
|
||||
// Start up the websocket server
|
||||
var WS = new WSServer(Config, VM);
|
||||
var WS = new WSServer(Config, VM, auth);
|
||||
WS.listen();
|
||||
}
|
||||
start();
|
Loading…
Reference in a new issue