From df094f63d5054e7aa14df72909e040d00b01c255 Mon Sep 17 00:00:00 2001 From: Elijah R Date: Sat, 13 Apr 2024 12:32:34 -0400 Subject: [PATCH] implement auth and make a few minor changed --- collab-vm-server-1.3/AuthManager.cs | 41 +++++++++++++ collab-vm-server-1.3/IConfig.cs | 16 +++++ collab-vm-server-1.3/Program.cs | 7 +++ collab-vm-server-1.3/Rank.cs | 1 + collab-vm-server-1.3/User.cs | 92 ++++++++++++++++++++++++++++- collab-vm-server-1.3/Utilities.cs | 2 +- collab-vm-server-1.3/VM.cs | 8 +-- config.example.toml | 10 ++++ 8 files changed, 169 insertions(+), 8 deletions(-) create mode 100644 collab-vm-server-1.3/AuthManager.cs diff --git a/collab-vm-server-1.3/AuthManager.cs b/collab-vm-server-1.3/AuthManager.cs new file mode 100644 index 0000000..628e156 --- /dev/null +++ b/collab-vm-server-1.3/AuthManager.cs @@ -0,0 +1,41 @@ +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; +using CollabVM.Server.Config; + +namespace CollabVM.Server; + +public class AuthManager +{ + private readonly AuthConfig config; + + public AuthManager(AuthConfig config) + { + this.config = config; + } + + public async Task AuthenticateUser(string token, User user) + { + using var http = new HttpClient(); + var body = new StringContent( + JsonSerializer.Serialize(new + { + secretKey = this.config.SecretKey, + sessionToken = token, + ip = user.IP.ToString(), + }), Encoding.UTF8, "application/json"); + body.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + var response = await http.PostAsync(this.config.AuthServerURI + "/api/v1/join", body); + return await response.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to parse JSON response from auth server."); + } +} + +public class JoinResponse +{ + public bool success { get; set; } + public bool clientSuccess { get; set; } = false; + public string? error { get; set; } + public string? username { get; set; } + public Rank? rank { get; set; } +} \ No newline at end of file diff --git a/collab-vm-server-1.3/IConfig.cs b/collab-vm-server-1.3/IConfig.cs index cd0fa6d..f05b207 100644 --- a/collab-vm-server-1.3/IConfig.cs +++ b/collab-vm-server-1.3/IConfig.cs @@ -3,6 +3,7 @@ namespace CollabVM.Server.Config; public class IConfig { public HTTPConfig HTTP { get; set; } + public AuthConfig Auth { get; set; } public TurnConfig Turns { get; set; } public VoteConfig Votes { get; set; } public ChatConfig Chat { get; set; } @@ -118,4 +119,19 @@ public class QEMUVMConfig public int QMPPort { get; set; } public string? QMPSocketDir { get; set; } public bool Snapshots { get; set; } +} + +public class AuthConfig +{ + public bool Enabled { get; set; } + public string? AuthServerURI { get; set; } + public string? SecretKey { get; set; } + public AuthConfigGuestPermissions GuestPermissions { get; set; } +} + +public class AuthConfigGuestPermissions +{ + public bool Chat { get; set; } + public bool Turns { get; set; } + public bool Vote { get; set; } } \ No newline at end of file diff --git a/collab-vm-server-1.3/Program.cs b/collab-vm-server-1.3/Program.cs index e9880b5..c770b0a 100644 --- a/collab-vm-server-1.3/Program.cs +++ b/collab-vm-server-1.3/Program.cs @@ -17,6 +17,7 @@ class Program public static Random rnd = new(); public static IConfig Config; public static Database? Database; + public static AuthManager AuthManager; // These can go here for now, might move them later public static readonly string ScreenHiddenBase64 = Convert.ToBase64String(Utilities.GetAsset("screenhidden.jpeg")); public static readonly byte[] ScreenHiddenThumb = Utilities.GetAsset("screenhiddenthumb.jpeg"); @@ -56,6 +57,12 @@ class Program await Database.OpenAsync(); Utilities.Log(LogLevel.INFO, "Connected to MySQL Database."); } + // Initialize auth + if (Config.Auth.Enabled) + { + AuthManager = new AuthManager(Config.Auth); + Utilities.Log(LogLevel.INFO, $"Account authentication enabled (Server: {Config.Auth.AuthServerURI})"); + } // Initialize the VMs VMManager = new VMManager(Config.VMs); await VMManager.StartAll(); diff --git a/collab-vm-server-1.3/Rank.cs b/collab-vm-server-1.3/Rank.cs index f510dfb..c01e864 100644 --- a/collab-vm-server-1.3/Rank.cs +++ b/collab-vm-server-1.3/Rank.cs @@ -3,6 +3,7 @@ namespace CollabVM.Server; public enum Rank { Unregistered = 0, + Registered = 1, Admin = 2, Moderator = 3, } \ No newline at end of file diff --git a/collab-vm-server-1.3/User.cs b/collab-vm-server-1.3/User.cs index a68baef..0e0e674 100644 --- a/collab-vm-server-1.3/User.cs +++ b/collab-vm-server-1.3/User.cs @@ -64,6 +64,7 @@ public class User this.Disconnected += OnDisconnected; this.ConnectedToVM += delegate { }; this.Renamed += delegate { }; + if (Program.Config.Auth.Enabled) SendAsync(Guacutils.Encode("auth", Program.Config.Auth.AuthServerURI!)); SendAsync("3.nop;"); ReadLoop(tokenSource.Token); } @@ -122,6 +123,25 @@ public class User break; case "rename": { + if (Program.Config.Auth.Enabled) + { + if (_rank != Rank.Unregistered) + { + await SendChat("", "Go to your account settings to change your username."); + return; + } else if (msgArr.Length != 1) + { + // Don't send an error message if the user doesn't yet have a username, since it was probably an automated attempt by the client + if (this._username != null) + { + await this.SendChat("", "You need to log in to do that."); + return; + } + // If the user doesn't have a username, assign them a guest username + await this.AssignGuestUsername(this.vm); + return; + } + } if (msgArr.Length == 1) await this.AssignGuestUsername(this.vm); else @@ -177,10 +197,10 @@ public class User chatmsg.Add(vm.Config.MOTD); await this.SendAsync(Guacutils.Encode(chatmsg.ToArray())); var turn = vm.TurnQueue.CurrentTurn(); - if (turn.Queue.Length > 0) await SendTurnUpdate(turn); + await SendTurnUpdate(turn); if (vm.Vote != null) await SendVoteUpdate(vm.Vote.GetStatus()); this.ConnectedToVM.Invoke(this, vm.Config.ID); - if (vm.ScreenHidden && _rank == Rank.Unregistered) + if (vm.ScreenHidden && _rank == Rank.Unregistered || _rank == Rank.Registered) { await this.SendScreenSize(new(1024, 768)); await this.SendRect(Program.ScreenHiddenBase64, 0, 0); @@ -192,10 +212,52 @@ public class User } } break; + case "login": + { + if (this.vm == null || msgArr.Length != 2 || !Program.Config.Auth.Enabled) return; + var res = await Program.AuthManager.AuthenticateUser(msgArr[1], this); + if (!res.success) + { + Utilities.Log(LogLevel.ERROR, $"Failed to query auth server: {res.error}. Rejecting login."); + await this.SendAsync(Guacutils.Encode("login", "0", "Internal error")); + return; + } + if (!res.clientSuccess) + { + await this.SendAsync(Guacutils.Encode("login", "0", res.error ?? "Unknown error")); + if (res.error == "You are banned") await Close(true); + } + // Success + Utilities.Log(LogLevel.INFO, $"{_ip.ToString()} logged in as {res.username}"); + await this.SendAsync(Guacutils.Encode("login", "1")); + var _user = vm!.Users.Find(u => u.Username == res.username); + if (_user != null) + { + // Only one login per user + await _user.Close(); + } + await Rename(res.username); + this._rank = (Rank)res.rank!; + if (this._rank == Rank.Admin) + { + await this.SendAsync(Guacutils.Encode("admin", "0", "1")); + } else if (this._rank == Rank.Moderator) + { + await this.SendAsync(Guacutils.Encode("admin", "0", "3", Program.Config.ModPermissions.ToMask().ToString())); + } + await vm.ReannounceUser(this); + } + break; case "chat": { if (this.vm == null || msgArr.Length != 2 || IPData!.MuteStatus != MuteStatus.None) return; - if (_rank == Rank.Unregistered && ChatLimiter != null && !ChatLimiter.Limit()) + if (Program.Config.Auth.Enabled && !Program.Config.Auth.GuestPermissions.Chat && + _rank == Rank.Unregistered) + { + await this.SendChat("", "You need to login to do that."); + return; + } + if ((_rank == Rank.Unregistered || _rank == Rank.Registered) && ChatLimiter != null && !ChatLimiter.Limit()) { if (IPData!.MuteStatus == MuteStatus.None) await Mute(false); return; @@ -241,6 +303,12 @@ public class User case "turn": { if (this.vm == null || msgArr.Length > 2 || IPData!.MuteStatus != MuteStatus.None) return; + if (Program.Config.Auth.Enabled && !Program.Config.Auth.GuestPermissions.Turns && + _rank == Rank.Unregistered) + { + await this.SendChat("", "You need to login to do that."); + return; + } if (msgArr.Length == 1 || msgArr[1] == "1" && !IPData!.IsTurning && (vm.TurnsAllowed || turnsAllowed || _rank == Rank.Admin || _rank == Rank.Moderator)) { vm.TurnQueue.AddUser(this); @@ -253,6 +321,12 @@ public class User case "vote": { if (this.vm == null || msgArr.Length != 2 || IPData!.IsVoting || IPData!.MuteStatus != MuteStatus.None) return; + if (Program.Config.Auth.Enabled && !Program.Config.Auth.GuestPermissions.Vote && + _rank == Rank.Unregistered) + { + await this.SendChat("", "You need to login to do that."); + return; + } if (!vm.Controller.Snapshots) { await this.SendChat("", "This VM does not support voting to reset"); @@ -281,6 +355,13 @@ public class User { // Login if (msgArr.Length != 3) return; + if (Program.Config.Auth.Enabled) + { + await this.SendChat("", + "This server does not support staff passwords. Please log in to become staff."); + await this.SendAsync(Guacutils.Encode("admin", "0", "0")); + return; + } using var sha = SHA256.Create(); var hash = Utilities.BytesToHex(sha.ComputeHash(Encoding.UTF8.GetBytes(msgArr[2]))); if (hash == Program.Config.Staff.AdminPasswordHash) @@ -414,6 +495,11 @@ public class User { // Rename user if (_rank != Rank.Admin && (_rank != Rank.Moderator || !Program.Config.ModPermissions.Rename) || msgArr.Length != 4 || vm == null) return; + if (Program.Config.Auth.Enabled) + { + await this.SendChat("", "Cannot rename users on a server that uses authentication."); + return; + } var user = vm.Users.First(u => u.Username == msgArr[2]); if (user == null) return; await user.Rename(msgArr[3]); diff --git a/collab-vm-server-1.3/Utilities.cs b/collab-vm-server-1.3/Utilities.cs index f802cc7..1868b26 100644 --- a/collab-vm-server-1.3/Utilities.cs +++ b/collab-vm-server-1.3/Utilities.cs @@ -59,7 +59,7 @@ public static class Utilities case LogLevel.WARN: case LogLevel.ERROR: case LogLevel.FATAL: - Console.Error.Write(logstr.ToString()); + Console.Error.WriteLine(logstr.ToString()); break; } } diff --git a/collab-vm-server-1.3/VM.cs b/collab-vm-server-1.3/VM.cs index 7fe9365..e7a077b 100644 --- a/collab-vm-server-1.3/VM.cs +++ b/collab-vm-server-1.3/VM.cs @@ -42,7 +42,7 @@ public class VM // Send the new size to all users foreach (User user in this.Users.ToArray()) { - if (screenHidden && user.Rank == Rank.Unregistered) continue; + if (screenHidden && user.Rank == Rank.Unregistered || user.Rank == Rank.Registered) continue; user.SendScreenSize(e); } } @@ -68,7 +68,7 @@ public class VM // Send the dirty rect to all users foreach (User user in this.Users.ToArray()) { - if (screenHidden && user.Rank == Rank.Unregistered) continue; + if (screenHidden && user.Rank == Rank.Unregistered || user.Rank == Rank.Registered) continue; user.SendRect(jpg64, e.X, e.Y); } } @@ -236,7 +236,7 @@ public class VM if (hidden) { this.screenHidden = true; - foreach (User u in this.Users.Where(u => u.Rank == Rank.Unregistered)) + foreach (User u in this.Users.Where(u => u.Rank == Rank.Unregistered || u.Rank == Rank.Registered)) { await u.SendScreenSize(new Size(1024, 768)); await u.SendRect(Program.ScreenHiddenBase64, 0, 0); @@ -245,7 +245,7 @@ public class VM else { this.screenHidden = false; - foreach (User u in this.Users.Where(u => u.Rank == Rank.Unregistered)) + foreach (User u in this.Users.Where(u => u.Rank == Rank.Unregistered || u.Rank == Rank.Registered)) { await u.SendScreenSize(GetScreenSize()); await u.SendRect(Convert.ToBase64String(await GetFramebufferJpeg()), 0, 0); diff --git a/config.example.toml b/config.example.toml index 0fa1b5c..785f8fd 100644 --- a/config.example.toml +++ b/config.example.toml @@ -15,6 +15,16 @@ OriginCheck = false # List of domains allowed to host webapps that connect to your VMs. AllowedOrigins = ["https://computernewb.com", "http://localhost:3000"] +[Auth] +Enabled = false +AuthServerURI = "http://127.0.0.1:5858" +SecretKey = "hunter2" + +[Auth.GuestPermissions] +Chat = true +Turns = false +Vote = false + [Turns] # How long each turn is TurnTime = 20