Implement account authentication (server)
This commit is contained in:
@@ -13,6 +13,15 @@ originAllowedDomains = ["computernewb.com"]
|
|||||||
# Maximum amount of active connections allowed from the same IP.
|
# Maximum amount of active connections allowed from the same IP.
|
||||||
maxConnections = 3
|
maxConnections = 3
|
||||||
|
|
||||||
|
[auth]
|
||||||
|
enabled = false
|
||||||
|
apiEndpoint = ""
|
||||||
|
secretKey = "hunter2"
|
||||||
|
|
||||||
|
[auth.guestPermissions]
|
||||||
|
chat = true
|
||||||
|
turn = false
|
||||||
|
|
||||||
[vm]
|
[vm]
|
||||||
qemuArgs = "qemu-system-x86_64"
|
qemuArgs = "qemu-system-x86_64"
|
||||||
vncPort = 5900
|
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[];
|
originAllowedDomains : string[];
|
||||||
maxConnections: number;
|
maxConnections: number;
|
||||||
};
|
};
|
||||||
|
auth : {
|
||||||
|
enabled : boolean;
|
||||||
|
apiEndpoint : string;
|
||||||
|
secretKey : string;
|
||||||
|
guestPermissions : {
|
||||||
|
chat : boolean;
|
||||||
|
turn : boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
vm : {
|
vm : {
|
||||||
qemuArgs : string;
|
qemuArgs : string;
|
||||||
vncPort : number;
|
vncPort : number;
|
||||||
|
|||||||
@@ -141,6 +141,8 @@ export class User {
|
|||||||
|
|
||||||
export enum Rank {
|
export enum Rank {
|
||||||
Unregistered = 0,
|
Unregistered = 0,
|
||||||
|
// After all these years
|
||||||
|
Registered = 1,
|
||||||
Admin = 2,
|
Admin = 2,
|
||||||
Moderator = 3,
|
Moderator = 3,
|
||||||
// Giving a good gap between server only internal ranks just in case
|
// 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 VM from './VM.js';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import AuthManager from './AuthManager.js';
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
@@ -54,7 +55,11 @@ export default class WSServer {
|
|||||||
private indefiniteTurn : User | null;
|
private indefiniteTurn : User | null;
|
||||||
private ModPerms : number;
|
private ModPerms : number;
|
||||||
private VM : VM;
|
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.Config = config;
|
||||||
this.ChatHistory = new CircularBuffer<{user:string,msg:string}>(Array, this.Config.collabvm.maxChatHistoryLength);
|
this.ChatHistory = new CircularBuffer<{user:string,msg:string}>(Array, this.Config.collabvm.maxChatHistoryLength);
|
||||||
this.TurnQueue = new Queue<User>();
|
this.TurnQueue = new Queue<User>();
|
||||||
@@ -84,6 +89,8 @@ export default class WSServer {
|
|||||||
this.VM = vm;
|
this.VM = vm;
|
||||||
this.VM.on("dirtyrect", (j, x, y) => this.newrect(j, x, y));
|
this.VM.on("dirtyrect", (j, x, y) => this.newrect(j, x, y));
|
||||||
this.VM.on("size", (s) => this.newsize(s));
|
this.VM.on("size", (s) => this.newsize(s));
|
||||||
|
// authentication manager
|
||||||
|
this.auth = auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
listen() {
|
listen() {
|
||||||
@@ -216,6 +223,7 @@ export default class WSServer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private connectionClosed(user : User) {
|
private connectionClosed(user : User) {
|
||||||
|
if (this.clients.indexOf(user) === -1) return;
|
||||||
if(user.IP.vote != null) {
|
if(user.IP.vote != null) {
|
||||||
user.IP.vote = null;
|
user.IP.vote = null;
|
||||||
this.sendVoteUpdate();
|
this.sendVoteUpdate();
|
||||||
@@ -236,6 +244,36 @@ export default class WSServer {
|
|||||||
var msgArr = guacutils.decode(message);
|
var msgArr = guacutils.decode(message);
|
||||||
if (msgArr.length < 1) return;
|
if (msgArr.length < 1) return;
|
||||||
switch (msgArr[0]) {
|
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":
|
case "list":
|
||||||
client.sendMsg(guacutils.encode("list", this.Config.collabvm.node, this.Config.collabvm.displayname, this.screenHidden ? this.screenHiddenThumb : await this.getThumbnail()));
|
client.sendMsg(guacutils.encode("list", this.Config.collabvm.node, this.Config.collabvm.displayname, this.screenHidden ? this.screenHiddenThumb : await this.getThumbnail()));
|
||||||
break;
|
break;
|
||||||
@@ -246,6 +284,9 @@ export default class WSServer {
|
|||||||
}
|
}
|
||||||
client.connectedToNode = true;
|
client.connectedToNode = true;
|
||||||
client.sendMsg(guacutils.encode("connect", "1", "1", this.Config.vm.snapshots ? "1" : "0", "0"));
|
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.ChatHistory.size !== 0) client.sendMsg(this.getChatHistoryMsg());
|
||||||
if (this.Config.collabvm.motd) client.sendMsg(guacutils.encode("chat", "", this.Config.collabvm.motd));
|
if (this.Config.collabvm.motd) client.sendMsg(guacutils.encode("chat", "", this.Config.collabvm.motd));
|
||||||
if (this.screenHidden) {
|
if (this.screenHidden) {
|
||||||
@@ -304,12 +345,26 @@ export default class WSServer {
|
|||||||
case "rename":
|
case "rename":
|
||||||
if (!client.RenameRateLimit.request()) return;
|
if (!client.RenameRateLimit.request()) return;
|
||||||
if (client.connectedToNode && client.IP.muted) 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]);
|
this.renameUser(client, msgArr[1]);
|
||||||
break;
|
break;
|
||||||
case "chat":
|
case "chat":
|
||||||
if (!client.username) return;
|
if (!client.username) return;
|
||||||
if (client.IP.muted) return;
|
if (client.IP.muted) return;
|
||||||
if (msgArr.length !== 2) 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]);
|
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
|
// 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);
|
if (msg.length > this.Config.collabvm.maxChatLength) msg = msg.substring(0, this.Config.collabvm.maxChatLength);
|
||||||
@@ -321,6 +376,10 @@ export default class WSServer {
|
|||||||
break;
|
break;
|
||||||
case "turn":
|
case "turn":
|
||||||
if ((!this.turnsAllowed || this.Config.collabvm.turnwhitelist) && client.rank !== Rank.Admin && client.rank !== Rank.Moderator && client.rank !== Rank.Turn) return;
|
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.TurnRateLimit.request()) return;
|
||||||
if (!client.connectedToNode) return;
|
if (!client.connectedToNode) return;
|
||||||
if (msgArr.length > 2) return;
|
if (msgArr.length > 2) return;
|
||||||
@@ -413,6 +472,10 @@ export default class WSServer {
|
|||||||
switch (msgArr[1]) {
|
switch (msgArr[1]) {
|
||||||
case "2":
|
case "2":
|
||||||
// Login
|
// 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 (!client.LoginRateLimit.request() || !client.username) return;
|
||||||
if (msgArr.length !== 3) return;
|
if (msgArr.length !== 3) return;
|
||||||
var sha256 = createHash("sha256");
|
var sha256 = createHash("sha256");
|
||||||
@@ -527,6 +590,9 @@ export default class WSServer {
|
|||||||
case "18":
|
case "18":
|
||||||
// Rename user
|
// Rename user
|
||||||
if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.rename)) return;
|
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;
|
if (msgArr.length !== 4) return;
|
||||||
var user = this.clients.find(c => c.username === msgArr[2]);
|
var user = this.clients.find(c => c.username === msgArr[2]);
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import * as fs from "fs";
|
|||||||
import WSServer from './WSServer.js';
|
import WSServer from './WSServer.js';
|
||||||
import QEMUVM from './QEMUVM.js';
|
import QEMUVM from './QEMUVM.js';
|
||||||
import log from './log.js';
|
import log from './log.js';
|
||||||
|
import AuthManager from './AuthManager.js';
|
||||||
|
|
||||||
log("INFO", "CollabVM Server starting up");
|
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.");
|
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
|
// Fire up the VM
|
||||||
var VM = new QEMUVM(Config);
|
var VM = new QEMUVM(Config);
|
||||||
await VM.Start();
|
await VM.Start();
|
||||||
|
|
||||||
// Start up the websocket server
|
// Start up the websocket server
|
||||||
var WS = new WSServer(Config, VM);
|
var WS = new WSServer(Config, VM, auth);
|
||||||
WS.listen();
|
WS.listen();
|
||||||
}
|
}
|
||||||
start();
|
start();
|
||||||
Reference in New Issue
Block a user