diff --git a/XIVChat Desktop/App.xaml.cs b/XIVChat Desktop/App.xaml.cs index a0388dc..fa071cb 100644 --- a/XIVChat Desktop/App.xaml.cs +++ b/XIVChat Desktop/App.xaml.cs @@ -129,12 +129,12 @@ namespace XIVChat_Desktop { this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.Connected))); } - public void Connect(string host, ushort port) { + public void Connect(string host, ushort port, string? relayAuth, string? relayTarget) { if (this.Connected) { return; } - this.Connection = new Connection(this, host, port); + this.Connection = new Connection(this, host, port, relayAuth, relayTarget); this.Connection.ReceiveMessage += this.OnReceiveMessage; Task.Run(this.Connection.Connect); } diff --git a/XIVChat Desktop/Configuration.cs b/XIVChat Desktop/Configuration.cs index 11a6d88..5ef52a8 100644 --- a/XIVChat Desktop/Configuration.cs +++ b/XIVChat Desktop/Configuration.cs @@ -122,44 +122,64 @@ namespace XIVChat_Desktop { [JsonObject] public class SavedServer : INotifyPropertyChanged { - private string name; - private string host; - private ushort port; + private string _name; + private string _host; + private ushort _port; + private string? _relayAuth; + private string? _relayTarget; public string Name { - get => this.name; + get => this._name; set { - this.name = value; + this._name = value; this.OnPropertyChanged(nameof(this.Name)); } } public string Host { - get => this.host; + get => this._host; set { - this.host = value; + this._host = value; this.OnPropertyChanged(nameof(this.Host)); } } public ushort Port { - get => this.port; + get => this._port; set { - this.port = value; + this._port = value; this.OnPropertyChanged(nameof(this.Port)); } } + public string? RelayAuth { + get => this._relayAuth; + set { + this._relayAuth = value; + this.OnPropertyChanged(nameof(this.RelayAuth)); + } + } + + public string? RelayTarget { + get => this._relayTarget; + set { + this._relayTarget = value; + this.OnPropertyChanged(nameof(this.RelayTarget)); + } + } + public event PropertyChangedEventHandler? PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) { this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } - public SavedServer(string name, string host, ushort port) { - this.name = name; - this.host = host; - this.port = port; + public SavedServer(string name, string host, ushort port, string? relayAuth, string? relayTarget) { + this._name = name; + this._host = host; + this._port = port; + this._relayAuth = relayAuth; + this._relayTarget = relayTarget; } protected bool Equals(SavedServer other) { diff --git a/XIVChat Desktop/ConnectDialog.xaml.cs b/XIVChat Desktop/ConnectDialog.xaml.cs index bcfce4d..8e27999 100644 --- a/XIVChat Desktop/ConnectDialog.xaml.cs +++ b/XIVChat Desktop/ConnectDialog.xaml.cs @@ -31,7 +31,7 @@ namespace XIVChat_Desktop { return; } - this.App.Connect(server.Host, server.Port); + this.App.Connect(server.Host, server.Port, server.RelayAuth, server.RelayTarget); this.Close(); } diff --git a/XIVChat Desktop/Connection.cs b/XIVChat Desktop/Connection.cs index d99c6fb..ef73a0c 100644 --- a/XIVChat Desktop/Connection.cs +++ b/XIVChat Desktop/Connection.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Linq; using System.Net.Sockets; using System.Security.Cryptography; +using System.Text; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; @@ -16,10 +17,30 @@ using XIVChatCommon.Message.Server; namespace XIVChat_Desktop { public class Connection : INotifyPropertyChanged { + #if DEBUG + private static readonly IEnumerable RelayPublicKey = new byte[] { + 8, 202, 178, 253, 125, 176, 212, 227, 110, 108, 113, 80, 110, 126, 57, 248, 182, 251, 122, 48, 80, 49, 57, 202, 119, 126, 69, 66, 170, 25, 126, 115, + }; + #else + private static readonly IEnumerable RelayPublicKey = new byte[] { + 194, 81, 22, 123, 80, 172, 145, 167, 212, 251, 198, 173, 55, 160, 11, 18, 247, 11, 210, 6, 98, 43, 102, 73, 54, 255, 214, 233, 144, 193, 98, 47 + }; + #endif + + #if DEBUG + private const string RelayHost = "localhost"; + private const ushort RelayPort = 14725; + #else + private const string RelayHost = "relay.xiv.chat"; + private const ushort RelayPort = 14777; + #endif + private readonly App app; private readonly string host; private readonly ushort port; + private readonly string? relayAuth; + private readonly string? relayTarget; private TcpClient? client; @@ -47,11 +68,13 @@ namespace XIVChat_Desktop { } } - public Connection(App app, string host, ushort port) { + public Connection(App app, string host, ushort port, string? relayAuth = null, string? relayTarget = null) { this.app = app; this.host = host; this.port = port; + this.relayAuth = relayAuth; + this.relayTarget = relayTarget; } public void SendMessage(string message) { @@ -80,13 +103,49 @@ namespace XIVChat_Desktop { } public async Task Connect() { - this.client = new TcpClient(this.host, this.port); + var usingRelay = this.relayAuth != null; + + var host = usingRelay ? RelayHost : this.host; + var port = usingRelay ? RelayPort : this.port; + + this.client = new TcpClient(host, port); + var stream = this.client.GetStream(); - // write the magic bytes - await stream.WriteAsync(new byte[] { - 14, 20, 67, - }); + switch (usingRelay) { + // do relay auth before connecting if necessary + case true: { + var relayHandshake = await KeyExchange.ClientHandshake(this.app.Config.KeyPair, stream); + + // ensure the relay's public key is what we expect + if (!relayHandshake.RemotePublicKey.SequenceEqual(RelayPublicKey)) { + this.app.Dispatch(() => { + MessageBox.Show("Unexpected relay public key."); + }); + return; + } + + // send auth token + var authBytes = Encoding.UTF8.GetBytes(this.relayAuth!); + await SecretMessage.SendSecretMessage(stream, relayHandshake.Keys.tx, authBytes); + + // TODO: receive response + + // send the public key of the server + var pk = Util.StringToByteArray(this.relayTarget!); + await SecretMessage.SendSecretMessage(stream, relayHandshake.Keys.tx, pk); + + // TODO: receive response + break; + } + // only send magic bytes if not using the relay + case false: + // write the magic bytes + await stream.WriteAsync(new byte[] { + 14, 20, 67, + }); + break; + } // do the handshake var handshake = await KeyExchange.ClientHandshake(this.app.Config.KeyPair, stream); diff --git a/XIVChat Desktop/ManageServer.xaml b/XIVChat Desktop/ManageServer.xaml index 2ac1ac9..8820f87 100644 --- a/XIVChat Desktop/ManageServer.xaml +++ b/XIVChat Desktop/ManageServer.xaml @@ -17,6 +17,9 @@ + + + @@ -27,39 +30,75 @@ + + Direct + Relay + + + + + + + + + diff --git a/XIVChat Desktop/ManageServer.xaml.cs b/XIVChat Desktop/ManageServer.xaml.cs index e00c70c..31e5f35 100644 --- a/XIVChat Desktop/ManageServer.xaml.cs +++ b/XIVChat Desktop/ManageServer.xaml.cs @@ -5,7 +5,7 @@ namespace XIVChat_Desktop { /// Interaction logic for ManageServer.xaml /// public partial class ManageServer { - public App App => (App)Application.Current; + public App App => (App) Application.Current; public SavedServer? Server { get; private set; } private readonly bool isNewServer; @@ -35,6 +35,16 @@ namespace XIVChat_Desktop { private void Save_Click(object sender, RoutedEventArgs e) { var serverName = this.ServerName.Text; var serverHost = this.ServerHost.Text; + var relayAuth = this.RelayAuth.Text.Trim(); + var relayTarget = this.RelayTarget.Text.Trim(); + + if (relayAuth.Length == 0) { + relayAuth = null; + } + + if (relayTarget.Length == 0) { + relayTarget = null; + } if (serverName.Length == 0 || serverHost.Length == 0) { MessageBox.Show("Server must have a name and host."); @@ -55,13 +65,18 @@ namespace XIVChat_Desktop { this.Server = new SavedServer( serverName, serverHost, - port + port, + relayAuth, + relayTarget ); + this.App.Config.Servers.Add(this.Server); } else { this.Server!.Name = serverName; this.Server.Host = serverHost; this.Server.Port = port; + this.Server.RelayAuth = relayAuth; + this.Server.RelayTarget = relayTarget; } this.App.Config.Save(); diff --git a/XIVChat Desktop/ServerScan.xaml.cs b/XIVChat Desktop/ServerScan.xaml.cs index ea9a5a9..cffa204 100644 --- a/XIVChat Desktop/ServerScan.xaml.cs +++ b/XIVChat Desktop/ServerScan.xaml.cs @@ -72,7 +72,7 @@ namespace XIVChat_Desktop { continue; } - var saved = new SavedServer(server.playerName, server.address, server.port); + var saved = new SavedServer(server.playerName, server.address, server.port, null, null); if (this.Servers.Contains(saved)) { continue; } diff --git a/XIVChat Desktop/Util.cs b/XIVChat Desktop/Util.cs index d3b9051..777562e 100644 --- a/XIVChat Desktop/Util.cs +++ b/XIVChat Desktop/Util.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Globalization; +using System.Linq; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using System.Windows.Controls; @@ -9,6 +10,14 @@ using System.Windows.Threading; namespace XIVChat_Desktop { public static class Util { + // GOOD HEAVENS! I REALISE HOW INEFFICIENT THIS IS + public static byte[] StringToByteArray(string hex) { + return Enumerable.Range(0, hex.Length) + .Where(x => x % 2 == 0) + .Select(x => Convert.ToByte(hex.Substring(x, 2), 16)) + .ToArray(); + } + public static IEnumerable> Chunks(this List locations, int nSize) { for (var i = 0; i < locations.Count; i += nSize) { yield return locations.GetRange(i, Math.Min(nSize, locations.Count - i)); diff --git a/XIVChatCommon/KeyExchange.cs b/XIVChatCommon/KeyExchange.cs index 73eebe3..9d02de8 100644 --- a/XIVChatCommon/KeyExchange.cs +++ b/XIVChatCommon/KeyExchange.cs @@ -54,6 +54,7 @@ namespace XIVChatCommon { // send our public key await stream.WriteAsync(server.PublicKey, 0, server.PublicKey.Length); + await stream.FlushAsync(); // get shared secret and derive keys var keys = ServerSessionKeys(server, clientPublic); diff --git a/XIVChatCommon/Message/Relay/Relay.cs b/XIVChatCommon/Message/Relay/Relay.cs new file mode 100644 index 0000000..367061f --- /dev/null +++ b/XIVChatCommon/Message/Relay/Relay.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using MessagePack; + +namespace XIVChatCommon.Message.Relay { + [Union(1, typeof(RelayRegister))] + [Union(2, typeof(RelayedMessage))] + public interface IToRelay { + } + + [MessagePackObject] + public class RelayRegister : IToRelay { + [Key(0)] + public string AuthToken { get; set; } + + [Key(1)] + public byte[] PublicKey { get; set; } + } + + [Union(1, typeof(RelaySuccess))] + [Union(2, typeof(RelayNewClient))] + [Union(3, typeof(RelayedMessage))] + public interface IFromRelay { + } + + [MessagePackObject] + public class RelaySuccess : IFromRelay { + [Key(0)] + public bool Success { get; set; } + + [Key(1)] + public string? Info { get; set; } + } + + [MessagePackObject] + public class RelayNewClient : IFromRelay { + [Key(0)] + public List PublicKey { get; set; } + + [Key(1)] + public string Address { get; set; } + } + + [MessagePackObject] + public class RelayedMessage : IFromRelay, IToRelay { + [Key(0)] + public List PublicKey { get; set; } + + [Key(1)] + public List Message { get; set; } + } +} diff --git a/XIVChatCommon/SecretMessage.cs b/XIVChatCommon/SecretMessage.cs index b185fc4..b67d9a5 100644 --- a/XIVChatCommon/SecretMessage.cs +++ b/XIVChatCommon/SecretMessage.cs @@ -46,6 +46,7 @@ namespace XIVChatCommon { await s.WriteAsync(len, 0, len.Length, token); await s.WriteAsync(nonce, 0, nonce.Length, token); await s.WriteAsync(ciphertext, 0, ciphertext.Length, token); + await s.FlushAsync(token); } public async static Task SendSecretMessage(Stream s, byte[] key, IEncodable message, CancellationToken token = default) { diff --git a/XIVChatPlugin/Client.cs b/XIVChatPlugin/Client.cs new file mode 100644 index 0000000..a9e4327 --- /dev/null +++ b/XIVChatPlugin/Client.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using XIVChatCommon; +using XIVChatCommon.Message.Client; +using XIVChatCommon.Message.Relay; +using XIVChatCommon.Message.Server; + +namespace XIVChatPlugin { + public abstract class BaseClient : Stream { + public virtual bool Connected { get; set; } + + public HandshakeInfo? Handshake { get; set; } + + public ClientPreferences? Preferences { get; set; } + + public IPAddress? Remote { get; set; } + + public CancellationTokenSource TokenSource { get; } = new CancellationTokenSource(); + + public Channel Queue { get; } = Channel.CreateUnbounded(); + + public void Disconnect() { + this.Connected = false; + this.TokenSource.Cancel(); + + try { + this.Close(); + } catch (ObjectDisposedException) { + // ignored + } + } + + public T GetPreference(ClientPreference pref, T def = default) { + var prefs = this.Preferences; + + if (prefs == null) { + return def; + } + + return prefs.TryGetValue(pref, out T result) ? result : def; + } + } + + public sealed class TcpConnected : BaseClient { + private TcpClient Client { get; } + private readonly Stream _streamImplementation; + private bool _connected; + + public override bool Connected { + get { + var ret = this._connected; + try { + ret = ret && this.Client.Connected; + } catch (ObjectDisposedException) { + return false; + } + + return ret; + } + set => this._connected = value; + } + + public TcpConnected(TcpClient client) { + this.Client = client; + + this.Client.ReceiveTimeout = 5_000; + this.Client.SendTimeout = 5_000; + + this.Client.Client.ReceiveTimeout = 5_000; + this.Client.Client.SendTimeout = 5_000; + + if (this.Client.Client.RemoteEndPoint is IPEndPoint endPoint) { + this.Remote = endPoint.Address; + } + + this.Connected = this.Client.Connected; + this._streamImplementation = this.Client.GetStream(); + } + + public override void Flush() { + this._streamImplementation.Flush(); + } + + public override long Seek(long offset, SeekOrigin origin) { + return this._streamImplementation.Seek(offset, origin); + } + + public override void SetLength(long value) { + this._streamImplementation.SetLength(value); + } + + public override int Read(byte[] buffer, int offset, int count) { + return this._streamImplementation.Read(buffer, offset, count); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { + return this._streamImplementation.ReadAsync(buffer, offset, count, cancellationToken); + } + + public override void Write(byte[] buffer, int offset, int count) { + this._streamImplementation.Write(buffer, offset, count); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { + return this._streamImplementation.WriteAsync(buffer, offset, count, cancellationToken); + } + + public override bool CanRead => this._streamImplementation.CanRead; + + public override bool CanSeek => this._streamImplementation.CanSeek; + + public override bool CanWrite => this._streamImplementation.CanWrite; + + public override long Length => this._streamImplementation.Length; + + public override long Position { + get => this._streamImplementation.Position; + set => this._streamImplementation.Position = value; + } + } + + public sealed class RelayConnected : BaseClient { + internal byte[] PublicKey { get; } + + private ChannelWriter ToRelay { get; } + private Channel FromRelay { get; } + + internal ChannelWriter FromRelayWriter => this.FromRelay.Writer; + + private List ReadBuffer { get; } = new List(); + private List WriteBuffer { get; } = new List(); + + public RelayConnected(byte[] publicKey, IPAddress remote, ChannelWriter toRelay, Channel fromRelay) { + this.PublicKey = publicKey; + this.Remote = remote; + this.Connected = true; + this.ToRelay = toRelay; + this.FromRelay = fromRelay; + } + + public override void Flush() { + if (this.WriteBuffer.Count == 0) { + return; + } + + var message = new RelayedMessage { + PublicKey = this.PublicKey.ToList(), + Message = this.WriteBuffer.ToList(), + }; + this.WriteBuffer.Clear(); + + // write the contents of the write buffer to the relay + this.ToRelay.WriteAsync(message).AsTask().Wait(); + } + + public override long Seek(long offset, SeekOrigin origin) { + throw new NotSupportedException(); + } + + public override void SetLength(long value) { + throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) { + var read = 0; + + // if there are bytes in the buffer, take from them first + if (this.ReadBuffer.Count > 0) { + // determine how many bytes to take from the buffer + var toRead = count > this.ReadBuffer.Count ? this.ReadBuffer.Count : count; + + // copy bytes, then remove them + this.ReadBuffer.CopyTo(0, buffer, offset, toRead); + this.ReadBuffer.RemoveRange(0, toRead); + // increment the read count + read += toRead; + } + + // if we've read everything, return + if (read == count) { + return read; + } + + // get new bytes + var readTask = this.FromRelay.Reader.ReadAsync().AsTask(); + readTask.Wait(); + var bytes = readTask.Result; + + // add new bytes to buffer + this.ReadBuffer.AddRange(bytes); + + // and keep going + return read + this.Read(buffer, offset + read, count - read); + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { + var read = 0; + + // if there are bytes in the buffer, take from them first + if (this.ReadBuffer.Count > 0) { + // determine how many bytes to take from the buffer + var toRead = count > this.ReadBuffer.Count ? this.ReadBuffer.Count : count; + + // copy bytes, then remove them + this.ReadBuffer.CopyTo(0, buffer, offset, toRead); + this.ReadBuffer.RemoveRange(0, toRead); + // increment the read count + read += toRead; + } + + // if we've read everything, return + if (read == count) { + return read; + } + + // get new bytes + var bytes = await this.FromRelay.Reader.ReadAsync(cancellationToken); + + // add new bytes to buffer + this.ReadBuffer.AddRange(bytes); + + // and keep going + return read + await this.ReadAsync(buffer, offset + read, count - read, cancellationToken); + } + + public override void Write(byte[] buffer, int offset, int count) { + // create a new array of the bytes to send + var bytes = new byte[count]; + // copy bytes over + Array.Copy(buffer, 0, bytes, 0, count); + // push them into the write buffer + this.WriteBuffer.AddRange(bytes); + } + + public override bool CanRead => true; + public override bool CanWrite => true; + public override bool CanSeek => false; + public override long Length => throw new NotSupportedException(); + + public override long Position { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + } +} diff --git a/XIVChatPlugin/Configuration.cs b/XIVChatPlugin/Configuration.cs index fc8eb14..ecf6215 100644 --- a/XIVChatPlugin/Configuration.cs +++ b/XIVChatPlugin/Configuration.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; namespace XIVChatPlugin { public class Configuration : IPluginConfiguration { - private Plugin? plugin; + private Plugin? _plugin; public int Version { get; set; } = 1; public ushort Port { get; set; } = 14777; @@ -19,15 +19,18 @@ namespace XIVChatPlugin { public bool AcceptNewClients { get; set; } = true; + public bool AllowRelayConnections { get; set; } + public string? RelayAuth { get; set; } + public Dictionary> TrustedKeys { get; set; } = new Dictionary>(); public KeyPair? KeyPair { get; set; } public void Initialise(Plugin plugin) { - this.plugin = plugin ?? throw new ArgumentNullException(nameof(plugin), "Plugin cannot be null"); + this._plugin = plugin ?? throw new ArgumentNullException(nameof(plugin), "Plugin cannot be null"); } public void Save() { - this.plugin?.Interface.SavePluginConfig(this); + this._plugin?.Interface.SavePluginConfig(this); } } } diff --git a/XIVChatPlugin/GameFunctions.cs b/XIVChatPlugin/GameFunctions.cs index 6913772..06121a9 100644 --- a/XIVChatPlugin/GameFunctions.cs +++ b/XIVChatPlugin/GameFunctions.cs @@ -69,7 +69,7 @@ namespace XIVChatPlugin { var formatPtr = this.Plugin.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 41 56 48 83 EC 30 48 8B 6C 24 ??"); var recvChunkPtr = this.Plugin.ScanText("48 89 5C 24 ?? 56 48 83 EC 20 48 8B 0D ?? ?? ?? ?? 48 8B F2"); var getColourPtr = this.Plugin.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 8B F2 48 8D B9 ?? ?? ?? ??"); - var channelPtr = this.Plugin.ScanText("40 55 48 8D 6C 24 ?? 48 81 EC A0 00 00 00 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 45 ?? 48 8B 0D ?? ?? ?? ?? 33 C0 48 83 C1 10 89 45 ?? C7 45 ?? 01 00 00 00"); + var channelPtr = this.Plugin.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 85 D2 BB ?? ?? ?? ??"); var channelCommandPtr = this.Plugin.ScanText("E8 ?? ?? ?? ?? 0F B7 44 37 ??"); var xivStringCtorPtr = this.Plugin.ScanText("E8 ?? ?? ?? ?? 44 2B F7"); var xivStringDtorPtr = this.Plugin.ScanText("E8 ?? ?? ?? ?? B0 6E"); diff --git a/XIVChatPlugin/ILRepack.targets b/XIVChatPlugin/ILRepack.targets index 294f09c..d20acee 100644 --- a/XIVChatPlugin/ILRepack.targets +++ b/XIVChatPlugin/ILRepack.targets @@ -6,6 +6,7 @@ + this._showSettings; set => this._showSettings = value; } - private readonly Dictionary>> _pending = new Dictionary>>(); + private bool ShowSettings { + get => this._showSettings; + set => this._showSettings = value; + } + + private readonly Dictionary>> _pending = new Dictionary>>(); private readonly Dictionary _pendingNames = new Dictionary(0); public PluginUi(Plugin plugin) { @@ -42,9 +46,11 @@ namespace XIVChatPlugin { ImGui.PushStyleColor(ImGuiCol.ButtonActive, Colours.ButtonActive); ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Colours.ButtonHovered); - this.DrawInner(); - - ImGui.PopStyleColor(8); + try { + this.DrawInner(); + } finally { + ImGui.PopStyleColor(8); + } } private static T WithWhiteText(Func func) { @@ -107,6 +113,12 @@ namespace XIVChatPlugin { if (WithWhiteText(() => ImGui.Button("Regenerate"))) { this.Plugin.Server.RegenerateKeyPair(); } + + ImGui.SameLine(); + + if (WithWhiteText(() => ImGui.Button("Copy"))) { + ImGui.SetClipboardText(serverPublic); + } } if (WithWhiteText(() => ImGui.CollapsingHeader("Settings", ImGuiTreeNodeFlags.DefaultOpen))) { @@ -114,7 +126,7 @@ namespace XIVChatPlugin { int port = this.Plugin.Config.Port; if (WithWhiteText(() => ImGui.InputInt("##port", ref port))) { - var realPort = (ushort)Math.Min(ushort.MaxValue, Math.Max(1, port)); + var realPort = (ushort) Math.Min(ushort.MaxValue, Math.Max(1, port)); this.Plugin.Config.Port = realPort; this.Plugin.Config.Save(); @@ -131,7 +143,7 @@ namespace XIVChatPlugin { int backlogCount = this.Plugin.Config.BacklogCount; if (WithWhiteText(() => ImGui.DragInt("Backlog messages", ref backlogCount, 1f, 0, ushort.MaxValue))) { - this.Plugin.Config.BacklogCount = (ushort)Math.Max(0, Math.Min(ushort.MaxValue, backlogCount)); + this.Plugin.Config.BacklogCount = (ushort) Math.Max(0, Math.Min(ushort.MaxValue, backlogCount)); this.Plugin.Config.Save(); } @@ -167,6 +179,40 @@ namespace XIVChatPlugin { ImGui.SameLine(); HelpMarker("If this is disabled, XIVChat Server will only allow clients with already-trusted keys to connect."); + + ImGui.Spacing(); + + var allowRelay = this.Plugin.Config.AllowRelayConnections; + if (WithWhiteText(() => ImGui.Checkbox("Allow relay connections", ref allowRelay))) { + if (allowRelay) { + this.Plugin.StartRelay(); + } else { + this.Plugin.StopRelay(); + } + + this.Plugin.Config.AllowRelayConnections = allowRelay; + this.Plugin.Config.Save(); + } + + ImGui.SameLine(); + HelpMarker("If this is enabled, connections from the XIVChat Relay will be accepted."); + + ImGui.Spacing(); + + var relayAuth = this.Plugin.Config.RelayAuth ?? ""; + WithWhiteText(() => ImGui.TextUnformatted("Relay authentication code")); + ImGui.PushItemWidth(-1f); + if (ImGui.InputText("###relay-auth", ref relayAuth, 32)) { + relayAuth = relayAuth.Trim(); + if (relayAuth.Length == 0) { + relayAuth = null; + } + + this.Plugin.Config.RelayAuth = relayAuth; + this.Plugin.Config.Save(); + } + + ImGui.PopItemWidth(); } if (WithWhiteText(() => ImGui.CollapsingHeader("Trusted keys"))) { @@ -208,7 +254,10 @@ namespace XIVChatPlugin { if (WithWhiteText(() => ImGui.CollapsingHeader("Connected clients"))) { - if (this.Plugin.Server.Clients.Count == 0) { + var clients = this.Plugin.Server.Clients + .Where(client => client.Value.Connected) + .ToList(); + if (clients.Count == 0) { ImGui.TextUnformatted("None"); } else { ImGui.Columns(3); @@ -219,26 +268,29 @@ namespace XIVChatPlugin { ImGui.NextColumn(); ImGui.NextColumn(); - foreach (var client in this.Plugin.Server.Clients) { - EndPoint remote; + foreach (var client in clients) { + if (!client.Value.Connected) { + continue; + } + + IPAddress? remote; try { - remote = client.Value.Conn.Client.RemoteEndPoint; + remote = client.Value.Remote; } catch (ObjectDisposedException) { continue; } - string ipAddress; - if (remote is IPEndPoint ip) { - ipAddress = ip.Address.ToString(); - } else { - ipAddress = "Unknown"; + var ipAddress = remote?.ToString() ?? "Unknown"; + + if (client.Value is RelayConnected) { + ipAddress += " (R)"; } ImGui.TextUnformatted(ipAddress); ImGui.NextColumn(); - var trustedKey = this.Plugin.Config.TrustedKeys.Values.FirstOrDefault(entry => entry.Item2.SequenceEqual(client.Value.Handshake!.RemotePublicKey)); + var trustedKey = this.Plugin.Config.TrustedKeys.Values.FirstOrDefault(entry => client.Value.Handshake != null && entry.Item2.SequenceEqual(client.Value.Handshake.RemotePublicKey)); if (trustedKey != null && !trustedKey.Equals(default(Tuple))) { ImGui.TextUnformatted(trustedKey!.Item1); if (ImGui.IsItemHovered()) { @@ -316,12 +368,12 @@ namespace XIVChatPlugin { } private void AcceptPending() { - while (this.Plugin.Server.pendingClients.Reader.TryRead(out var item)) { + while (this.Plugin.Server.PendingClients.Reader.TryRead(out var item)) { this._pending[Guid.NewGuid()] = item; } } - private bool DrawPending(Guid id, Client client, Channel accepted) { + private bool DrawPending(Guid id, BaseClient client, Channel accepted) { var ret = false; var clientPublic = client.Handshake!.RemotePublicKey; diff --git a/XIVChatPlugin/Relay.cs b/XIVChatPlugin/Relay.cs new file mode 100644 index 0000000..c18a9f2 --- /dev/null +++ b/XIVChatPlugin/Relay.cs @@ -0,0 +1,145 @@ +using System; +using System.Linq; +using System.Net; +using System.Threading.Channels; +using System.Threading.Tasks; +using Dalamud.Plugin; +using MessagePack; +using WebSocketSharp; +using XIVChatCommon.Message.Relay; + +namespace XIVChatPlugin { + public class Relay : IDisposable { + #if DEBUG + private const string RelayUrl = "ws://localhost:14555/"; + #else + private const string RelayUrl = "wss://relay.xiv.chat/"; + #endif + + private Plugin Plugin { get; } + + private WebSocket Connection { get; } + + private bool Running { get; set; } + + private Channel ToRelay { get; } = Channel.CreateUnbounded(); + + internal Relay(Plugin plugin) { + this.Plugin = plugin; + + this.Connection = new WebSocket(RelayUrl) { + SslConfiguration = { + EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls12, + }, + }; + PluginLog.Log($"using {RelayUrl}"); + this.Connection.OnOpen += this.OnOpen; + this.Connection.OnMessage += this.OnMessage; + this.Connection.OnClose += this.OnClose; + this.Connection.OnError += this.OnError; + } + + public void Dispose() { + ((IDisposable) this.Connection).Dispose(); + } + + internal void Start() { + if (this.Plugin.Config.RelayAuth == null) { + return; + } + + this.Running = true; + + this.Connection.ConnectAsync(); + PluginLog.Log("ran connect"); + } + + internal void ResendPublicKey() { + var keys = this.Plugin.Config.KeyPair; + if (keys == null) { + return; + } + + var pk = keys.PublicKey.ToHexString(); + + this.Connection.Send(pk); + } + + private void OnOpen(object sender, EventArgs e) { + PluginLog.Log("onopen"); + var auth = this.Plugin.Config.RelayAuth; + if (auth == null) { + PluginLog.Log("auth null"); + return; + } + + var keys = this.Plugin.Config.KeyPair; + if (keys == null) { + PluginLog.Log("keys null"); + return; + } + + PluginLog.Log("making registration message"); + var message = new RelayRegister { + AuthToken = auth, + PublicKey = keys.PublicKey, + }; + PluginLog.Log("serialising"); + var bytes = MessagePackSerializer.Serialize((IToRelay) message); + + PluginLog.Log("sending"); + this.Connection.Send(bytes); + PluginLog.Log("sent registration"); + + Task.Run(async () => { + while (this.Running) { + var message = await this.ToRelay.Reader.ReadAsync(); + var bytes = MessagePackSerializer.Serialize(message); + + this.Connection.Send(bytes); + } + }); + } + + private void OnMessage(object sender, MessageEventArgs e) { + PluginLog.Log(e.RawData.ToHexString()); + var message = MessagePackSerializer.Deserialize(e.RawData); + switch (message) { + case RelaySuccess success: + if (!success.Success) { + this.Plugin.StopRelay(); + } + + break; + case RelayNewClient newClient: + IPAddress.TryParse(newClient.Address, out var remote); + var client = new RelayConnected( + newClient.PublicKey.ToArray(), + remote, + this.ToRelay.Writer, + Channel.CreateUnbounded() + ); + + this.Plugin.Server.SpawnClientTask(client, false); + break; + case RelayedMessage relayed: + var relayedClient = this.Plugin.Server.Clients.Values + .Where(client => client is RelayConnected) + .Cast() + .FirstOrDefault(client => client.PublicKey.SequenceEqual(relayed.PublicKey)); + + relayedClient?.FromRelayWriter.WriteAsync(relayed.Message.ToArray()).AsTask().Wait(); + break; + } + } + + private void OnClose(object sender, CloseEventArgs e) { + // TODO ? + } + + private void OnError(object sender, ErrorEventArgs e) { + PluginLog.LogError(e.Exception, e.Message); + // TODO ? + } + } +} diff --git a/XIVChatPlugin/Server.cs b/XIVChatPlugin/Server.cs index 2dccb17..8f9446c 100644 --- a/XIVChatPlugin/Server.cs +++ b/XIVChatPlugin/Server.cs @@ -9,7 +9,6 @@ using Sodium; using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; @@ -24,37 +23,37 @@ using XIVChatCommon.Message.Server; namespace XIVChatPlugin { public class Server : IDisposable { - private readonly Plugin plugin; + private readonly Plugin _plugin; - private readonly CancellationTokenSource tokenSource = new CancellationTokenSource(); - private readonly ConcurrentQueue toGame = new ConcurrentQueue(); + private readonly CancellationTokenSource _tokenSource = new CancellationTokenSource(); + private readonly ConcurrentQueue _toGame = new ConcurrentQueue(); - private readonly ConcurrentDictionary clients = new ConcurrentDictionary(); - public IReadOnlyDictionary Clients => this.clients; - public readonly Channel>> pendingClients = Channel.CreateUnbounded>>(); + private readonly ConcurrentDictionary _clients = new ConcurrentDictionary(); + public IReadOnlyDictionary Clients => this._clients; + public readonly Channel>> PendingClients = Channel.CreateUnbounded>>(); - private readonly HashSet waitingForFriendList = new HashSet(); + private readonly HashSet _waitingForFriendList = new HashSet(); - private readonly LinkedList backlog = new LinkedList(); + private readonly LinkedList _backlog = new LinkedList(); - private TcpListener? listener; + private TcpListener? _listener; - private bool sendPlayerData; + private bool _sendPlayerData; - private volatile bool running; - private bool Running => this.running; + private volatile bool _running; + private bool Running => this._running; - private InputChannel currentChannel = InputChannel.Say; + private InputChannel _currentChannel = InputChannel.Say; private const int MaxMessageSize = 128_000; public Server(Plugin plugin) { - this.plugin = plugin ?? throw new ArgumentNullException(nameof(plugin), "Plugin cannot be null"); - if (this.plugin.Config.KeyPair == null) { + this._plugin = plugin ?? throw new ArgumentNullException(nameof(plugin), "Plugin cannot be null"); + if (this._plugin.Config.KeyPair == null) { this.RegenerateKeyPair(); } - this.plugin.Functions.ReceiveFriendList += this.OnReceiveFriendList; + this._plugin.Functions.ReceiveFriendList += this.OnReceiveFriendList; } private void SpawnPairingModeTask() { @@ -75,12 +74,12 @@ namespace XIVChatPlugin { Task? receiveTask = null; while (this.Running) { - if (!this.plugin.Config.PairingMode) { + if (!this._plugin.Config.PairingMode) { await Task.Delay(5_000); continue; } - var playerName = this.plugin.Interface.ClientState.LocalPlayer?.Name; + var playerName = this._plugin.Interface.ClientState.LocalPlayer?.Name; if (playerName != null) { lastPlayerName = playerName; @@ -115,8 +114,8 @@ namespace XIVChatPlugin { } var utf8 = Encoding.UTF8.GetBytes(lastPlayerName); - var portBytes = BitConverter.GetBytes(this.plugin.Config.Port).Reverse().ToArray(); - var key = this.plugin.Config.KeyPair!.PublicKey; + var portBytes = BitConverter.GetBytes(this._plugin.Config.Port).Reverse().ToArray(); + var key = this._plugin.Config.KeyPair!.PublicKey; // magic + string length + string + port + key var payload = new byte[1 + 1 + utf8.Length + portBytes.Length + key.Length]; // assuming names can only be 32 bytes here payload[0] = 14; @@ -135,43 +134,48 @@ namespace XIVChatPlugin { private async void OnReceiveFriendList(List friends) { var msg = new ServerPlayerList(PlayerListType.Friend, friends.ToArray()); - foreach (var id in this.waitingForFriendList) { + foreach (var id in this._waitingForFriendList) { if (!this.Clients.TryGetValue(id, out var client)) { continue; } try { - await SecretMessage.SendSecretMessage(client.Conn.GetStream(), client.Handshake!.Keys.tx, msg, client.TokenSource.Token); + await SecretMessage.SendSecretMessage(client, client.Handshake!.Keys.tx, msg, client.TokenSource.Token); } catch (Exception ex) { PluginLog.LogError($"Could not send message: {ex.Message}"); } } - this.waitingForFriendList.Clear(); + this._waitingForFriendList.Clear(); } public void Spawn() { - var port = this.plugin.Config.Port; + var port = this._plugin.Config.Port; Task.Run(async () => { - this.listener = new TcpListener(IPAddress.Any, port); - this.listener.Start(); + this._listener = new TcpListener(IPAddress.Any, port); + this._listener.Start(); - this.running = true; + this._running = true; PluginLog.Log("Running..."); this.SpawnPairingModeTask(); - while (!this.tokenSource.IsCancellationRequested) { - var conn = await this.listener.GetTcpClient(this.tokenSource); - this.SpawnClientTask(conn); + while (!this._tokenSource.IsCancellationRequested) { + var conn = await this._listener.GetTcpClient(this._tokenSource); + if (conn == null) { + continue; + } + + var client = new TcpConnected(conn); + this.SpawnClientTask(client, true); } - this.running = false; + this._running = false; }); } public void RegenerateKeyPair() { - this.plugin.Config.KeyPair = PublicKeyBox.GenerateKeyPair(); - this.plugin.Config.Save(); + this._plugin.Config.KeyPair = PublicKeyBox.GenerateKeyPair(); + this._plugin.Config.Save(); } [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "delegate")] @@ -182,13 +186,13 @@ namespace XIVChatPlugin { var chatCode = new ChatCode((ushort) type); - if (!this.plugin.Config.SendBattle && chatCode.IsBattle()) { + if (!this._plugin.Config.SendBattle && chatCode.IsBattle()) { return; } var chunks = new List(); - var colour = this.plugin.Functions.GetChannelColour(chatCode) ?? chatCode.DefaultColour(); + var colour = this._plugin.Functions.GetChannelColour(chatCode) ?? chatCode.DefaultColour(); if (sender.Payloads.Count > 0) { var format = this.FormatFor(chatCode.Type); @@ -213,126 +217,119 @@ namespace XIVChatPlugin { chunks ); - this.backlog.AddLast(msg); - while (this.backlog.Count > this.plugin.Config.BacklogCount) { - this.backlog.RemoveFirst(); + this._backlog.AddLast(msg); + while (this._backlog.Count > this._plugin.Config.BacklogCount) { + this._backlog.RemoveFirst(); } - foreach (var client in this.clients.Values) { + foreach (var client in this._clients.Values) { client.Queue.Writer.TryWrite(msg); } } [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "delegate")] public void OnFrameworkUpdate(Framework framework) { - if (this.sendPlayerData && this.plugin.Interface.ClientState.LocalPlayer != null) { + if (this._sendPlayerData && this._plugin.Interface.ClientState.LocalPlayer != null) { this.BroadcastPlayerData(); - this.sendPlayerData = false; + this._sendPlayerData = false; } - if (!this.toGame.TryDequeue(out var message)) { + if (!this._toGame.TryDequeue(out var message)) { return; } - this.plugin.Functions.ProcessChatBox(message); + this._plugin.Functions.ProcessChatBox(message); } private static readonly IReadOnlyList Magic = new byte[] { 14, 20, 67, }; - private void SpawnClientTask(TcpClient? conn) { - if (conn == null) { - return; - } + internal void SpawnClientTask(BaseClient client, bool requiresMagic) { + var id = Guid.NewGuid(); + this._clients[id] = client; Task.Run(async () => { - var stream = conn.GetStream(); + if (requiresMagic) { + // get ready for reading magic bytes + var magic = new byte[Magic.Count]; + var read = 0; - // get ready for reading magic bytes - var magic = new byte[Magic.Count]; - var read = 0; + // only listen for magic for five seconds + using var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromSeconds(5)); - // only listen for magic for five seconds - using var cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromSeconds(5)); + // read magic bytes + while (read < magic.Length) { + if (cts.IsCancellationRequested) { + return; + } - // read magic bytes - while (read < magic.Length) { - if (cts.IsCancellationRequested) { - return; + read += await client.ReadAsync(magic, read, magic.Length - read, cts.Token); } - read += await stream.ReadAsync(magic, read, magic.Length - read, cts.Token); + // ignore this connection if incorrect magic bytes + if (!magic.SequenceEqual(Magic)) { + return; + } } - // ignore this connection if incorrect magic bytes - if (!magic.SequenceEqual(Magic)) { - return; - } - - var handshake = await KeyExchange.ServerHandshake(this.plugin.Config.KeyPair!, stream); - var newClient = new Client(conn) { - Handshake = handshake, - }; + var handshake = await KeyExchange.ServerHandshake(this._plugin.Config.KeyPair!, client); + client.Handshake = handshake; // if this public key isn't trusted, prompt first - if (!this.plugin.Config.TrustedKeys.Values.Any(entry => entry.Item2.SequenceEqual(handshake.RemotePublicKey))) { + if (!this._plugin.Config.TrustedKeys.Values.Any(entry => entry.Item2.SequenceEqual(handshake.RemotePublicKey))) { // if configured to not accept new clients, reject connection - if (!this.plugin.Config.AcceptNewClients) { + if (!this._plugin.Config.AcceptNewClients) { return; } var accepted = Channel.CreateBounded(1); - await this.pendingClients.Writer.WriteAsync(Tuple.Create(newClient, accepted), this.tokenSource.Token); - if (!await accepted.Reader.ReadAsync(this.tokenSource.Token)) { + await this.PendingClients.Writer.WriteAsync(Tuple.Create(client, accepted), this._tokenSource.Token); + if (!await accepted.Reader.ReadAsync(this._tokenSource.Token)) { return; } } - var id = Guid.NewGuid(); - newClient.Connected = true; - this.clients[id] = newClient; + client.Connected = true; // send availability - var available = this.plugin.Interface.ClientState.LocalPlayer != null; + var available = this._plugin.Interface.ClientState.LocalPlayer != null; try { - await SecretMessage.SendSecretMessage(stream, handshake.Keys.tx, new Availability(available), this.tokenSource.Token); + await SecretMessage.SendSecretMessage(client, handshake.Keys.tx, new Availability(available), this._tokenSource.Token); } catch (Exception ex) { PluginLog.LogError($"Could not send message: {ex.Message}"); } // send player data try { - await this.SendPlayerData(newClient); + await this.SendPlayerData(client); } catch (Exception ex) { PluginLog.LogError($"Could not send message: {ex.Message}"); } // send current channel try { - var channel = this.currentChannel; + var channel = this._currentChannel; await SecretMessage.SendSecretMessage( - stream, + client, handshake.Keys.tx, new ServerChannel( channel, this.LocalisedChannelName(channel) ), - this.tokenSource.Token + this._tokenSource.Token ); } catch (Exception ex) { PluginLog.LogError($"Could not send message: {ex.Message}"); } var listen = Task.Run(async () => { - conn.ReceiveTimeout = 5_000; - - while (this.clients.TryGetValue(id, out var client) && client.Connected && !client.TokenSource.IsCancellationRequested && conn.Connected) { + while (this._clients.TryGetValue(id, out var client) && client.Connected && !client.TokenSource.IsCancellationRequested) { byte[] msg; try { - msg = await SecretMessage.ReadSecretMessage(stream, handshake.Keys.rx, client.TokenSource.Token); + msg = await SecretMessage.ReadSecretMessage(client, handshake.Keys.rx, client.TokenSource.Token); } catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut) { continue; } catch (Exception ex) { @@ -340,116 +337,115 @@ namespace XIVChatPlugin { continue; } - var op = (ClientOperation) msg[0]; - - var payload = new byte[msg.Length - 1]; - Array.Copy(msg, 1, payload, 0, payload.Length); - - switch (op) { - case ClientOperation.Ping: - try { - await SecretMessage.SendSecretMessage(stream, handshake.Keys.tx, Pong.Instance, client.TokenSource.Token); - } catch (Exception ex) { - PluginLog.LogError($"Could not send message: {ex.Message}"); - } - - break; - case ClientOperation.Message: - var clientMessage = ClientMessage.Decode(payload); - foreach (var part in Wrap(clientMessage.Content)) { - this.toGame.Enqueue(part); - } - - break; - case ClientOperation.Shutdown: - client.Disconnect(); - break; - case ClientOperation.Backlog: - // ReSharper disable once LocalVariableHidesMember - var backlog = ClientBacklog.Decode(payload); - - var backlogMessages = new List(); - - var node = this.backlog.Last; - while (node != null) { - if (backlogMessages.Count >= backlog.Amount) { - break; - } - - backlogMessages.Add(node.Value); - node = node.Previous; - } - - if (!client.GetPreference(ClientPreference.BacklogNewestMessagesFirst, false)) { - backlogMessages.Reverse(); - } - - await SendBacklogs(backlogMessages.ToArray(), stream, client); - break; - case ClientOperation.CatchUp: - var catchUp = ClientCatchUp.Decode(payload); - // I'm not sure why this needs to be done, but apparently it does - var after = catchUp.After.AddMilliseconds(1); - var msgs = this.MessagesAfter(after); - - if (client.GetPreference(ClientPreference.BacklogNewestMessagesFirst, false)) { - msgs = msgs.Reverse(); - } - - await SendBacklogs(msgs, stream, client); - break; - case ClientOperation.PlayerList: - var playerList = ClientPlayerList.Decode(payload); - - if (playerList.Type == PlayerListType.Friend) { - this.waitingForFriendList.Add(id); - - if (!this.plugin.Functions.RequestingFriendList && !this.plugin.Functions.RequestFriendList()) { - this.plugin.Interface.Framework.Gui.Chat.PrintError($"[{this.plugin.Name}] Please open your friend list to enable friend list support. You should only need to do this on initial install or after updates."); - } - } - - break; - case ClientOperation.Preferences: - var preferences = ClientPreferences.Decode(payload); - client.Preferences = preferences; - - break; - case ClientOperation.Channel: - var channel = ClientChannel.Decode(payload); - this.plugin.Functions.ChangeChatChannel(channel.Channel); - - break; - } + await this.ProcessMessage(id, client, handshake, msg); } }); - while (this.clients.TryGetValue(id, out var client) && client.Connected && !client.TokenSource.IsCancellationRequested && conn.Connected) { + while (this._clients.TryGetValue(id, out var client) && client.Connected && !client.TokenSource.IsCancellationRequested) { try { var msg = await client.Queue.Reader.ReadAsync(client.TokenSource.Token); - await SecretMessage.SendSecretMessage(stream, handshake.Keys.tx, msg, client.TokenSource.Token); + await SecretMessage.SendSecretMessage(client, handshake.Keys.tx, msg, client.TokenSource.Token); } catch (Exception ex) { PluginLog.LogError($"Could not send message: {ex.Message}"); } } - try { - conn.Close(); - } catch (ObjectDisposedException) { - } + client.Disconnect(); await listen; - this.clients.TryRemove(id, out _); + this._clients.TryRemove(id, out _); PluginLog.Log($"Client thread ended: {id}"); }).ContinueWith(_ => { - try { - conn.Close(); - } catch (ObjectDisposedException) { - } + client.Disconnect(); + this._clients.TryRemove(id, out var _); }); } + private async Task ProcessMessage(Guid id, BaseClient client, HandshakeInfo handshake, byte[] msg) { + var op = (ClientOperation) msg[0]; + + var payload = new byte[msg.Length - 1]; + Array.Copy(msg, 1, payload, 0, payload.Length); + + switch (op) { + case ClientOperation.Ping: + try { + await SecretMessage.SendSecretMessage(client, handshake.Keys.tx, Pong.Instance, client.TokenSource.Token); + } catch (Exception ex) { + PluginLog.LogError($"Could not send message: {ex.Message}"); + } + + break; + case ClientOperation.Message: + var clientMessage = ClientMessage.Decode(payload); + foreach (var part in Wrap(clientMessage.Content)) { + this._toGame.Enqueue(part); + } + + break; + case ClientOperation.Shutdown: + client.Disconnect(); + break; + case ClientOperation.Backlog: + // ReSharper disable once LocalVariableHidesMember + var backlog = ClientBacklog.Decode(payload); + + var backlogMessages = new List(); + + var node = this._backlog.Last; + while (node != null) { + if (backlogMessages.Count >= backlog.Amount) { + break; + } + + backlogMessages.Add(node.Value); + node = node.Previous; + } + + if (!client.GetPreference(ClientPreference.BacklogNewestMessagesFirst, false)) { + backlogMessages.Reverse(); + } + + await SendBacklogs(backlogMessages.ToArray(), client); + break; + case ClientOperation.CatchUp: + var catchUp = ClientCatchUp.Decode(payload); + // I'm not sure why this needs to be done, but apparently it does + var after = catchUp.After.AddMilliseconds(1); + var msgs = this.MessagesAfter(after); + + if (client.GetPreference(ClientPreference.BacklogNewestMessagesFirst, false)) { + msgs = msgs.Reverse(); + } + + await SendBacklogs(msgs, client); + break; + case ClientOperation.PlayerList: + var playerList = ClientPlayerList.Decode(payload); + + if (playerList.Type == PlayerListType.Friend) { + this._waitingForFriendList.Add(id); + + if (!this._plugin.Functions.RequestingFriendList && !this._plugin.Functions.RequestFriendList()) { + this._plugin.Interface.Framework.Gui.Chat.PrintError($"[{this._plugin.Name}] Please open your friend list to enable friend list support. You should only need to do this on initial install or after updates."); + } + } + + break; + case ClientOperation.Preferences: + var preferences = ClientPreferences.Decode(payload); + client.Preferences = preferences; + + break; + case ClientOperation.Channel: + var channel = ClientChannel.Decode(payload); + this._plugin.Functions.ChangeChatChannel(channel.Channel); + + break; + } + } + public class NameFormatting { public string Before { get; private set; } = string.Empty; public string After { get; private set; } = string.Empty; @@ -476,14 +472,14 @@ namespace XIVChatPlugin { return cached; } - var logKind = this.plugin.Interface.Data.GetExcelSheet().GetRow((ushort) type); + var logKind = this._plugin.Interface.Data.GetExcelSheet().GetRow((ushort) type); if (logKind == null) { return null; } var format = logKind.Format; - var sestring = this.plugin.Interface.SeStringManager.Parse(format.RawData.ToArray()); + var sestring = this._plugin.Interface.SeStringManager.Parse(format.RawData.ToArray()); static bool IsStringParam(Payload payload, byte num) { var data = payload.Encode(); @@ -519,14 +515,14 @@ namespace XIVChatPlugin { return nameFormatting; } - private static async Task SendBacklogs(IEnumerable messages, Stream stream, Client client) { + private static async Task SendBacklogs(IEnumerable messages, BaseClient client) { var size = 5 + SecretMessage.MacSize(); // assume 5 bytes for payload lead-in, although it's likely to be less var responseMessages = new List(); async Task SendBacklog() { var resp = new ServerBacklog(responseMessages.ToArray()); try { - await SecretMessage.SendSecretMessage(stream, client.Handshake!.Keys.tx, resp, client.TokenSource.Token); + await SecretMessage.SendSecretMessage(client, client.Handshake!.Keys.tx, resp, client.TokenSource.Token); } catch (Exception ex) { PluginLog.LogError($"Could not send backlog: {ex.Message}"); } @@ -628,7 +624,7 @@ namespace XIVChatPlugin { return chunks; } - private IEnumerable MessagesAfter(DateTime time) => this.backlog.Where(msg => msg.Timestamp > time).ToArray(); + private IEnumerable MessagesAfter(DateTime time) => this._backlog.Where(msg => msg.Timestamp > time).ToArray(); private static IEnumerable Wrap(string input) { const int limit = 500; @@ -701,11 +697,13 @@ namespace XIVChatPlugin { private void BroadcastMessage(IEncodable message) { foreach (var client in this.Clients.Values) { - if (client.Handshake == null || client.Conn == null) { + if (client.Handshake == null) { continue; } - Task.Run(async () => { await SecretMessage.SendSecretMessage(client.Conn.GetStream(), client.Handshake.Keys.tx, message); }); + Task.Run(async () => { + await SecretMessage.SendSecretMessage(client, client.Handshake.Keys.tx, message); + }); } } @@ -739,12 +737,12 @@ namespace XIVChatPlugin { _ => 0, }; - return this.plugin.Interface.Data.GetExcelSheet().GetRow(rowId).Name; + return this._plugin.Interface.Data.GetExcelSheet().GetRow(rowId).Name; } public void OnChatChannelChange(uint channel) { var inputChannel = (InputChannel) channel; - this.currentChannel = inputChannel; + this._currentChannel = inputChannel; var localisedName = this.LocalisedChannelName(inputChannel); @@ -757,27 +755,27 @@ namespace XIVChatPlugin { } private PlayerData? GeneratePlayerData() { - var player = this.plugin.Interface.ClientState.LocalPlayer; + var player = this._plugin.Interface.ClientState.LocalPlayer; if (player == null) { return null; } var homeWorld = player.HomeWorld.GameData.Name; var currentWorld = player.CurrentWorld.GameData.Name; - var territory = this.plugin.Interface.Data.GetExcelSheet().GetRow(this.plugin.Interface.ClientState.TerritoryType); + var territory = this._plugin.Interface.Data.GetExcelSheet().GetRow(this._plugin.Interface.ClientState.TerritoryType); var location = territory?.PlaceName?.Value?.Name ?? "???"; var name = player.Name; return new PlayerData(homeWorld, currentWorld, location, name); } - private async Task SendPlayerData(Client client) { + private async Task SendPlayerData(BaseClient client) { var playerData = this.GeneratePlayerData(); if (playerData == null) { return; } - await SecretMessage.SendSecretMessage(client.Conn.GetStream(), client.Handshake!.Keys.tx, playerData); + await SecretMessage.SendSecretMessage(client, client.Handshake!.Keys.tx, playerData); } private void BroadcastPlayerData() { @@ -794,7 +792,7 @@ namespace XIVChatPlugin { public void OnLogIn(object sender, EventArgs e) { this.BroadcastAvailability(true); // send player data on next framework update - this.sendPlayerData = true; + this._sendPlayerData = true; } public void OnLogOut(object sender, EventArgs e) { @@ -802,20 +800,19 @@ namespace XIVChatPlugin { this.BroadcastPlayerData(); } - public void OnTerritoryChange(object sender, ushort territoryId) => this.sendPlayerData = true; + public void OnTerritoryChange(object sender, ushort territoryId) => this._sendPlayerData = true; public void Dispose() { // stop accepting new clients - this.tokenSource.Cancel(); - foreach (var client in this.clients.Values) { + this._tokenSource.Cancel(); + foreach (var client in this._clients.Values) { Task.Run(async () => { // tell clients we're shutting down if (client.Handshake != null) { try { - // time out after 5 seconds - client.Conn.SendTimeout = 5_000; - await SecretMessage.SendSecretMessage(client.Conn.GetStream(), client.Handshake.Keys.tx, ServerShutdown.Instance); + await SecretMessage.SendSecretMessage(client, client.Handshake.Keys.tx, ServerShutdown.Instance); } catch (Exception) { + // ignored } } @@ -824,36 +821,7 @@ namespace XIVChatPlugin { }); } - this.plugin.Functions.ReceiveFriendList -= this.OnReceiveFriendList; - } - } - - public class Client { - public bool Connected { get; set; } - public TcpClient Conn { get; } - public HandshakeInfo? Handshake { get; set; } - public ClientPreferences? Preferences { get; set; } - public CancellationTokenSource TokenSource { get; } = new CancellationTokenSource(); - public Channel Queue { get; } = Channel.CreateUnbounded(); - - public Client(TcpClient conn) { - this.Conn = conn; - } - - public void Disconnect() { - this.Connected = false; - this.TokenSource.Cancel(); - this.Conn.Close(); - } - - public T GetPreference(ClientPreference pref, T def = default) { - var prefs = this.Preferences; - - if (prefs == null) { - return def; - } - - return prefs.TryGetValue(pref, out T result) ? result : def; + this._plugin.Functions.ReceiveFriendList -= this.OnReceiveFriendList; } } diff --git a/XIVChatPlugin/XIVChatPlugin.csproj b/XIVChatPlugin/XIVChatPlugin.csproj index a1352dd..0ab41a3 100644 --- a/XIVChatPlugin/XIVChatPlugin.csproj +++ b/XIVChatPlugin/XIVChatPlugin.csproj @@ -114,14 +114,20 @@ + + ..\packages\WebSocketSharp.1.0.3-rc11\lib\websocket-sharp.dll + True + + + diff --git a/XIVChatPlugin/packages.config b/XIVChatPlugin/packages.config index 310a002..095c178 100644 --- a/XIVChatPlugin/packages.config +++ b/XIVChatPlugin/packages.config @@ -10,6 +10,7 @@ +