using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Security.Authentication; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using MessagePack; using WebSocketSharp; using XIVChatCommon.Message.Relay; namespace XIVChatPlugin { internal enum ConnectionStatus { Disconnected, Connecting, Negotiating, Connected, } internal class Relay : IDisposable { #if DEBUG private static readonly Uri RelayUrl = new("ws://localhost:14555/", UriKind.Absolute); #else private static readonly Uri RelayUrl = new("wss://relay.xiv.chat/", UriKind.Absolute); #endif internal static string? ConnectionError { get; private set; } private bool Disposed { get; set; } private Plugin Plugin { get; } private WebSocket Connection { get; } private bool Running { get; set; } internal ConnectionStatus Status { get; private set; } private Channel ToRelay { get; } = Channel.CreateUnbounded(); internal Relay(Plugin plugin) { this.Plugin = plugin; this.Connection = new WebSocket(RelayUrl.ToString()) { SslConfiguration = { EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13, }, }; this.Connection.OnOpen += this.OnOpen; this.Connection.OnMessage += this.OnMessage; this.Connection.OnClose += this.OnClose; this.Connection.OnError += this.OnError; } public void Dispose() { this.Disposed = true; new Thread(() => this.Connection.Close(CloseStatusCode.Normal)).Start(); this.Running = false; } internal void Start() { if (this.Plugin.Config.RelayAuth == null) { return; } this.Running = true; this.Status = ConnectionStatus.Connecting; new Thread(() => this.Connection.Connect()).Start(); } internal void ResendPublicKey() { var keys = this.Plugin.Config.KeyPair; if (keys == null) { return; } var msg = new RelayRegister { AuthToken = "", PublicKey = keys.PublicKey, }; var bytes = MessagePackSerializer.Serialize((IToRelay) msg); this.Connection.Send(bytes); } internal void DisconnectClient(IEnumerable pk) { var msg = new RelayClientDisconnect { PublicKey = pk.ToList(), }; var bytes = MessagePackSerializer.Serialize((IToRelay) msg); this.Connection.Send(bytes); } private void OnOpen(object? o, EventArgs eventArgs) { this.Status = ConnectionStatus.Negotiating; var auth = this.Plugin.Config.RelayAuth; if (auth == null) { return; } var keys = this.Plugin.Config.KeyPair; if (keys == null) { return; } var message = new RelayRegister { AuthToken = auth, PublicKey = keys.PublicKey, }; var bytes = MessagePackSerializer.Serialize((IToRelay) message); this.Connection.Send(bytes); Task.Run(async () => { while (this.Running) { this.Connection.Ping(); await Task.Delay(TimeSpan.FromSeconds(30)); } }); 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 args) { var message = MessagePackSerializer.Deserialize(args.RawData); switch (message) { case RelaySuccess success: if (success.Success) { ConnectionError = null; this.Status = ConnectionStatus.Connected; } else { Plugin.Log.Warning($"Relay: {success.Info}"); ConnectionError = success.Info; this.Status = ConnectionStatus.Disconnected; this.Plugin.StopRelay(); } break; case RelayNewClient newClient: #pragma warning disable CA1806 IPAddress.TryParse(newClient.Address, out var remote); #pragma warning restore CA1806 var client = new RelayConnected( newClient.PublicKey.ToArray(), remote, this.ToRelay.Writer, Channel.CreateUnbounded() ); this.Plugin.Server.SpawnClientTask(client, false); break; case RelayClientDisconnect disconnect: var clientPk = disconnect.PublicKey.ToArray(); var id = this.Plugin.Server.Clients .Where(client => client.Value is RelayConnected) .Where(client => client.Value.Handshake?.RemotePublicKey?.SequenceEqual(clientPk) ?? false) .Select(client => client.Key) .FirstOrDefault(); if (id != default) { this.Plugin.Server.RemoveClient(id); } 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 args) { this.Running = false; this.Status = ConnectionStatus.Disconnected; if (!args.WasClean && !this.Disposed) { Task.Run(async () => await Task.Delay(3_000)).ContinueWith(_ => this.Start()); } } private void OnError(object? sender, ErrorEventArgs args) { Plugin.Log.Error(args.Exception, $"Error in relay connection: {args.Message}"); this.Running = false; this.Status = ConnectionStatus.Disconnected; if (!this.Disposed) { Task.Run(async () => await Task.Delay(3_000)).ContinueWith(_ => this.Start()); } } } }