implement auth and make a few minor changed

This commit is contained in:
Elijah R 2024-04-13 12:32:34 -04:00
parent 02ef45de9f
commit df094f63d5
8 changed files with 169 additions and 8 deletions

View file

@ -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<JoinResponse> 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<JoinResponse>() ?? 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; }
}

View file

@ -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; }
}

View file

@ -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();

View file

@ -3,6 +3,7 @@ namespace CollabVM.Server;
public enum Rank
{
Unregistered = 0,
Registered = 1,
Admin = 2,
Moderator = 3,
}

View file

@ -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]);

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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