feat: begin adding relay support

This commit is contained in:
Anna 2021-01-19 23:02:26 -05:00
parent 1128a49557
commit ed34db2131
Signed by: anna
GPG Key ID: 0B391D8F06FCD9E0
21 changed files with 931 additions and 292 deletions

View File

@ -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);
}

View File

@ -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) {

View File

@ -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();
}

View File

@ -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<byte> 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<byte> 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);

View File

@ -17,6 +17,9 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
@ -27,39 +30,75 @@
<Label VerticalAlignment="Center"
Grid.Row="0"
Grid.Column="0">
Server type
</Label>
<ComboBox Grid.Row="0"
Grid.Column="1"
SelectedIndex="0">
<ComboBoxItem>Direct</ComboBoxItem>
<ComboBoxItem>Relay</ComboBoxItem>
</ComboBox>
<Label VerticalAlignment="Center"
Grid.Row="1"
Grid.Column="0">
Name
</Label>
<TextBox Margin="4,0,0,0"
Grid.Row="0"
Grid.Row="1"
Grid.Column="1"
x:Name="ServerName"
Text="{Binding Server.Name, Mode=OneTime}" />
<Label VerticalAlignment="Center"
Grid.Row="1"
Grid.Row="2"
Grid.Column="0">
IP Address
</Label>
<TextBox Margin="4,4,0,0"
Grid.Row="1"
Grid.Row="2"
Grid.Column="1"
x:Name="ServerHost"
Text="{Binding Server.Host, Mode=OneTime}" />
<Label VerticalAlignment="Center"
Grid.Row="2"
Grid.Row="3"
Grid.Column="0">
Port
</Label>
<TextBox Margin="4,4,0,0"
Grid.Row="2"
Grid.Row="3"
Grid.Column="1"
x:Name="ServerPort"
Text="{Binding Server.Port, Mode=OneTime}"
ui:ControlHelper.PlaceholderText="14777" />
<Label VerticalAlignment="Center"
Grid.Row="4"
Grid.Column="0">
Relay auth
</Label>
<TextBox Margin="4,4,0,0"
Grid.Row="4"
Grid.Column="1"
x:Name="RelayAuth"
Text="{Binding Server.RelayAuth, Mode=OneTime}"
ui:ControlHelper.PlaceholderText="Optional (only enter if using relay)" />
<Label VerticalAlignment="Center"
Grid.Row="5"
Grid.Column="0">
Server public key
</Label>
<TextBox Margin="4,4,0,0"
Grid.Row="5"
Grid.Column="1"
x:Name="RelayTarget"
Text="{Binding Server.RelayTarget, Mode=OneTime}"
ui:ControlHelper.PlaceholderText="Optional (only enter if using relay)" />
<WrapPanel Margin="0,8,0,0"
Grid.Row="3"
Grid.Row="6"
Grid.ColumnSpan="2"
Grid.Column="0"
HorizontalAlignment="Right">

View File

@ -5,7 +5,7 @@ namespace XIVChat_Desktop {
/// Interaction logic for ManageServer.xaml
/// </summary>
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();

View File

@ -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;
}

View File

@ -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<List<T>> Chunks<T>(this List<T> locations, int nSize) {
for (var i = 0; i < locations.Count; i += nSize) {
yield return locations.GetRange(i, Math.Min(nSize, locations.Count - i));

View File

@ -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);

View File

@ -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<byte> PublicKey { get; set; }
[Key(1)]
public string Address { get; set; }
}
[MessagePackObject]
public class RelayedMessage : IFromRelay, IToRelay {
[Key(0)]
public List<byte> PublicKey { get; set; }
[Key(1)]
public List<byte> Message { get; set; }
}
}

View File

@ -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) {

252
XIVChatPlugin/Client.cs Normal file
View File

@ -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<ServerMessage> Queue { get; } = Channel.CreateUnbounded<ServerMessage>();
public void Disconnect() {
this.Connected = false;
this.TokenSource.Cancel();
try {
this.Close();
} catch (ObjectDisposedException) {
// ignored
}
}
public T GetPreference<T>(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<int> 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<IToRelay> ToRelay { get; }
private Channel<byte[]> FromRelay { get; }
internal ChannelWriter<byte[]> FromRelayWriter => this.FromRelay.Writer;
private List<byte> ReadBuffer { get; } = new List<byte>();
private List<byte> WriteBuffer { get; } = new List<byte>();
public RelayConnected(byte[] publicKey, IPAddress remote, ChannelWriter<IToRelay> toRelay, Channel<byte[]> 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<int> 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();
}
}
}

View File

@ -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<Guid, Tuple<string, byte[]>> TrustedKeys { get; set; } = new Dictionary<Guid, Tuple<string, byte[]>>();
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);
}
}
}

View File

@ -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");

View File

@ -6,6 +6,7 @@
<InputAssemblies Include="$(OutputPath)\XIVChatCommon.dll"/>
<InputAssemblies Include="$(OutputPath)\M*.dll"/>
<InputAssemblies Include="$(OutputPath)\S*.dll"/>
<InputAssemblies Include="$(OutputPath)\websocket*.dll"/>
</ItemGroup>
<ILRepack

View File

@ -1,5 +1,4 @@
using Dalamud.Game.Command;
using Dalamud.Hooking;
using Dalamud.Plugin;
using System;
using System.Collections.Generic;
@ -9,8 +8,6 @@ using System.IO;
#endif
using System.Reflection;
// TODO: hostable relay server (run one but have option to run your own)?
namespace XIVChatPlugin {
public class Plugin : IDalamudPlugin {
private bool _disposedValue;
@ -25,13 +22,12 @@ namespace XIVChatPlugin {
this.Location = path;
}
#pragma warning disable 8618
public DalamudPluginInterface Interface { get; private set; }
public Configuration Config { get; private set; }
private PluginUi Ui { get; set; }
public Server Server { get; private set; }
public GameFunctions Functions { get; private set; }
#pragma warning restore 8618
public DalamudPluginInterface Interface { get; private set; } = null!;
public Configuration Config { get; private set; } = null!;
private PluginUi Ui { get; set; } = null!;
public Server Server { get; private set; } = null!;
public Relay? Relay { get; private set; }
public GameFunctions Functions { get; private set; } = null!;
public void Initialize(DalamudPluginInterface pluginInterface) {
this.Interface = pluginInterface ?? throw new ArgumentNullException(nameof(pluginInterface), "DalamudPluginInterface cannot be null");
@ -64,6 +60,25 @@ namespace XIVChatPlugin {
});
}
internal void StartRelay() {
if (this.Relay != null) {
return;
}
this.Relay = new Relay(this);
this.Relay.Start();
PluginLog.Log("started");
}
internal void StopRelay() {
if (this.Relay == null) {
return;
}
this.Relay.Dispose();
this.Relay = null;
}
internal IntPtr ScanText(string sig) {
try {
return this.Interface.TargetModuleScanner.ScanText(sig);
@ -101,6 +116,7 @@ namespace XIVChatPlugin {
}
if (disposing) {
this.Relay?.Dispose();
this.Server.Dispose();
this.Interface.UiBuilder.OnBuildUi -= this.Ui.Draw;

View File

@ -11,9 +11,13 @@ namespace XIVChatPlugin {
private Plugin Plugin { get; }
private bool _showSettings;
private bool ShowSettings { get => this._showSettings; set => this._showSettings = value; }
private readonly Dictionary<Guid, Tuple<Client, Channel<bool>>> _pending = new Dictionary<Guid, Tuple<Client, Channel<bool>>>();
private bool ShowSettings {
get => this._showSettings;
set => this._showSettings = value;
}
private readonly Dictionary<Guid, Tuple<BaseClient, Channel<bool>>> _pending = new Dictionary<Guid, Tuple<BaseClient, Channel<bool>>>();
private readonly Dictionary<Guid, string> _pendingNames = new Dictionary<Guid, string>(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<T>(Func<T> 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<string, byte[]>))) {
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<bool, bool> accepted) {
private bool DrawPending(Guid id, BaseClient client, Channel<bool, bool> accepted) {
var ret = false;
var clientPublic = client.Handshake!.RemotePublicKey;

145
XIVChatPlugin/Relay.cs Normal file
View File

@ -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<IToRelay> ToRelay { get; } = Channel.CreateUnbounded<IToRelay>();
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<IFromRelay>(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<byte[]>()
);
this.Plugin.Server.SpawnClientTask(client, false);
break;
case RelayedMessage relayed:
var relayedClient = this.Plugin.Server.Clients.Values
.Where(client => client is RelayConnected)
.Cast<RelayConnected>()
.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 ?
}
}
}

View File

@ -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<string> toGame = new ConcurrentQueue<string>();
private readonly CancellationTokenSource _tokenSource = new CancellationTokenSource();
private readonly ConcurrentQueue<string> _toGame = new ConcurrentQueue<string>();
private readonly ConcurrentDictionary<Guid, Client> clients = new ConcurrentDictionary<Guid, Client>();
public IReadOnlyDictionary<Guid, Client> Clients => this.clients;
public readonly Channel<Tuple<Client, Channel<bool>>> pendingClients = Channel.CreateUnbounded<Tuple<Client, Channel<bool>>>();
private readonly ConcurrentDictionary<Guid, BaseClient> _clients = new ConcurrentDictionary<Guid, BaseClient>();
public IReadOnlyDictionary<Guid, BaseClient> Clients => this._clients;
public readonly Channel<Tuple<BaseClient, Channel<bool>>> PendingClients = Channel.CreateUnbounded<Tuple<BaseClient, Channel<bool>>>();
private readonly HashSet<Guid> waitingForFriendList = new HashSet<Guid>();
private readonly HashSet<Guid> _waitingForFriendList = new HashSet<Guid>();
private readonly LinkedList<ServerMessage> backlog = new LinkedList<ServerMessage>();
private readonly LinkedList<ServerMessage> _backlog = new LinkedList<ServerMessage>();
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<UdpReceiveResult>? 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<Player> 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<Chunk>();
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<byte> 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<bool>(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<ServerMessage>();
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<ServerMessage>();
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<LogKind>().GetRow((ushort) type);
var logKind = this._plugin.Interface.Data.GetExcelSheet<LogKind>().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<ServerMessage> messages, Stream stream, Client client) {
private static async Task SendBacklogs(IEnumerable<ServerMessage> 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<ServerMessage>();
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<ServerMessage> MessagesAfter(DateTime time) => this.backlog.Where(msg => msg.Timestamp > time).ToArray();
private IEnumerable<ServerMessage> MessagesAfter(DateTime time) => this._backlog.Where(msg => msg.Timestamp > time).ToArray();
private static IEnumerable<string> 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<LogFilter>().GetRow(rowId).Name;
return this._plugin.Interface.Data.GetExcelSheet<LogFilter>().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<TerritoryType>().GetRow(this.plugin.Interface.ClientState.TerritoryType);
var territory = this._plugin.Interface.Data.GetExcelSheet<TerritoryType>().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<ServerMessage> Queue { get; } = Channel.CreateUnbounded<ServerMessage>();
public Client(TcpClient conn) {
this.Conn = conn;
}
public void Disconnect() {
this.Connected = false;
this.TokenSource.Cancel();
this.Conn.Close();
}
public T GetPreference<T>(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;
}
}

View File

@ -114,14 +114,20 @@
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="websocket-sharp, Version=1.0.2.59611, Culture=neutral, PublicKeyToken=5660b08a1845a91e">
<HintPath>..\packages\WebSocketSharp.1.0.3-rc11\lib\websocket-sharp.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Client.cs" />
<Compile Include="Configuration.cs" />
<Compile Include="Extensions.cs" />
<Compile Include="GameFunctions.cs" />
<Compile Include="Plugin.cs" />
<Compile Include="PluginUi.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Relay.cs" />
<Compile Include="Server.cs" />
</ItemGroup>
<ItemGroup>

View File

@ -10,6 +10,7 @@
<package id="System.Reflection.Emit" version="4.7.0" targetFramework="net48" />
<package id="System.Reflection.Emit.Lightweight" version="4.7.0" targetFramework="net48" />
<package id="System.Collections.Immutable" version="5.0.0" targetFramework="net48" />
<package id="WebSocketSharp" version="1.0.3-rc11" targetFramework="net48" />
<package id="libsodium" version="1.0.18" targetFramework="net48" />
<package id="MessagePack.Annotations" version="2.2.60" targetFramework="net48" />
<package id="Microsoft.NETCore.Platforms" version="5.0.0" targetFramework="net48" />