feat: begin adding relay support
This commit is contained in:
parent
1128a49557
commit
ed34db2131
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 ?
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
Loading…
Reference in New Issue