diff --git a/XIVChatPlugin/Configuration.cs b/XIVChatPlugin/Configuration.cs index 8b071a5..578899f 100644 --- a/XIVChatPlugin/Configuration.cs +++ b/XIVChatPlugin/Configuration.cs @@ -17,6 +17,8 @@ namespace XIVChatPlugin { public bool PairingMode { get; set; } = true; + public bool AcceptNewClients { get; set; } = true; + public Dictionary> TrustedKeys { get; set; } = new Dictionary>(); public KeyPair KeyPair { get; set; } = null; diff --git a/XIVChatPlugin/PluginUI.cs b/XIVChatPlugin/PluginUI.cs index b4904c6..53e1138 100644 --- a/XIVChatPlugin/PluginUI.cs +++ b/XIVChatPlugin/PluginUI.cs @@ -138,8 +138,8 @@ namespace XIVChatPlugin { this.plugin.Config.SendBattle = sendBattle; this.plugin.Config.Save(); } - - ImGui.TextUnformatted("Changing this setting will not affect messages already in the backlog."); + ImGui.SameLine(); + HelpMarker("Changing this setting will not affect messages already in the backlog."); ImGui.Spacing(); @@ -150,6 +150,16 @@ namespace XIVChatPlugin { } ImGui.SameLine(); HelpMarker("While in pairing mode, XIVChat Server will listen for information requests from clients broadcast on your local network and respond with information about the server. This will make it easier to add your server to a client, but this should be turned off when not actively adding new devices."); + + ImGui.Spacing(); + + bool acceptNew = this.plugin.Config.AcceptNewClients; + if (WithWhiteText(() => ImGui.Checkbox("Accept new clients", ref acceptNew))) { + this.plugin.Config.AcceptNewClients = acceptNew; + this.plugin.Config.Save(); + } + ImGui.SameLine(); + HelpMarker("If this is disabled, XIVChat Server will only allow clients with already-trusted keys to connect."); } if (WithWhiteText(() => ImGui.CollapsingHeader("Trusted keys"))) { diff --git a/XIVChatPlugin/Server.cs b/XIVChatPlugin/Server.cs index 834a7ff..51c4a09 100644 --- a/XIVChatPlugin/Server.cs +++ b/XIVChatPlugin/Server.cs @@ -243,6 +243,12 @@ namespace XIVChatPlugin { this.plugin.Functions.ProcessChatBox(message); } + private static readonly IReadOnlyList MAGIC = new byte[] { + 14, + 20, + 67, + }; + private void SpawnClientTask(TcpClient conn) { if (conn == null) { return; @@ -251,6 +257,28 @@ namespace XIVChatPlugin { Task.Run(async () => { var stream = conn.GetStream(); + // 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)); + + // read magic bytes + while (read < magic.Length) { + if (cts.IsCancellationRequested) { + return; + } + + read += await stream.ReadAsync(magic, read, magic.Length - read, cts.Token); + } + + // 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, @@ -258,6 +286,11 @@ namespace XIVChatPlugin { // if this public key isn't trusted, prompt first 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) { + return; + } + var accepted = Channel.CreateBounded(1); await this.pendingClients.Writer.WriteAsync(Tuple.Create(newClient, accepted)); @@ -392,7 +425,11 @@ namespace XIVChatPlugin { this.clients.TryRemove(id, out var _); PluginLog.Log($"Client thread ended: {id}"); - }); + }).ContinueWith(_ => { + try { + conn.Close(); + } catch (ObjectDisposedException) { } + }); ; } private static readonly Regex colorRegex = new Regex(@"", RegexOptions.Compiled);