forked from collabvm/collab-vm-server-1.3
Compare commits
4 commits
Author | SHA1 | Date | |
---|---|---|---|
Elijah R | b5c9fe159e | ||
Elijah R | 7f79b36b29 | ||
Elijah R | df094f63d5 | ||
Elijah R | 02ef45de9f |
2
QMPSharp
2
QMPSharp
|
@ -1 +1 @@
|
||||||
Subproject commit f0de933380c0c6488306d04b340f9a1599a4d217
|
Subproject commit cd42387f6e4610341b15e0859f78f8a394914aae
|
41
collab-vm-server-1.3/AuthManager.cs
Normal file
41
collab-vm-server-1.3/AuthManager.cs
Normal 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; }
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ namespace CollabVM.Server.Config;
|
||||||
public class IConfig
|
public class IConfig
|
||||||
{
|
{
|
||||||
public HTTPConfig HTTP { get; set; }
|
public HTTPConfig HTTP { get; set; }
|
||||||
|
public AuthConfig Auth { get; set; }
|
||||||
public TurnConfig Turns { get; set; }
|
public TurnConfig Turns { get; set; }
|
||||||
public VoteConfig Votes { get; set; }
|
public VoteConfig Votes { get; set; }
|
||||||
public ChatConfig Chat { get; set; }
|
public ChatConfig Chat { get; set; }
|
||||||
|
@ -119,3 +120,18 @@ public class QEMUVMConfig
|
||||||
public string? QMPSocketDir { get; set; }
|
public string? QMPSocketDir { get; set; }
|
||||||
public bool Snapshots { 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; }
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ class Program
|
||||||
public static Random rnd = new();
|
public static Random rnd = new();
|
||||||
public static IConfig Config;
|
public static IConfig Config;
|
||||||
public static Database? Database;
|
public static Database? Database;
|
||||||
|
public static AuthManager AuthManager;
|
||||||
// These can go here for now, might move them later
|
// These can go here for now, might move them later
|
||||||
public static readonly string ScreenHiddenBase64 = Convert.ToBase64String(Utilities.GetAsset("screenhidden.jpeg"));
|
public static readonly string ScreenHiddenBase64 = Convert.ToBase64String(Utilities.GetAsset("screenhidden.jpeg"));
|
||||||
public static readonly byte[] ScreenHiddenThumb = Utilities.GetAsset("screenhiddenthumb.jpeg");
|
public static readonly byte[] ScreenHiddenThumb = Utilities.GetAsset("screenhiddenthumb.jpeg");
|
||||||
|
@ -56,6 +57,12 @@ class Program
|
||||||
await Database.OpenAsync();
|
await Database.OpenAsync();
|
||||||
Utilities.Log(LogLevel.INFO, "Connected to MySQL Database.");
|
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
|
// Initialize the VMs
|
||||||
VMManager = new VMManager(Config.VMs);
|
VMManager = new VMManager(Config.VMs);
|
||||||
await VMManager.StartAll();
|
await VMManager.StartAll();
|
||||||
|
|
|
@ -3,6 +3,7 @@ namespace CollabVM.Server;
|
||||||
public enum Rank
|
public enum Rank
|
||||||
{
|
{
|
||||||
Unregistered = 0,
|
Unregistered = 0,
|
||||||
|
Registered = 1,
|
||||||
Admin = 2,
|
Admin = 2,
|
||||||
Moderator = 3,
|
Moderator = 3,
|
||||||
}
|
}
|
|
@ -64,6 +64,7 @@ public class User
|
||||||
this.Disconnected += OnDisconnected;
|
this.Disconnected += OnDisconnected;
|
||||||
this.ConnectedToVM += delegate { };
|
this.ConnectedToVM += delegate { };
|
||||||
this.Renamed += delegate { };
|
this.Renamed += delegate { };
|
||||||
|
if (Program.Config.Auth.Enabled) SendAsync(Guacutils.Encode("auth", Program.Config.Auth.AuthServerURI!));
|
||||||
SendAsync("3.nop;");
|
SendAsync("3.nop;");
|
||||||
ReadLoop(tokenSource.Token);
|
ReadLoop(tokenSource.Token);
|
||||||
}
|
}
|
||||||
|
@ -122,6 +123,25 @@ public class User
|
||||||
break;
|
break;
|
||||||
case "rename":
|
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)
|
if (msgArr.Length == 1)
|
||||||
await this.AssignGuestUsername(this.vm);
|
await this.AssignGuestUsername(this.vm);
|
||||||
else
|
else
|
||||||
|
@ -177,10 +197,10 @@ public class User
|
||||||
chatmsg.Add(vm.Config.MOTD);
|
chatmsg.Add(vm.Config.MOTD);
|
||||||
await this.SendAsync(Guacutils.Encode(chatmsg.ToArray()));
|
await this.SendAsync(Guacutils.Encode(chatmsg.ToArray()));
|
||||||
var turn = vm.TurnQueue.CurrentTurn();
|
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());
|
if (vm.Vote != null) await SendVoteUpdate(vm.Vote.GetStatus());
|
||||||
this.ConnectedToVM.Invoke(this, vm.Config.ID);
|
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.SendScreenSize(new(1024, 768));
|
||||||
await this.SendRect(Program.ScreenHiddenBase64, 0, 0);
|
await this.SendRect(Program.ScreenHiddenBase64, 0, 0);
|
||||||
|
@ -192,10 +212,52 @@ public class User
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
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":
|
case "chat":
|
||||||
{
|
{
|
||||||
if (this.vm == null || msgArr.Length != 2 || IPData!.MuteStatus != MuteStatus.None) return;
|
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);
|
if (IPData!.MuteStatus == MuteStatus.None) await Mute(false);
|
||||||
return;
|
return;
|
||||||
|
@ -241,6 +303,12 @@ public class User
|
||||||
case "turn":
|
case "turn":
|
||||||
{
|
{
|
||||||
if (this.vm == null || msgArr.Length > 2 || IPData!.MuteStatus != MuteStatus.None) return;
|
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))
|
if (msgArr.Length == 1 || msgArr[1] == "1" && !IPData!.IsTurning && (vm.TurnsAllowed || turnsAllowed || _rank == Rank.Admin || _rank == Rank.Moderator))
|
||||||
{
|
{
|
||||||
vm.TurnQueue.AddUser(this);
|
vm.TurnQueue.AddUser(this);
|
||||||
|
@ -253,6 +321,12 @@ public class User
|
||||||
case "vote":
|
case "vote":
|
||||||
{
|
{
|
||||||
if (this.vm == null || msgArr.Length != 2 || IPData!.IsVoting || IPData!.MuteStatus != MuteStatus.None) return;
|
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)
|
if (!vm.Controller.Snapshots)
|
||||||
{
|
{
|
||||||
await this.SendChat("", "This VM does not support voting to reset");
|
await this.SendChat("", "This VM does not support voting to reset");
|
||||||
|
@ -281,6 +355,13 @@ public class User
|
||||||
{
|
{
|
||||||
// Login
|
// Login
|
||||||
if (msgArr.Length != 3) return;
|
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();
|
using var sha = SHA256.Create();
|
||||||
var hash = Utilities.BytesToHex(sha.ComputeHash(Encoding.UTF8.GetBytes(msgArr[2])));
|
var hash = Utilities.BytesToHex(sha.ComputeHash(Encoding.UTF8.GetBytes(msgArr[2])));
|
||||||
if (hash == Program.Config.Staff.AdminPasswordHash)
|
if (hash == Program.Config.Staff.AdminPasswordHash)
|
||||||
|
@ -414,6 +495,11 @@ public class User
|
||||||
{
|
{
|
||||||
// Rename user
|
// Rename user
|
||||||
if (_rank != Rank.Admin && (_rank != Rank.Moderator || !Program.Config.ModPermissions.Rename) || msgArr.Length != 4 || vm == null) return;
|
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]);
|
var user = vm.Users.First(u => u.Username == msgArr[2]);
|
||||||
if (user == null) return;
|
if (user == null) return;
|
||||||
await user.Rename(msgArr[3]);
|
await user.Rename(msgArr[3]);
|
||||||
|
|
|
@ -59,7 +59,7 @@ public static class Utilities
|
||||||
case LogLevel.WARN:
|
case LogLevel.WARN:
|
||||||
case LogLevel.ERROR:
|
case LogLevel.ERROR:
|
||||||
case LogLevel.FATAL:
|
case LogLevel.FATAL:
|
||||||
Console.Error.Write(logstr.ToString());
|
Console.Error.WriteLine(logstr.ToString());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ public class VM
|
||||||
// Send the new size to all users
|
// Send the new size to all users
|
||||||
foreach (User user in this.Users.ToArray())
|
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);
|
user.SendScreenSize(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,7 @@ public class VM
|
||||||
// Send the dirty rect to all users
|
// Send the dirty rect to all users
|
||||||
foreach (User user in this.Users.ToArray())
|
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);
|
user.SendRect(jpg64, e.X, e.Y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -236,7 +236,7 @@ public class VM
|
||||||
if (hidden)
|
if (hidden)
|
||||||
{
|
{
|
||||||
this.screenHidden = true;
|
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.SendScreenSize(new Size(1024, 768));
|
||||||
await u.SendRect(Program.ScreenHiddenBase64, 0, 0);
|
await u.SendRect(Program.ScreenHiddenBase64, 0, 0);
|
||||||
|
@ -245,7 +245,7 @@ public class VM
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
this.screenHidden = false;
|
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.SendScreenSize(GetScreenSize());
|
||||||
await u.SendRect(Convert.ToBase64String(await GetFramebufferJpeg()), 0, 0);
|
await u.SendRect(Convert.ToBase64String(await GetFramebufferJpeg()), 0, 0);
|
||||||
|
|
|
@ -244,6 +244,9 @@ public class QEMUController : VMController
|
||||||
|
|
||||||
private async Task StopQEMU()
|
private async Task StopQEMU()
|
||||||
{
|
{
|
||||||
|
if (_expectedExit) return;
|
||||||
|
_qmpReconnectTimer.Stop();
|
||||||
|
_qemuRestartTimer?.Stop();
|
||||||
_expectedExit = true;
|
_expectedExit = true;
|
||||||
if (this._qemuProc == null || this._qemuProc.HasExited) return;
|
if (this._qemuProc == null || this._qemuProc.HasExited) return;
|
||||||
await this._vnc.Disconnect();
|
await this._vnc.Disconnect();
|
||||||
|
@ -272,6 +275,7 @@ public class QEMUController : VMController
|
||||||
public override async Task Reset()
|
public override async Task Reset()
|
||||||
{
|
{
|
||||||
if (!Snapshots) return;
|
if (!Snapshots) return;
|
||||||
|
if (_expectedExit) return;
|
||||||
await this.StopQEMU();
|
await this.StopQEMU();
|
||||||
await this.Start();
|
await this.Start();
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<PackageReference Include="CircularBuffer" Version="1.3.0" />
|
<PackageReference Include="CircularBuffer" Version="1.3.0" />
|
||||||
<PackageReference Include="MySqlConnector" Version="2.3.3" />
|
<PackageReference Include="MySqlConnector" Version="2.3.3" />
|
||||||
<PackageReference Include="Samboy063.Tomlet" Version="5.2.0" />
|
<PackageReference Include="Samboy063.Tomlet" Version="5.2.0" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.1" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,16 @@ OriginCheck = false
|
||||||
# List of domains allowed to host webapps that connect to your VMs.
|
# List of domains allowed to host webapps that connect to your VMs.
|
||||||
AllowedOrigins = ["https://computernewb.com", "http://localhost:3000"]
|
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]
|
[Turns]
|
||||||
# How long each turn is
|
# How long each turn is
|
||||||
TurnTime = 20
|
TurnTime = 20
|
||||||
|
|
Loading…
Reference in a new issue