From 41834c461ebc190a3e824c018abfd7e46d6f0d59 Mon Sep 17 00:00:00 2001 From: elijahr2411 Date: Sat, 18 Feb 2023 21:12:26 -0500 Subject: [PATCH] all the shit --- .gitignore | 6 + CollabVMSharp.sln | 22 + CollabVMSharp/ChatMessage.cs | 6 + CollabVMSharp/CollabVMClient.cs | 652 +++++++++++++++++++++++++++++ CollabVMSharp/CollabVMSharp.csproj | 12 + CollabVMSharp/EventArgs.cs | 60 +++ CollabVMSharp/Exceptions.cs | 12 + CollabVMSharp/Guacutils.cs | 45 ++ CollabVMSharp/Keyboard.cs | 144 +++++++ CollabVMSharp/Mouse.cs | 38 ++ CollabVMSharp/Node.cs | 19 + CollabVMSharp/Permissions.cs | 47 +++ CollabVMSharp/TurnStatus.cs | 7 + CollabVMSharp/User.cs | 7 + global.json | 7 + 15 files changed, 1084 insertions(+) create mode 100644 .gitignore create mode 100644 CollabVMSharp.sln create mode 100644 CollabVMSharp/ChatMessage.cs create mode 100644 CollabVMSharp/CollabVMClient.cs create mode 100644 CollabVMSharp/CollabVMSharp.csproj create mode 100644 CollabVMSharp/EventArgs.cs create mode 100644 CollabVMSharp/Exceptions.cs create mode 100644 CollabVMSharp/Guacutils.cs create mode 100644 CollabVMSharp/Keyboard.cs create mode 100644 CollabVMSharp/Mouse.cs create mode 100644 CollabVMSharp/Node.cs create mode 100644 CollabVMSharp/Permissions.cs create mode 100644 CollabVMSharp/TurnStatus.cs create mode 100644 CollabVMSharp/User.cs create mode 100644 global.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39c9242 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ +.idea/ diff --git a/CollabVMSharp.sln b/CollabVMSharp.sln new file mode 100644 index 0000000..8502f33 --- /dev/null +++ b/CollabVMSharp.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CollabVMSharp", "CollabVMSharp\CollabVMSharp.csproj", "{71E96501-6FF7-48B4-BCB5-4D777ED300D2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cvmsharptest", "..\cvmsharptest\cvmsharptest\cvmsharptest.csproj", "{3285A3D2-F8CB-43D6-93BC-61FB3C73F990}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {71E96501-6FF7-48B4-BCB5-4D777ED300D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71E96501-6FF7-48B4-BCB5-4D777ED300D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71E96501-6FF7-48B4-BCB5-4D777ED300D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71E96501-6FF7-48B4-BCB5-4D777ED300D2}.Release|Any CPU.Build.0 = Release|Any CPU + {3285A3D2-F8CB-43D6-93BC-61FB3C73F990}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3285A3D2-F8CB-43D6-93BC-61FB3C73F990}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3285A3D2-F8CB-43D6-93BC-61FB3C73F990}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3285A3D2-F8CB-43D6-93BC-61FB3C73F990}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/CollabVMSharp/ChatMessage.cs b/CollabVMSharp/ChatMessage.cs new file mode 100644 index 0000000..5afcdfd --- /dev/null +++ b/CollabVMSharp/ChatMessage.cs @@ -0,0 +1,6 @@ +namespace CollabVMSharp; + +public class ChatMessage { + public string Username { get; set; } + public string Message { get; set; } +} \ No newline at end of file diff --git a/CollabVMSharp/CollabVMClient.cs b/CollabVMSharp/CollabVMClient.cs new file mode 100644 index 0000000..cec3d4f --- /dev/null +++ b/CollabVMSharp/CollabVMClient.cs @@ -0,0 +1,652 @@ +#nullable enable +#pragma warning disable CS4014 +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.WebSockets; +using System.Security.Authentication; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using Timer = System.Timers.Timer; +// ReSharper disable FieldCanBeMadeReadOnly.Local +// ReSharper disable ArrangeObjectCreationWhenTypeNotEvident +// ReSharper disable InconsistentNaming +// ReSharper disable ArrangeObjectCreationWhenTypeEvident + +namespace CollabVMSharp; +public class CollabVMClient { + // Fields + private Uri url; + private string? username; + private string? node; + private Rank _rank; + private Permissions _perms; + private bool _connected; + private bool _connectedToVM; + private ClientWebSocket socket; + private List _users; + private Timer NOPRecieve; + private Image framebuffer; + private TurnStatus _turnStatus; + private Mouse mouse; + // Tasks + private TaskCompletionSource GotNodeList; + private TaskCompletionSource GotConnectionToNode; + private TaskCompletionSource GotTurn; + private TaskCompletionSource GotStaff; + private List GotIPTasks; + // Properties + public Rank Rank { get { return this._rank; } } + public Permissions Permissions { get { return this._perms; } } + public bool Connected { get { return this._connected; } } + public bool ConnectedToVM { get { return this._connectedToVM; } } + public User[] Users => _users.ToArray(); + public TurnStatus TurnStatus { get { return this._turnStatus; } } + // Events + public event EventHandler Chat; + public event EventHandler ChatHistory; + public event EventHandler ConnectedToNode; + public event EventHandler NodeConnectFailed; + public event EventHandler Rect; + public event EventHandler Renamed; + public event EventHandler UserRenamed; + public event EventHandler UserJoined; + public event EventHandler UserLeft; + public event EventHandler VoteUpdate; + public event EventHandler VoteEnded; + public event EventHandler VoteCooldown; + public event EventHandler TurnUpdate; + public event EventHandler ConnectionClosed; + /// + /// Client for the CollabVM 1.x Server + /// + /// URL of the CollabVM Server to connect to (Should start with ws:// or wss://) + /// Username to join the VM as. If null, the server will assign a guest name. + /// Node to connect to. If null, a VM will not be automatically joined. + /// HTTP proxy to connect with. If null, a proxy will not be used. + public CollabVMClient(string url, string? username = null, string? node = null, string? proxy = null) { + if (!Uri.TryCreate(url, UriKind.Absolute, out this.url)) { + throw new UriFormatException("An invalid URI string was passed."); + } + if (this.url.Scheme != "ws" && this.url.Scheme != "wss") { + throw new UriFormatException("The URL must have a valid websocket scheme (ws or wss)"); + } + this.username = username; + this.node = node; + this._rank = Rank.Unregistered; + this._perms = Permissions.None; + this._connected = false; + this._connectedToVM = false; + this.framebuffer = new Image(1, 1); + this.socket = new(); + this.socket.Options.AddSubProtocol("guacamole"); + this.socket.Options.SetRequestHeader("Origin", "https://computernewb.com"); + if (proxy != null) { + this.socket.Options.Proxy = new WebProxy(proxy); + } + this.NOPRecieve = new(10000); + this.NOPRecieve.AutoReset = false; + this.NOPRecieve.Elapsed += delegate { this.Disconnect(); }; + this._users = new(); + this.mouse = new(); + this.GotNodeList = new(); + this.GotConnectionToNode = new(); + this.GotTurn = new(); + this.GotStaff = new(); + this.GotIPTasks = new(); + // Assign empty handlers + Chat += delegate { }; + ChatHistory += delegate { }; + ConnectedToNode += delegate { }; + NodeConnectFailed += delegate { }; + Rect += delegate { }; + Renamed += delegate { }; + UserRenamed += delegate { }; + UserJoined += delegate { }; + UserLeft += delegate { }; + VoteUpdate += delegate { }; + VoteEnded += delegate { }; + VoteCooldown += delegate { }; + TurnUpdate += delegate { }; + ConnectionClosed += delegate { }; + } + /// + /// Connect to the CollabVM Server + /// + public async Task Connect() { + await this.socket.ConnectAsync(this.url, CancellationToken.None); + this._connected = true; + if (this.username != null) + this.SendMsg(Guacutils.Encode("rename", this.username)); + else + this.SendMsg(Guacutils.Encode("rename")); + if (this.node != null) + this.SendMsg(Guacutils.Encode("connect", this.node)); + this.NOPRecieve.Start(); + this.WebSocketLoop(); + } + + private async void WebSocketLoop() { + ArraySegment receivebuffer = new ArraySegment(new byte[8192]); + do { + MemoryStream ms = new(); + WebSocketReceiveResult res; + do { + res = await socket.ReceiveAsync(receivebuffer, CancellationToken.None); + if (res.MessageType == WebSocketMessageType.Close) { + this.Disconnect(); + return; + } + await ms.WriteAsync(receivebuffer.Array, 0, res.Count); + } while (!res.EndOfMessage); + string msg; + try { + msg = Encoding.UTF8.GetString(ms.ToArray()); + } catch (Exception e) { + #if DEBUG + await Console.Error.WriteLineAsync($"Failed to read message from socket: {e.Message}"); + #endif + continue; + } finally {ms.Dispose();} + this.ProcessMessage(msg); + } while (socket.State == WebSocketState.Open); + this.Cleanup(); + } + + private async void ProcessMessage(string msg) { + string[] msgArr; + try { + msgArr = Guacutils.Decode(msg); + } catch (Exception e) { + #if DEBUG + await Console.Error.WriteLineAsync($"Failed to decode incoming message: {e.Message}"); + #endif + return; + } + if (msgArr.Length < 1) return; + this.NOPRecieve.Stop(); + this.NOPRecieve.Interval = 10000; + this.NOPRecieve.Start(); + switch (msgArr[0]) { + case "nop": { + this.SendMsg("3.nop;"); + break; + } + case "connect": { + switch (msgArr[1]) { + case "0": + this.NodeConnectFailed.Invoke(this, EventArgs.Empty); + this.GotConnectionToNode.TrySetResult(false); + break; + case "1": + this._connectedToVM = true; + this.ConnectedToNode.Invoke(this, EventArgs.Empty); + this.GotConnectionToNode.TrySetResult(true); + break; + } + break; + } + case "chat": { + if (msgArr.Length > 3) { + List msgs = new(); + for (int i = 1; i < msgArr.Length; i += 2) { + msgs.Add(new ChatMessage { + Username = msgArr[i], + Message = WebUtility.HtmlDecode(msgArr[i+1]) + }); + } + ChatHistory.Invoke(this, msgs.ToArray()); + // I should probably add a config option for whether or not the message should be HTML encoded + } else Chat.Invoke(this, new ChatMessage {Username = msgArr[1], Message = WebUtility.HtmlDecode(msgArr[2])}); + break; + } + case "captcha": { + throw new Exception("This VM requires a captcha to connect to. Please do not attempt to bypass this. Contact the CollabVM admins (or the owner of the server you're connecting to) to request to be added to the bot whitelist."); + break; + } + case "list": { + List nodes = new(); + for (var i = 1; i < msgArr.Length; i += 3) { + nodes.Add(new Node { + ID = msgArr[i], + Name = msgArr[i+1], + Thumbnail = Convert.FromBase64String(msgArr[i+2]) + }); + this.GotNodeList.TrySetResult(nodes.ToArray()); + } + break; + } + case "size": { + if (msgArr[1] != "0") return; + this.framebuffer = new Image(int.Parse(msgArr[2]), int.Parse(msgArr[3])); + break; + } + case "png": { + if (msgArr[2] != "0") return; + Image rect = Image.Load(Convert.FromBase64String(msgArr[5])); + framebuffer.Mutate(f => f.DrawImage(rect, new Point(int.Parse(msgArr[3]), int.Parse(msgArr[4])), 1)); + this.Rect.Invoke(this, new RectEventArgs { + X = int.Parse(msgArr[3]), + Y = int.Parse(msgArr[4]), + Data = rect, + }); + break; + } + case "rename": { + switch (msgArr[1]) { + case "0": { + var user = _users.Find(u => u.Username == username); + if (user != null) { + user.Username = msgArr[3]; + } + this.username = msgArr[3]; + this.Renamed.Invoke(this, msgArr[3]); + break; + } + default: { + var user = _users.Find(u => u.Username == msgArr[2]); + user.Username = msgArr[3]; + this.UserRenamed.Invoke(this, new UserRenamedEventArgs { + OldName = msgArr[2], + NewName = msgArr[3], + User = user + }); + break; + } + } + break; + } + case "adduser": { + for (int i = 2; i < msgArr.Length; i += 2) { + // This can happen when a user logs in + _users.RemoveAll(u => u.Username == msgArr[i]); + var user = new User { + Username = msgArr[i], + Rank = msgArr[i + 1] switch { + "0" => Rank.Unregistered, + "2" => Rank.Admin, + "3" => Rank.Moderator, + _ => Rank.Unregistered + }, + Turn = TurnStatus.None + }; + this.UserJoined.Invoke(this, user); + this._users.Add(user); + } + break; + } + case "remuser": { + for (int i = 2; i < msgArr.Length; i++) { + var user = this._users.Find(u=>u.Username==msgArr[i]); + this._users.Remove(user); + UserLeft.Invoke(this, user); + } + break; + } + case "vote": { + switch (msgArr[1]) { + case "0": + case "1": + this.VoteUpdate.Invoke(this, new VoteUpdateEventArgs { + No = int.Parse(msgArr[4]), + Yes = int.Parse(msgArr[3]), + Status = msgArr[1] switch {"0" => VoteStatus.Started, "1" => VoteStatus.Update}, + TimeToVoteEnd = int.Parse(msgArr[2]) + }); + break; + case "2": + this.VoteEnded.Invoke(this, EventArgs.Empty); + break; + case "3": + this.VoteCooldown.Invoke(this, int.Parse(msgArr[2])); + break; + } + break; + } + case "turn": { + List queue = new(); + // Reset turn data + this._users.ForEach(u => u.Turn = TurnStatus.None); + this._turnStatus = TurnStatus.None; + int queuedUsers = int.Parse(msgArr[2]); + if (queuedUsers == 0) { + this.TurnUpdate.Invoke(this, new TurnUpdateEventArgs { + Queue = Array.Empty(), + TurnTimer = null, + QueueTimer = null + }); + return; + } + var currentTurnUser = _users.First(u => u.Username == msgArr[3]); + if (msgArr[3] == this.username) { + this._turnStatus = TurnStatus.HasTurn; + this.GotTurn.TrySetResult(int.Parse(msgArr[1])); + } + currentTurnUser.Turn = TurnStatus.HasTurn; + queue.Add(currentTurnUser); + if (queuedUsers > 1) { + for (int i = 1; i < queuedUsers; i++) { + var user = _users.Find(u => u.Username == msgArr[i + 3]); + user.Turn = TurnStatus.Waiting; + if (msgArr[i + 3] == this.username) + this._turnStatus = TurnStatus.Waiting; + queue.Add(user); + } + } + this.TurnUpdate.Invoke(this, new TurnUpdateEventArgs { + Queue = queue.ToArray(), + TurnTimer = (this._turnStatus == TurnStatus.HasTurn) ? int.Parse(msgArr[1]) : null, + QueueTimer = (this._turnStatus == TurnStatus.Waiting) ? int.Parse(msgArr[msgArr.Length-1]) : null, + }); + break; + } + case "admin": { + switch (msgArr[1]) { + case "0": { + switch (msgArr[2]) { + case "0": + throw new InvalidCredentialException("The provided password was incorrect."); + break; + case "1": + this._rank = Rank.Admin; + this._perms = Permissions.All; + this.GotStaff.TrySetResult(Rank.Admin); + break; + case "3": + this._rank = Rank.Moderator; + this._perms = new Permissions(int.Parse(msgArr[3])); + this.GotStaff.TrySetResult(Rank.Moderator); + break; + } + break; + } + case "19": + var tsk = this.GotIPTasks.Find(x => x.username == msgArr[2]); + if (tsk == null) return; + tsk.IPTask.TrySetResult(msgArr[3]); + break; + } + break; + } + } + } + /// + /// Close the connection to the server + /// + public async Task Disconnect() { + this._connected = false; + if (this.socket.State == WebSocketState.Open) + await this.SendMsg("10.disconnect;"); + await this.socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); + this.Cleanup(); + return; + } + + private void Cleanup(bool fireDisconnect = true) { + this._users.Clear(); + this._connected = false; + this._connectedToVM = false; + this._rank = Rank.Unregistered; + this._perms = Permissions.None; + this.NOPRecieve.Stop(); + this.NOPRecieve.Interval = 10000; + if (fireDisconnect) + this.ConnectionClosed.Invoke(this, EventArgs.Empty); + } + + /// + /// Send a raw string message over the socket + /// + public Task SendMsg(string msg) { + if (!this._connected) throw new WebSocketException("Cannot send a message while the socket is closed"); + return this.socket.SendAsync(new ArraySegment(Encoding.UTF8.GetBytes(msg)), WebSocketMessageType.Text, + true, CancellationToken.None); + } + /// + /// Request a list of VMs from the server. + /// + /// A list of VMs + public async Task ListVMs() { + GotNodeList = new(); + SendMsg(Guacutils.Encode("list")); + return await GotNodeList.Task; + } + /// + /// Attempt to connect to a VM + /// + /// ID of the VM to connect to + /// True if successful, false otherwise + public async Task ConnectToVM(string node) { + if (this._connectedToVM) + throw new Exception("Already connected to a node. You must disconnect and reconnect to switch nodes."); + this.GotConnectionToNode = new(); + this.SendMsg(Guacutils.Encode("connect", node)); + return await this.GotConnectionToNode.Task; + } + + /// + /// Send a key to the VM. If you don't have the turn, nothing will happen + /// + /// X11 Keysym of the key to send. + /// Whether or not the key is pressed + public Task SendKey(int keysym, bool down) => this.SendMsg(Guacutils.Encode("key", keysym.ToString(), down ? "1" : "0")); + + /// + /// Move the mouse + /// + /// Horizontal position or offset of the mouse + /// Vertical position or offset of the mouse + /// If true, mouse is moved relative to it's current position. If false, the mouse will be moved to the exact coordinates given + public async Task MoveMouse(int x, int y, bool relative = false) { + if (relative) { + this.mouse.X += x; + this.mouse.Y += y; + } + else { + this.mouse.X = x; + this.mouse.Y = y; + } + this.sendMouse(); + } + /// + /// Set the pressed mouse buttons to the given mask + /// + /// The button mask, representing the pressed or released status of each mouse button. + public async Task MouseBtn(int mask) { + this.mouse.LoadMask(mask); + await this.sendMouse(); + } + + /// + /// Press or release a mouse button + /// + /// The button to change the state of + /// True if the button is down, false if it's up + public async Task MouseBtn(MouseButton btn, bool down) { + switch (btn) { + case MouseButton.Left: + this.mouse.Left = down; + break; + case MouseButton.Middle: + this.mouse.Middle = down; + break; + case MouseButton.Right: + this.mouse.Right = down; + break; + } + await this.sendMouse(); + } + + /// + /// Type a specified character to the VM + /// + /// The character to type + /// Whether the key is pressed or released, or null to press and then release + public async Task SendChar(char c, bool? down = null) { + int keysym = Keyboard.KeyMap[c]; + if (down != null) + await this.SendKey(keysym, (bool)down); + else { + await this.SendKey(keysym, true); + await Task.Delay(20); + await this.SendKey(keysym, false); + } + } + /// + /// Press a special key on the VM + /// + /// Key to send + /// Whether the key is pressed or released, or null to press and then release + public async Task SendSpecialKey(SpecialKey key, bool? down = null) { + int keysym = (int)key; + if (down != null) + await this.SendKey(keysym, (bool)down); + else { + await this.SendKey(keysym, true); + await Task.Delay(20); + await this.SendKey(keysym, false); + } + } + + /// + /// Type a string into the VM + /// + /// String to type + public async Task TypeString(string str) { + foreach (char c in str) { + await SendChar(c); + await Task.Delay(20); + } + } + /// + /// Request or cancel a turn + /// + /// True to request a turn, false to cancel + public Task Turn(bool take) => this.SendMsg(Guacutils.Encode("turn", take ? "1" : "0")); + + /// + /// Request a turn and returns once the turn is received. + /// + /// How long you have the turn, in milliseconds + public async Task GetTurn() { + if (this._turnStatus == TurnStatus.HasTurn) + throw new Exception("You already have the turn"); + this.GotTurn = new(); + this.Turn(true); + return await this.GotTurn.Task; + } + + /// + /// Log in as an Admin or Moderator + /// + /// Password to log in with + /// The rank received + public async Task Login(string password) { + this.GotStaff = new(); + this.SendMsg(Guacutils.Encode("admin", "2", password)); + return await this.GotStaff.Task; + } + + /// + /// Send a message to the VM chat + /// + /// Message to send` + public Task SendChat(string msg) => this.SendMsg(Guacutils.Encode("chat", msg)); + + /// + /// Restore the VM + /// + public async Task Restore() { + if (!this._perms.Restore) + throw new NoPermissionException("Restore VM"); + if (!this._connectedToVM) + throw new NotConnectedToNodeException("Restore VM"); + await this.SendMsg(Guacutils.Encode("admin", "8", this.node)); + } + + public async Task Reboot() { + if (!this._perms.Reboot) + throw new NoPermissionException("Reboot VM"); + if (!this._connectedToVM) + throw new NotConnectedToNodeException("Reboot VM"); + await this.SendMsg(Guacutils.Encode("admin", "10", this.node)); + } + + public async Task ClearTurnQueue() { + if (!this._perms.BypassAndEndTurns) + throw new NoPermissionException("Clear the turn queue"); + if (!this._connectedToVM) + throw new NotConnectedToNodeException("Clear the turn queue"); + await this.SendMsg(Guacutils.Encode("admin", "17", this.node)); + } + + public async Task BypassTurn() { + if (!this._perms.BypassAndEndTurns) + throw new NoPermissionException("Bypass Turn"); + if (!this._connectedToVM) + throw new NotConnectedToNodeException("Bypass Turn"); + await this.SendMsg(Guacutils.Encode("admin", "20")); + } + + public async Task EndTurn(string user) { + if (!this._perms.BypassAndEndTurns) + throw new NoPermissionException("End Turn"); + if (!this._connectedToVM) + throw new NotConnectedToNodeException("End Turn"); + await this.SendMsg(Guacutils.Encode("admin", "16", user)); + } + + public async Task Ban(string user) { + if (!this._perms.Ban) + throw new NoPermissionException("Ban"); + if (!this._connectedToVM) + throw new NotConnectedToNodeException("Ban"); + await this.SendMsg(Guacutils.Encode("admin", "12", user)); + } + + public async Task Kick(string user) { + if (!this._perms.Kick) + throw new NoPermissionException("Kick"); + if (!this._connectedToVM) + throw new NotConnectedToNodeException("Kick"); + await this.SendMsg(Guacutils.Encode("admin", "15", user)); + } + + public async Task RenameUser(string user, string newname) { + if (!this._perms.Rename) + throw new NoPermissionException("Rename user"); + if (!this._connectedToVM) + throw new NotConnectedToNodeException("Rename user"); + await this.SendMsg(Guacutils.Encode("admin", "18", user, newname)); + } + + public async Task MuteUser(string user, bool permanent) { + if (!this._perms.Mute) + throw new NoPermissionException("Mute user"); + if (!this._connectedToVM) + throw new NotConnectedToNodeException("Mute user"); + await this.SendMsg(Guacutils.Encode("admin", "14", user, permanent ? "1" : "0")); + } + + public async Task GetIP(string user) { + if (!this._perms.GetIP) + throw new NoPermissionException("Get IP"); + if (!this._connectedToVM) + throw new NotConnectedToNodeException("Get IP"); + var tsk = new GetIPTask { + IPTask = new(), + username = user + }; + this.GotIPTasks.Add(tsk); + this.SendMsg(Guacutils.Encode("admin", "19", user)); + return await tsk.IPTask.Task; + } + + private Task sendMouse() => this.SendMsg(Guacutils.Encode("mouse", mouse.X.ToString(), mouse.Y.ToString(), mouse.MakeMask().ToString())); +} \ No newline at end of file diff --git a/CollabVMSharp/CollabVMSharp.csproj b/CollabVMSharp/CollabVMSharp.csproj new file mode 100644 index 0000000..fabed75 --- /dev/null +++ b/CollabVMSharp/CollabVMSharp.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.0 + 10 + + + + + + + diff --git a/CollabVMSharp/EventArgs.cs b/CollabVMSharp/EventArgs.cs new file mode 100644 index 0000000..284ea0f --- /dev/null +++ b/CollabVMSharp/EventArgs.cs @@ -0,0 +1,60 @@ +using System.Threading.Tasks; +using SixLabors.ImageSharp; + +namespace CollabVMSharp; + +public class UserRenamedEventArgs { + public string OldName { get; set; } + public string NewName { get; set; } + public User User; +} + +public class VoteUpdateEventArgs { + public int Yes { get; set; } + public int No { get; set; } + public VoteStatus Status { get; set; } + /// + /// Amount of time until the vote ends, in milliseconds + /// + public int TimeToVoteEnd { get; set; } +} + +public class SendVoteResult { + /// + /// True if your vote was sent successfully, false if there's a cooldown. + /// + public bool Success { get; set; } + public int? CooldownTime { get; set; } +} + +public class TurnUpdateEventArgs { + /// + /// The amount of time left on your turn in milliseconds. Null if you don't have the turn. + /// + public int? TurnTimer; + /// + /// The amount of time left before you get your turn in milliseconds. Null if you aren't waiting. + /// + public int? QueueTimer; + /// + /// The turn queue. The first element (index 0) has the turn, all following elements are the waiting users in order + /// + public User[] Queue; +} + +public class RectEventArgs { + public int X { get; set; } + public int Y { get; set; } + public Image Data { get; set; } +} + +// this might not be the best place for this IDK +public enum VoteStatus { + Started, + Update, +} + +public class GetIPTask { + public string username { get; set; } + public TaskCompletionSource IPTask; +} \ No newline at end of file diff --git a/CollabVMSharp/Exceptions.cs b/CollabVMSharp/Exceptions.cs new file mode 100644 index 0000000..e4325a7 --- /dev/null +++ b/CollabVMSharp/Exceptions.cs @@ -0,0 +1,12 @@ +using System; + +namespace CollabVMSharp; + +public class NoPermissionException : Exception { + public NoPermissionException(string action) : base($"You do not have permission to {action} on this VM.") {} +} + +public class NotConnectedToNodeException : Exception { + public NotConnectedToNodeException(string action) : base($"You must be connected to a node to {action}") { + } +} \ No newline at end of file diff --git a/CollabVMSharp/Guacutils.cs b/CollabVMSharp/Guacutils.cs new file mode 100644 index 0000000..35b90dd --- /dev/null +++ b/CollabVMSharp/Guacutils.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; + +namespace CollabVMSharp { + /// + /// Utilities for converting lists of strings to and from Guacamole format + /// + public static class Guacutils { + /// + /// Encode an array of strings to guacamole format + /// + /// List of strings to be encoded + /// A guacamole string array containing the provided strings + public static string Encode(params string[] msgArr) { + string res = ""; + int i = 0; + foreach (string s in msgArr) { + res += s.Length.ToString(); + res += "."; + res += s; + if (i == msgArr.Length - 1) res += ";"; + else res += ","; + i++; + } + return res; + } + /// + /// Decode a guacamole string array + /// + /// String containing a guacamole array + /// An array of strings + public static string[] Decode(string msg) { + List outArr = new List(); + int pos = 0; + while (pos < msg.Length - 1) { + int dotpos = msg.IndexOf('.', pos + 1); + string lenstr = msg.Substring(pos, dotpos - pos); + int len = int.Parse(lenstr); + string str = msg.Substring(dotpos + 1, len); + outArr.Add(str); + pos = dotpos + len + 2; + } + return outArr.ToArray(); + } + } +} diff --git a/CollabVMSharp/Keyboard.cs b/CollabVMSharp/Keyboard.cs new file mode 100644 index 0000000..c6c8b66 --- /dev/null +++ b/CollabVMSharp/Keyboard.cs @@ -0,0 +1,144 @@ +using System.Collections.Generic; + +namespace CollabVMSharp; + +public enum SpecialKey : int { + Back = 0xff08, + Tab = 0xff09, + Clear = 0xff0b, + Return = 0xff0d, + Pause = 0xff13, + Scroll = 0xff14, + Escape = 0xff1b, + Delete = 0xffff, + Home = 0xff58, + Left = 0xff51, + Up = 0xff52, + Right = 0xff53, + Down = 0xff54, + PageUp = 0xff55, + PageDown = 0xff56, + End = 0xff57, + Insert = 0xff63, + F1 = 0xffbe, + F2 = 0xffbf, + F3 = 0xffc0, + F4 = 0xffc1, + F5 = 0xffc2, + F6 = 0xffc3, + F7 =0xffc4, + F8 = 0xffc5, + F9 = 0xffc6, + F10 = 0xffc7, + F11 = 0xffc8, + F12 = 0xffc9, + LeftShift = 0xffe1, + RightShift = 0xffe2, + LeftCtrl = 0xffe3, + RightCtrl = 0xffe4, + CapsLock = 0xffe5, + LWin = 0xffe7, + LeftAlt = 0xffe9, + RightAlt = 0xffea, +}; +public static class Keyboard { + public static IReadOnlyDictionary KeyMap = new Dictionary() { + {' ', 0x0020 }, + {'!', 0x0021 }, + {'"', 0x0022 }, + {'#', 0x0023 }, + {'$', 0x0024 }, + {'%', 0x0025 }, + {'&', 0x0026 }, + {'\'', 0x0027 }, + {'(', 0x0028 }, + {')', 0x0029 }, + {'*', 0x002a }, + {'+', 0x002b }, + {',', 0x002c }, + {'-', 0x002d }, + {'.', 0x002e }, + {'/', 0x002f }, + {'0', 0x0030 }, + {'1', 0x0031 }, + {'2', 0x0032 }, + {'3', 0x0033 }, + {'4', 0x0034 }, + {'5', 0x0035 }, + {'6', 0x0036 }, + {'7', 0x0037 }, + {'8', 0x0038 }, + {'9', 0x0039 }, + {':', 0x003a }, + {';', 0x003b }, + {'<', 0x003c }, + {'=', 0x003d }, + {'>', 0x003e }, + {'?', 0x003f }, + {'@', 0x0040 }, + {'A', 0x0041 }, + {'B', 0x0042 }, + {'C', 0x0043 }, + {'D', 0x0044 }, + {'E', 0x0045 }, + {'F', 0x0046 }, + {'G', 0x0047 }, + {'H', 0x0048 }, + {'I', 0x0049 }, + {'J', 0x004a }, + {'K', 0x004b }, + {'L', 0x004c }, + {'M', 0x004d }, + {'N', 0x004e }, + {'O', 0x004f }, + {'P', 0x0050 }, + {'Q', 0x0051 }, + {'R', 0x0052 }, + {'S', 0x0053 }, + {'T', 0x0054 }, + {'U', 0x0055 }, + {'V', 0x0056 }, + {'W', 0x0057 }, + {'X', 0x0058 }, + {'Y', 0x0059 }, + {'Z', 0x005a }, + {'[', 0x005b }, + {'\\', 0x005c }, + {']', 0x005d }, + {'^', 0x005e }, + {'_', 0x005f }, + {'`', 0x0060 }, + {'a', 0x0061 }, + {'b', 0x0062 }, + {'c', 0x0063 }, + {'d', 0x0064 }, + {'e', 0x0065 }, + {'f', 0x0066 }, + {'g', 0x0067 }, + {'h', 0x0068 }, + {'i', 0x0069 }, + {'j', 0x006a }, + {'k', 0x006b }, + {'l', 0x006c }, + {'m', 0x006d }, + {'n', 0x006e }, + {'o', 0x006f }, + {'p', 0x0070 }, + {'q', 0x0071 }, + {'r', 0x0072 }, + {'s', 0x0073 }, + {'t', 0x0074 }, + {'u', 0x0075 }, + {'v', 0x0076 }, + {'w', 0x0077 }, + {'x', 0x0078 }, + {'y', 0x0079 }, + {'z', 0x007a }, + {'{', 0x007b }, + {'|', 0x007c }, + {'}', 0x007d }, + {'~', 0x007e }, + {'\n', 0xff0d}, + {'\t', 0xff09} + }; +} \ No newline at end of file diff --git a/CollabVMSharp/Mouse.cs b/CollabVMSharp/Mouse.cs new file mode 100644 index 0000000..0aa0491 --- /dev/null +++ b/CollabVMSharp/Mouse.cs @@ -0,0 +1,38 @@ +namespace CollabVMSharp; + +public class Mouse { + public bool Left { get; set; } + public bool Middle { get; set; } + public bool Right { get; set; } + public int X { get; set; } + public int Y { get; set; } + public Mouse() { + Left = false; + Middle = false; + Right = false; + X = 0; + Y = 0; + } + public int MakeMask() { + int mask = 0; + if (this.Left) mask |= 1; + if (this.Middle) mask |= 2; + if (this.Right) mask |= 4; + return mask; + } + + public void LoadMask(int mask) { + this.Left = false; + this.Middle = false; + this.Right = false; + if ((mask & 1) != 0) this.Left = true; + if ((mask & 2) != 0) this.Middle = true; + if ((mask & 4) != 0) this.Right = true; + } +} + +public enum MouseButton { + Left, + Middle, + Right +} \ No newline at end of file diff --git a/CollabVMSharp/Node.cs b/CollabVMSharp/Node.cs new file mode 100644 index 0000000..f425375 --- /dev/null +++ b/CollabVMSharp/Node.cs @@ -0,0 +1,19 @@ +namespace CollabVMSharp; + +/// +/// A VM recieved from the list opcode +/// +public class Node { + /// + /// ID of the VM + /// + public string ID { get; set; } + /// + /// Display name of the VM. May contain HTML. + /// + public string Name { get; set; } + /// + /// JPEG thumbnail of the VM, usually in 400x300 resolution + /// + public byte[] Thumbnail { get; set; } +} \ No newline at end of file diff --git a/CollabVMSharp/Permissions.cs b/CollabVMSharp/Permissions.cs new file mode 100644 index 0000000..ea32d5a --- /dev/null +++ b/CollabVMSharp/Permissions.cs @@ -0,0 +1,47 @@ +namespace CollabVMSharp; + +public class Permissions { + private bool restore; + private bool reboot; + private bool ban; + private bool kick; + private bool mute; + private bool forcevote; + private bool bypassendturn; + private bool rename; + private bool getip; + private bool xss; + + public bool Restore { get { return restore; } } + public bool Reboot { get { return reboot; } } + public bool Ban { get { return ban; } } + public bool Kick { get { return kick; } } + public bool Mute { get { return mute; } } + public bool ForceVote { get { return forcevote; } } + public bool BypassAndEndTurns { get { return bypassendturn; } } + public bool Rename { get { return rename; } } + public bool GetIP { get { return getip; } } + public bool XSS { get { return xss; } } + + public Permissions(int permissionvalue) { + if ((permissionvalue & 1) != 0) restore = true; + if ((permissionvalue & 2) != 0) reboot = true; + if ((permissionvalue & 4) != 0) ban = true; + if ((permissionvalue & 8) != 0) forcevote = true; + if ((permissionvalue & 16) != 0) mute = true; + if ((permissionvalue & 32) != 0) kick = true; + if ((permissionvalue & 64) != 0) bypassendturn = true; + if ((permissionvalue & 128) != 0) rename = true; + if ((permissionvalue & 256) != 0) getip = true; + if ((permissionvalue & 512) != 0) xss = true; + } + + public static Permissions All => new Permissions(65535); + public static Permissions None => new Permissions(0); +} + +public enum Rank { + Unregistered, + Moderator, + Admin +} \ No newline at end of file diff --git a/CollabVMSharp/TurnStatus.cs b/CollabVMSharp/TurnStatus.cs new file mode 100644 index 0000000..1d47239 --- /dev/null +++ b/CollabVMSharp/TurnStatus.cs @@ -0,0 +1,7 @@ +namespace CollabVMSharp; + +public enum TurnStatus { + None, + Waiting, + HasTurn +} \ No newline at end of file diff --git a/CollabVMSharp/User.cs b/CollabVMSharp/User.cs new file mode 100644 index 0000000..7c0a0a1 --- /dev/null +++ b/CollabVMSharp/User.cs @@ -0,0 +1,7 @@ +namespace CollabVMSharp; + +public class User { + public string Username { get; set; } + public Rank Rank { get; set; } + public TurnStatus Turn { get; set; } +} \ No newline at end of file diff --git a/global.json b/global.json new file mode 100644 index 0000000..1bcf6c0 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "6.0.0", + "rollForward": "latestMinor", + "allowPrerelease": false + } +} \ No newline at end of file