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))); 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) { if (this.Connected) {
return; return;
} }
this.Connection = new Connection(this, host, port); this.Connection = new Connection(this, host, port, relayAuth, relayTarget);
this.Connection.ReceiveMessage += this.OnReceiveMessage; this.Connection.ReceiveMessage += this.OnReceiveMessage;
Task.Run(this.Connection.Connect); Task.Run(this.Connection.Connect);
} }

View File

@ -122,44 +122,64 @@ namespace XIVChat_Desktop {
[JsonObject] [JsonObject]
public class SavedServer : INotifyPropertyChanged { public class SavedServer : INotifyPropertyChanged {
private string name; private string _name;
private string host; private string _host;
private ushort port; private ushort _port;
private string? _relayAuth;
private string? _relayTarget;
public string Name { public string Name {
get => this.name; get => this._name;
set { set {
this.name = value; this._name = value;
this.OnPropertyChanged(nameof(this.Name)); this.OnPropertyChanged(nameof(this.Name));
} }
} }
public string Host { public string Host {
get => this.host; get => this._host;
set { set {
this.host = value; this._host = value;
this.OnPropertyChanged(nameof(this.Host)); this.OnPropertyChanged(nameof(this.Host));
} }
} }
public ushort Port { public ushort Port {
get => this.port; get => this._port;
set { set {
this.port = value; this._port = value;
this.OnPropertyChanged(nameof(this.Port)); 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; public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) { protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) {
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
} }
public SavedServer(string name, string host, ushort port) { public SavedServer(string name, string host, ushort port, string? relayAuth, string? relayTarget) {
this.name = name; this._name = name;
this.host = host; this._host = host;
this.port = port; this._port = port;
this._relayAuth = relayAuth;
this._relayTarget = relayTarget;
} }
protected bool Equals(SavedServer other) { protected bool Equals(SavedServer other) {

View File

@ -31,7 +31,7 @@ namespace XIVChat_Desktop {
return; return;
} }
this.App.Connect(server.Host, server.Port); this.App.Connect(server.Host, server.Port, server.RelayAuth, server.RelayTarget);
this.Close(); this.Close();
} }

View File

@ -4,6 +4,7 @@ using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Net.Sockets; using System.Net.Sockets;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Channels; using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -16,10 +17,30 @@ using XIVChatCommon.Message.Server;
namespace XIVChat_Desktop { namespace XIVChat_Desktop {
public class Connection : INotifyPropertyChanged { 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 App app;
private readonly string host; private readonly string host;
private readonly ushort port; private readonly ushort port;
private readonly string? relayAuth;
private readonly string? relayTarget;
private TcpClient? client; 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.app = app;
this.host = host; this.host = host;
this.port = port; this.port = port;
this.relayAuth = relayAuth;
this.relayTarget = relayTarget;
} }
public void SendMessage(string message) { public void SendMessage(string message) {
@ -80,13 +103,49 @@ namespace XIVChat_Desktop {
} }
public async Task Connect() { 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(); var stream = this.client.GetStream();
// write the magic bytes switch (usingRelay) {
await stream.WriteAsync(new byte[] { // do relay auth before connecting if necessary
14, 20, 67, 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 // do the handshake
var handshake = await KeyExchange.ClientHandshake(this.app.Config.KeyPair, stream); 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" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@ -27,39 +30,75 @@
<Label VerticalAlignment="Center" <Label VerticalAlignment="Center"
Grid.Row="0" Grid.Row="0"
Grid.Column="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 Name
</Label> </Label>
<TextBox Margin="4,0,0,0" <TextBox Margin="4,0,0,0"
Grid.Row="0" Grid.Row="1"
Grid.Column="1" Grid.Column="1"
x:Name="ServerName" x:Name="ServerName"
Text="{Binding Server.Name, Mode=OneTime}" /> Text="{Binding Server.Name, Mode=OneTime}" />
<Label VerticalAlignment="Center" <Label VerticalAlignment="Center"
Grid.Row="1" Grid.Row="2"
Grid.Column="0"> Grid.Column="0">
IP Address IP Address
</Label> </Label>
<TextBox Margin="4,4,0,0" <TextBox Margin="4,4,0,0"
Grid.Row="1" Grid.Row="2"
Grid.Column="1" Grid.Column="1"
x:Name="ServerHost" x:Name="ServerHost"
Text="{Binding Server.Host, Mode=OneTime}" /> Text="{Binding Server.Host, Mode=OneTime}" />
<Label VerticalAlignment="Center" <Label VerticalAlignment="Center"
Grid.Row="2" Grid.Row="3"
Grid.Column="0"> Grid.Column="0">
Port Port
</Label> </Label>
<TextBox Margin="4,4,0,0" <TextBox Margin="4,4,0,0"
Grid.Row="2" Grid.Row="3"
Grid.Column="1" Grid.Column="1"
x:Name="ServerPort" x:Name="ServerPort"
Text="{Binding Server.Port, Mode=OneTime}" Text="{Binding Server.Port, Mode=OneTime}"
ui:ControlHelper.PlaceholderText="14777" /> 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" <WrapPanel Margin="0,8,0,0"
Grid.Row="3" Grid.Row="6"
Grid.ColumnSpan="2" Grid.ColumnSpan="2"
Grid.Column="0" Grid.Column="0"
HorizontalAlignment="Right"> HorizontalAlignment="Right">

View File

@ -5,7 +5,7 @@ namespace XIVChat_Desktop {
/// Interaction logic for ManageServer.xaml /// Interaction logic for ManageServer.xaml
/// </summary> /// </summary>
public partial class ManageServer { public partial class ManageServer {
public App App => (App)Application.Current; public App App => (App) Application.Current;
public SavedServer? Server { get; private set; } public SavedServer? Server { get; private set; }
private readonly bool isNewServer; private readonly bool isNewServer;
@ -35,6 +35,16 @@ namespace XIVChat_Desktop {
private void Save_Click(object sender, RoutedEventArgs e) { private void Save_Click(object sender, RoutedEventArgs e) {
var serverName = this.ServerName.Text; var serverName = this.ServerName.Text;
var serverHost = this.ServerHost.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) { if (serverName.Length == 0 || serverHost.Length == 0) {
MessageBox.Show("Server must have a name and host."); MessageBox.Show("Server must have a name and host.");
@ -55,13 +65,18 @@ namespace XIVChat_Desktop {
this.Server = new SavedServer( this.Server = new SavedServer(
serverName, serverName,
serverHost, serverHost,
port port,
relayAuth,
relayTarget
); );
this.App.Config.Servers.Add(this.Server); this.App.Config.Servers.Add(this.Server);
} else { } else {
this.Server!.Name = serverName; this.Server!.Name = serverName;
this.Server.Host = serverHost; this.Server.Host = serverHost;
this.Server.Port = port; this.Server.Port = port;
this.Server.RelayAuth = relayAuth;
this.Server.RelayTarget = relayTarget;
} }
this.App.Config.Save(); this.App.Config.Save();

View File

@ -72,7 +72,7 @@ namespace XIVChat_Desktop {
continue; 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)) { if (this.Servers.Contains(saved)) {
continue; continue;
} }

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Windows.Controls; using System.Windows.Controls;
@ -9,6 +10,14 @@ using System.Windows.Threading;
namespace XIVChat_Desktop { namespace XIVChat_Desktop {
public static class Util { 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) { public static IEnumerable<List<T>> Chunks<T>(this List<T> locations, int nSize) {
for (var i = 0; i < locations.Count; i += nSize) { for (var i = 0; i < locations.Count; i += nSize) {
yield return locations.GetRange(i, Math.Min(nSize, locations.Count - i)); yield return locations.GetRange(i, Math.Min(nSize, locations.Count - i));

View File

@ -54,6 +54,7 @@ namespace XIVChatCommon {
// send our public key // send our public key
await stream.WriteAsync(server.PublicKey, 0, server.PublicKey.Length); await stream.WriteAsync(server.PublicKey, 0, server.PublicKey.Length);
await stream.FlushAsync();
// get shared secret and derive keys // get shared secret and derive keys
var keys = ServerSessionKeys(server, clientPublic); 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(len, 0, len.Length, token);
await s.WriteAsync(nonce, 0, nonce.Length, token); await s.WriteAsync(nonce, 0, nonce.Length, token);
await s.WriteAsync(ciphertext, 0, ciphertext.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) { 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 { namespace XIVChatPlugin {
public class Configuration : IPluginConfiguration { public class Configuration : IPluginConfiguration {
private Plugin? plugin; private Plugin? _plugin;
public int Version { get; set; } = 1; public int Version { get; set; } = 1;
public ushort Port { get; set; } = 14777; public ushort Port { get; set; } = 14777;
@ -19,15 +19,18 @@ namespace XIVChatPlugin {
public bool AcceptNewClients { get; set; } = true; 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 Dictionary<Guid, Tuple<string, byte[]>> TrustedKeys { get; set; } = new Dictionary<Guid, Tuple<string, byte[]>>();
public KeyPair? KeyPair { get; set; } public KeyPair? KeyPair { get; set; }
public void Initialise(Plugin plugin) { 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() { 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 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 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 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 channelCommandPtr = this.Plugin.ScanText("E8 ?? ?? ?? ?? 0F B7 44 37 ??");
var xivStringCtorPtr = this.Plugin.ScanText("E8 ?? ?? ?? ?? 44 2B F7"); var xivStringCtorPtr = this.Plugin.ScanText("E8 ?? ?? ?? ?? 44 2B F7");
var xivStringDtorPtr = this.Plugin.ScanText("E8 ?? ?? ?? ?? B0 6E"); var xivStringDtorPtr = this.Plugin.ScanText("E8 ?? ?? ?? ?? B0 6E");

View File

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

View File

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

View File

@ -11,9 +11,13 @@ namespace XIVChatPlugin {
private Plugin Plugin { get; } private Plugin Plugin { get; }
private bool _showSettings; 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); private readonly Dictionary<Guid, string> _pendingNames = new Dictionary<Guid, string>(0);
public PluginUi(Plugin plugin) { public PluginUi(Plugin plugin) {
@ -42,9 +46,11 @@ namespace XIVChatPlugin {
ImGui.PushStyleColor(ImGuiCol.ButtonActive, Colours.ButtonActive); ImGui.PushStyleColor(ImGuiCol.ButtonActive, Colours.ButtonActive);
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Colours.ButtonHovered); ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Colours.ButtonHovered);
this.DrawInner(); try {
this.DrawInner();
ImGui.PopStyleColor(8); } finally {
ImGui.PopStyleColor(8);
}
} }
private static T WithWhiteText<T>(Func<T> func) { private static T WithWhiteText<T>(Func<T> func) {
@ -107,6 +113,12 @@ namespace XIVChatPlugin {
if (WithWhiteText(() => ImGui.Button("Regenerate"))) { if (WithWhiteText(() => ImGui.Button("Regenerate"))) {
this.Plugin.Server.RegenerateKeyPair(); this.Plugin.Server.RegenerateKeyPair();
} }
ImGui.SameLine();
if (WithWhiteText(() => ImGui.Button("Copy"))) {
ImGui.SetClipboardText(serverPublic);
}
} }
if (WithWhiteText(() => ImGui.CollapsingHeader("Settings", ImGuiTreeNodeFlags.DefaultOpen))) { if (WithWhiteText(() => ImGui.CollapsingHeader("Settings", ImGuiTreeNodeFlags.DefaultOpen))) {
@ -114,7 +126,7 @@ namespace XIVChatPlugin {
int port = this.Plugin.Config.Port; int port = this.Plugin.Config.Port;
if (WithWhiteText(() => ImGui.InputInt("##port", ref 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.Port = realPort;
this.Plugin.Config.Save(); this.Plugin.Config.Save();
@ -131,7 +143,7 @@ namespace XIVChatPlugin {
int backlogCount = this.Plugin.Config.BacklogCount; int backlogCount = this.Plugin.Config.BacklogCount;
if (WithWhiteText(() => ImGui.DragInt("Backlog messages", ref backlogCount, 1f, 0, ushort.MaxValue))) { 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(); this.Plugin.Config.Save();
} }
@ -167,6 +179,40 @@ namespace XIVChatPlugin {
ImGui.SameLine(); ImGui.SameLine();
HelpMarker("If this is disabled, XIVChat Server will only allow clients with already-trusted keys to connect."); 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"))) { if (WithWhiteText(() => ImGui.CollapsingHeader("Trusted keys"))) {
@ -208,7 +254,10 @@ namespace XIVChatPlugin {
if (WithWhiteText(() => ImGui.CollapsingHeader("Connected clients"))) { 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"); ImGui.TextUnformatted("None");
} else { } else {
ImGui.Columns(3); ImGui.Columns(3);
@ -219,26 +268,29 @@ namespace XIVChatPlugin {
ImGui.NextColumn(); ImGui.NextColumn();
ImGui.NextColumn(); ImGui.NextColumn();
foreach (var client in this.Plugin.Server.Clients) { foreach (var client in clients) {
EndPoint remote; if (!client.Value.Connected) {
continue;
}
IPAddress? remote;
try { try {
remote = client.Value.Conn.Client.RemoteEndPoint; remote = client.Value.Remote;
} catch (ObjectDisposedException) { } catch (ObjectDisposedException) {
continue; continue;
} }
string ipAddress; var ipAddress = remote?.ToString() ?? "Unknown";
if (remote is IPEndPoint ip) {
ipAddress = ip.Address.ToString(); if (client.Value is RelayConnected) {
} else { ipAddress += " (R)";
ipAddress = "Unknown";
} }
ImGui.TextUnformatted(ipAddress); ImGui.TextUnformatted(ipAddress);
ImGui.NextColumn(); 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[]>))) { if (trustedKey != null && !trustedKey.Equals(default(Tuple<string, byte[]>))) {
ImGui.TextUnformatted(trustedKey!.Item1); ImGui.TextUnformatted(trustedKey!.Item1);
if (ImGui.IsItemHovered()) { if (ImGui.IsItemHovered()) {
@ -316,12 +368,12 @@ namespace XIVChatPlugin {
} }
private void AcceptPending() { 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; 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 ret = false;
var clientPublic = client.Handshake!.RemotePublicKey; 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;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
@ -24,37 +23,37 @@ using XIVChatCommon.Message.Server;
namespace XIVChatPlugin { namespace XIVChatPlugin {
public class Server : IDisposable { public class Server : IDisposable {
private readonly Plugin plugin; private readonly Plugin _plugin;
private readonly CancellationTokenSource tokenSource = new CancellationTokenSource(); private readonly CancellationTokenSource _tokenSource = new CancellationTokenSource();
private readonly ConcurrentQueue<string> toGame = new ConcurrentQueue<string>(); private readonly ConcurrentQueue<string> _toGame = new ConcurrentQueue<string>();
private readonly ConcurrentDictionary<Guid, Client> clients = new ConcurrentDictionary<Guid, Client>(); private readonly ConcurrentDictionary<Guid, BaseClient> _clients = new ConcurrentDictionary<Guid, BaseClient>();
public IReadOnlyDictionary<Guid, Client> Clients => this.clients; public IReadOnlyDictionary<Guid, BaseClient> Clients => this._clients;
public readonly Channel<Tuple<Client, Channel<bool>>> pendingClients = Channel.CreateUnbounded<Tuple<Client, Channel<bool>>>(); 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 volatile bool _running;
private bool Running => this.running; private bool Running => this._running;
private InputChannel currentChannel = InputChannel.Say; private InputChannel _currentChannel = InputChannel.Say;
private const int MaxMessageSize = 128_000; private const int MaxMessageSize = 128_000;
public Server(Plugin plugin) { public Server(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");
if (this.plugin.Config.KeyPair == null) { if (this._plugin.Config.KeyPair == null) {
this.RegenerateKeyPair(); this.RegenerateKeyPair();
} }
this.plugin.Functions.ReceiveFriendList += this.OnReceiveFriendList; this._plugin.Functions.ReceiveFriendList += this.OnReceiveFriendList;
} }
private void SpawnPairingModeTask() { private void SpawnPairingModeTask() {
@ -75,12 +74,12 @@ namespace XIVChatPlugin {
Task<UdpReceiveResult>? receiveTask = null; Task<UdpReceiveResult>? receiveTask = null;
while (this.Running) { while (this.Running) {
if (!this.plugin.Config.PairingMode) { if (!this._plugin.Config.PairingMode) {
await Task.Delay(5_000); await Task.Delay(5_000);
continue; continue;
} }
var playerName = this.plugin.Interface.ClientState.LocalPlayer?.Name; var playerName = this._plugin.Interface.ClientState.LocalPlayer?.Name;
if (playerName != null) { if (playerName != null) {
lastPlayerName = playerName; lastPlayerName = playerName;
@ -115,8 +114,8 @@ namespace XIVChatPlugin {
} }
var utf8 = Encoding.UTF8.GetBytes(lastPlayerName); var utf8 = Encoding.UTF8.GetBytes(lastPlayerName);
var portBytes = BitConverter.GetBytes(this.plugin.Config.Port).Reverse().ToArray(); var portBytes = BitConverter.GetBytes(this._plugin.Config.Port).Reverse().ToArray();
var key = this.plugin.Config.KeyPair!.PublicKey; var key = this._plugin.Config.KeyPair!.PublicKey;
// magic + string length + string + port + key // 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 var payload = new byte[1 + 1 + utf8.Length + portBytes.Length + key.Length]; // assuming names can only be 32 bytes here
payload[0] = 14; payload[0] = 14;
@ -135,43 +134,48 @@ namespace XIVChatPlugin {
private async void OnReceiveFriendList(List<Player> friends) { private async void OnReceiveFriendList(List<Player> friends) {
var msg = new ServerPlayerList(PlayerListType.Friend, friends.ToArray()); 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)) { if (!this.Clients.TryGetValue(id, out var client)) {
continue; continue;
} }
try { 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) { } catch (Exception ex) {
PluginLog.LogError($"Could not send message: {ex.Message}"); PluginLog.LogError($"Could not send message: {ex.Message}");
} }
} }
this.waitingForFriendList.Clear(); this._waitingForFriendList.Clear();
} }
public void Spawn() { public void Spawn() {
var port = this.plugin.Config.Port; var port = this._plugin.Config.Port;
Task.Run(async () => { Task.Run(async () => {
this.listener = new TcpListener(IPAddress.Any, port); this._listener = new TcpListener(IPAddress.Any, port);
this.listener.Start(); this._listener.Start();
this.running = true; this._running = true;
PluginLog.Log("Running..."); PluginLog.Log("Running...");
this.SpawnPairingModeTask(); this.SpawnPairingModeTask();
while (!this.tokenSource.IsCancellationRequested) { while (!this._tokenSource.IsCancellationRequested) {
var conn = await this.listener.GetTcpClient(this.tokenSource); var conn = await this._listener.GetTcpClient(this._tokenSource);
this.SpawnClientTask(conn); if (conn == null) {
continue;
}
var client = new TcpConnected(conn);
this.SpawnClientTask(client, true);
} }
this.running = false; this._running = false;
}); });
} }
public void RegenerateKeyPair() { public void RegenerateKeyPair() {
this.plugin.Config.KeyPair = PublicKeyBox.GenerateKeyPair(); this._plugin.Config.KeyPair = PublicKeyBox.GenerateKeyPair();
this.plugin.Config.Save(); this._plugin.Config.Save();
} }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "delegate")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "delegate")]
@ -182,13 +186,13 @@ namespace XIVChatPlugin {
var chatCode = new ChatCode((ushort) type); var chatCode = new ChatCode((ushort) type);
if (!this.plugin.Config.SendBattle && chatCode.IsBattle()) { if (!this._plugin.Config.SendBattle && chatCode.IsBattle()) {
return; return;
} }
var chunks = new List<Chunk>(); 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) { if (sender.Payloads.Count > 0) {
var format = this.FormatFor(chatCode.Type); var format = this.FormatFor(chatCode.Type);
@ -213,126 +217,119 @@ namespace XIVChatPlugin {
chunks chunks
); );
this.backlog.AddLast(msg); this._backlog.AddLast(msg);
while (this.backlog.Count > this.plugin.Config.BacklogCount) { while (this._backlog.Count > this._plugin.Config.BacklogCount) {
this.backlog.RemoveFirst(); this._backlog.RemoveFirst();
} }
foreach (var client in this.clients.Values) { foreach (var client in this._clients.Values) {
client.Queue.Writer.TryWrite(msg); client.Queue.Writer.TryWrite(msg);
} }
} }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "delegate")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "delegate")]
public void OnFrameworkUpdate(Framework framework) { 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.BroadcastPlayerData();
this.sendPlayerData = false; this._sendPlayerData = false;
} }
if (!this.toGame.TryDequeue(out var message)) { if (!this._toGame.TryDequeue(out var message)) {
return; return;
} }
this.plugin.Functions.ProcessChatBox(message); this._plugin.Functions.ProcessChatBox(message);
} }
private static readonly IReadOnlyList<byte> Magic = new byte[] { private static readonly IReadOnlyList<byte> Magic = new byte[] {
14, 20, 67, 14, 20, 67,
}; };
private void SpawnClientTask(TcpClient? conn) { internal void SpawnClientTask(BaseClient client, bool requiresMagic) {
if (conn == null) { var id = Guid.NewGuid();
return; this._clients[id] = client;
}
Task.Run(async () => { 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 // only listen for magic for five seconds
var magic = new byte[Magic.Count]; using var cts = new CancellationTokenSource();
var read = 0; cts.CancelAfter(TimeSpan.FromSeconds(5));
// only listen for magic for five seconds // read magic bytes
using var cts = new CancellationTokenSource(); while (read < magic.Length) {
cts.CancelAfter(TimeSpan.FromSeconds(5)); if (cts.IsCancellationRequested) {
return;
}
// read magic bytes read += await client.ReadAsync(magic, read, magic.Length - read, cts.Token);
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;
}
} }
// ignore this connection if incorrect magic bytes var handshake = await KeyExchange.ServerHandshake(this._plugin.Config.KeyPair!, client);
if (!magic.SequenceEqual(Magic)) { client.Handshake = handshake;
return;
}
var handshake = await KeyExchange.ServerHandshake(this.plugin.Config.KeyPair!, stream);
var newClient = new Client(conn) {
Handshake = handshake,
};
// if this public key isn't trusted, prompt first // 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 configured to not accept new clients, reject connection
if (!this.plugin.Config.AcceptNewClients) { if (!this._plugin.Config.AcceptNewClients) {
return; return;
} }
var accepted = Channel.CreateBounded<bool>(1); var accepted = Channel.CreateBounded<bool>(1);
await this.pendingClients.Writer.WriteAsync(Tuple.Create(newClient, accepted), this.tokenSource.Token); await this.PendingClients.Writer.WriteAsync(Tuple.Create(client, accepted), this._tokenSource.Token);
if (!await accepted.Reader.ReadAsync(this.tokenSource.Token)) { if (!await accepted.Reader.ReadAsync(this._tokenSource.Token)) {
return; return;
} }
} }
var id = Guid.NewGuid(); client.Connected = true;
newClient.Connected = true;
this.clients[id] = newClient;
// send availability // send availability
var available = this.plugin.Interface.ClientState.LocalPlayer != null; var available = this._plugin.Interface.ClientState.LocalPlayer != null;
try { 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) { } catch (Exception ex) {
PluginLog.LogError($"Could not send message: {ex.Message}"); PluginLog.LogError($"Could not send message: {ex.Message}");
} }
// send player data // send player data
try { try {
await this.SendPlayerData(newClient); await this.SendPlayerData(client);
} catch (Exception ex) { } catch (Exception ex) {
PluginLog.LogError($"Could not send message: {ex.Message}"); PluginLog.LogError($"Could not send message: {ex.Message}");
} }
// send current channel // send current channel
try { try {
var channel = this.currentChannel; var channel = this._currentChannel;
await SecretMessage.SendSecretMessage( await SecretMessage.SendSecretMessage(
stream, client,
handshake.Keys.tx, handshake.Keys.tx,
new ServerChannel( new ServerChannel(
channel, channel,
this.LocalisedChannelName(channel) this.LocalisedChannelName(channel)
), ),
this.tokenSource.Token this._tokenSource.Token
); );
} catch (Exception ex) { } catch (Exception ex) {
PluginLog.LogError($"Could not send message: {ex.Message}"); PluginLog.LogError($"Could not send message: {ex.Message}");
} }
var listen = Task.Run(async () => { var listen = Task.Run(async () => {
conn.ReceiveTimeout = 5_000; while (this._clients.TryGetValue(id, out var client) && client.Connected && !client.TokenSource.IsCancellationRequested) {
while (this.clients.TryGetValue(id, out var client) && client.Connected && !client.TokenSource.IsCancellationRequested && conn.Connected) {
byte[] msg; byte[] msg;
try { 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) { } catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut) {
continue; continue;
} catch (Exception ex) { } catch (Exception ex) {
@ -340,116 +337,115 @@ namespace XIVChatPlugin {
continue; continue;
} }
var op = (ClientOperation) msg[0]; await this.ProcessMessage(id, client, handshake, msg);
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;
}
} }
}); });
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 { try {
var msg = await client.Queue.Reader.ReadAsync(client.TokenSource.Token); 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) { } catch (Exception ex) {
PluginLog.LogError($"Could not send message: {ex.Message}"); PluginLog.LogError($"Could not send message: {ex.Message}");
} }
} }
try { client.Disconnect();
conn.Close();
} catch (ObjectDisposedException) {
}
await listen; await listen;
this.clients.TryRemove(id, out _); this._clients.TryRemove(id, out _);
PluginLog.Log($"Client thread ended: {id}"); PluginLog.Log($"Client thread ended: {id}");
}).ContinueWith(_ => { }).ContinueWith(_ => {
try { client.Disconnect();
conn.Close(); this._clients.TryRemove(id, out var _);
} catch (ObjectDisposedException) {
}
}); });
} }
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 class NameFormatting {
public string Before { get; private set; } = string.Empty; public string Before { get; private set; } = string.Empty;
public string After { get; private set; } = string.Empty; public string After { get; private set; } = string.Empty;
@ -476,14 +472,14 @@ namespace XIVChatPlugin {
return cached; 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) { if (logKind == null) {
return null; return null;
} }
var format = logKind.Format; 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) { static bool IsStringParam(Payload payload, byte num) {
var data = payload.Encode(); var data = payload.Encode();
@ -519,14 +515,14 @@ namespace XIVChatPlugin {
return nameFormatting; 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 size = 5 + SecretMessage.MacSize(); // assume 5 bytes for payload lead-in, although it's likely to be less
var responseMessages = new List<ServerMessage>(); var responseMessages = new List<ServerMessage>();
async Task SendBacklog() { async Task SendBacklog() {
var resp = new ServerBacklog(responseMessages.ToArray()); var resp = new ServerBacklog(responseMessages.ToArray());
try { 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) { } catch (Exception ex) {
PluginLog.LogError($"Could not send backlog: {ex.Message}"); PluginLog.LogError($"Could not send backlog: {ex.Message}");
} }
@ -628,7 +624,7 @@ namespace XIVChatPlugin {
return chunks; 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) { private static IEnumerable<string> Wrap(string input) {
const int limit = 500; const int limit = 500;
@ -701,11 +697,13 @@ namespace XIVChatPlugin {
private void BroadcastMessage(IEncodable message) { private void BroadcastMessage(IEncodable message) {
foreach (var client in this.Clients.Values) { foreach (var client in this.Clients.Values) {
if (client.Handshake == null || client.Conn == null) { if (client.Handshake == null) {
continue; 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, _ => 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) { public void OnChatChannelChange(uint channel) {
var inputChannel = (InputChannel) channel; var inputChannel = (InputChannel) channel;
this.currentChannel = inputChannel; this._currentChannel = inputChannel;
var localisedName = this.LocalisedChannelName(inputChannel); var localisedName = this.LocalisedChannelName(inputChannel);
@ -757,27 +755,27 @@ namespace XIVChatPlugin {
} }
private PlayerData? GeneratePlayerData() { private PlayerData? GeneratePlayerData() {
var player = this.plugin.Interface.ClientState.LocalPlayer; var player = this._plugin.Interface.ClientState.LocalPlayer;
if (player == null) { if (player == null) {
return null; return null;
} }
var homeWorld = player.HomeWorld.GameData.Name; var homeWorld = player.HomeWorld.GameData.Name;
var currentWorld = player.CurrentWorld.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 location = territory?.PlaceName?.Value?.Name ?? "???";
var name = player.Name; var name = player.Name;
return new PlayerData(homeWorld, currentWorld, location, name); return new PlayerData(homeWorld, currentWorld, location, name);
} }
private async Task SendPlayerData(Client client) { private async Task SendPlayerData(BaseClient client) {
var playerData = this.GeneratePlayerData(); var playerData = this.GeneratePlayerData();
if (playerData == null) { if (playerData == null) {
return; return;
} }
await SecretMessage.SendSecretMessage(client.Conn.GetStream(), client.Handshake!.Keys.tx, playerData); await SecretMessage.SendSecretMessage(client, client.Handshake!.Keys.tx, playerData);
} }
private void BroadcastPlayerData() { private void BroadcastPlayerData() {
@ -794,7 +792,7 @@ namespace XIVChatPlugin {
public void OnLogIn(object sender, EventArgs e) { public void OnLogIn(object sender, EventArgs e) {
this.BroadcastAvailability(true); this.BroadcastAvailability(true);
// send player data on next framework update // send player data on next framework update
this.sendPlayerData = true; this._sendPlayerData = true;
} }
public void OnLogOut(object sender, EventArgs e) { public void OnLogOut(object sender, EventArgs e) {
@ -802,20 +800,19 @@ namespace XIVChatPlugin {
this.BroadcastPlayerData(); 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() { public void Dispose() {
// stop accepting new clients // stop accepting new clients
this.tokenSource.Cancel(); this._tokenSource.Cancel();
foreach (var client in this.clients.Values) { foreach (var client in this._clients.Values) {
Task.Run(async () => { Task.Run(async () => {
// tell clients we're shutting down // tell clients we're shutting down
if (client.Handshake != null) { if (client.Handshake != null) {
try { try {
// time out after 5 seconds await SecretMessage.SendSecretMessage(client, client.Handshake.Keys.tx, ServerShutdown.Instance);
client.Conn.SendTimeout = 5_000;
await SecretMessage.SendSecretMessage(client.Conn.GetStream(), client.Handshake.Keys.tx, ServerShutdown.Instance);
} catch (Exception) { } catch (Exception) {
// ignored
} }
} }
@ -824,36 +821,7 @@ namespace XIVChatPlugin {
}); });
} }
this.plugin.Functions.ReceiveFriendList -= this.OnReceiveFriendList; 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;
} }
} }

View File

@ -114,14 +114,20 @@
<Reference Include="System.Data" /> <Reference Include="System.Data" />
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
<Reference Include="System.Xml" /> <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>
<ItemGroup> <ItemGroup>
<Compile Include="Client.cs" />
<Compile Include="Configuration.cs" /> <Compile Include="Configuration.cs" />
<Compile Include="Extensions.cs" /> <Compile Include="Extensions.cs" />
<Compile Include="GameFunctions.cs" /> <Compile Include="GameFunctions.cs" />
<Compile Include="Plugin.cs" /> <Compile Include="Plugin.cs" />
<Compile Include="PluginUi.cs" /> <Compile Include="PluginUi.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Relay.cs" />
<Compile Include="Server.cs" /> <Compile Include="Server.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

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