XIVChat/XIVChat Desktop/Connection.cs

268 lines
11 KiB
C#

using System;
using System.ComponentModel;
using System.Linq;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using System.Windows;
using XIVChatCommon;
namespace XIVChat_Desktop {
public class Connection : INotifyPropertyChanged {
private readonly App app;
private readonly string host;
private readonly ushort port;
private TcpClient? client;
private readonly Channel<string> outgoing = Channel.CreateUnbounded<string>();
private readonly Channel<byte[]> incoming = Channel.CreateUnbounded<byte[]>();
private readonly Channel<byte> cancelChannel = Channel.CreateBounded<byte>(2);
public readonly CancellationTokenSource cancel = new CancellationTokenSource();
public event PropertyChangedEventHandler? PropertyChanged;
public string? CurrentChannel { get; private set; }
public Connection(App app, string host, ushort port) {
this.app = app;
this.host = host;
this.port = port;
}
public void SendMessage(string message) {
this.outgoing.Writer.TryWrite(message);
}
public void Disconnect() {
this.cancel.Cancel();
for (var i = 0; i < 2; i++) {
this.cancelChannel.Writer.TryWrite(1);
}
}
public async Task Connect() {
this.client = new TcpClient(this.host, this.port);
var stream = this.client.GetStream();
await stream.WriteAsync(new byte[] {
14, 20, 67,
});
var handshake = await KeyExchange.ClientHandshake(this.app.Config.KeyPair, stream);
if (!this.app.Config.TrustedKeys.Any(trusted => trusted.Key.SequenceEqual(handshake.RemotePublicKey))) {
var trustChannel = Channel.CreateBounded<bool>(1);
this.Dispatch(() => {
new TrustDialog(this.app.Window, trustChannel.Writer, handshake.RemotePublicKey).Show();
});
var trusted = await trustChannel.Reader.ReadAsync(this.cancel.Token);
if (!trusted) {
goto Close;
}
}
// clear messages if connecting to a different host
var currentHost = $"{this.host}:{this.port}";
var sameHost = this.app.LastHost == currentHost;
if (!sameHost) {
this.Dispatch(() => {
this.app.Window.ClearAllMessages();
this.app.LastHost = currentHost;
});
}
this.Dispatch(() => {
this.app.Window.AddSystemMessage("Connected");
});
// check if backlog or catch-up is needed
if (sameHost) {
// catch-up
var lastRealMessage = this.app.Window.Messages.LastOrDefault(msg => msg.Channel != 0);
if (lastRealMessage != null) {
var catchUp = new ClientCatchUp {
After = lastRealMessage.Timestamp,
};
await SecretMessage.SendSecretMessage(stream, handshake.Keys.tx, catchUp, this.cancel.Token);
}
} else if (this.app.Config.BacklogMessages > 0) {
// backlog
var backlogReq = new ClientBacklog {
Amount = this.app.Config.BacklogMessages,
};
await SecretMessage.SendSecretMessage(stream, handshake.Keys.tx, backlogReq, this.cancel.Token);
}
// start a task for accepting incoming messages and sending them down the channel
_ = Task.Run(async () => {
var inc = SecretMessage.ReadSecretMessage(stream, handshake.Keys.rx, this.cancel.Token);
var cancel = this.cancelChannel.Reader.ReadAsync().AsTask();
while (!this.cancel.IsCancellationRequested) {
var result = await Task.WhenAny(inc, cancel);
if (result == inc) {
if (inc.Exception != null) {
this.Dispatch(() => {
this.app.Window.AddSystemMessage("Error reading incoming message.");
// ReSharper disable once LocalizableElement
Console.WriteLine($"Error reading incoming message: {inc.Exception.Message}");
foreach (var inner in inc.Exception.InnerExceptions) {
Console.WriteLine(inner.StackTrace);
}
});
break;
}
var rawMessage = await inc;
inc = SecretMessage.ReadSecretMessage(stream, handshake.Keys.rx, this.cancel.Token);
await this.incoming.Writer.WriteAsync(rawMessage);
} else if (result == cancel) {
break;
}
}
});
var incoming = this.incoming.Reader.ReadAsync().AsTask();
var outgoing = this.outgoing.Reader.ReadAsync().AsTask();
var cancel = this.cancelChannel.Reader.ReadAsync().AsTask();
// listen for incoming and outgoing messages and cancel requests
while (!this.cancel.IsCancellationRequested) {
var result = await Task.WhenAny(incoming, outgoing, cancel);
if (result == incoming) {
if (this.incoming.Reader.Completion.IsCompleted) {
break;
}
var rawMessage = await incoming;
incoming = this.incoming.Reader.ReadAsync().AsTask();
await this.HandleIncoming(rawMessage);
} else if (result == outgoing) {
var toSend = await outgoing;
outgoing = this.outgoing.Reader.ReadAsync().AsTask();
var message = new ClientMessage {
Content = toSend,
};
try {
await SecretMessage.SendSecretMessage(stream, handshake.Keys.tx, message, this.cancel.Token);
} catch (Exception ex) {
this.Dispatch(() => {
this.app.Window.AddSystemMessage("Error sending message.");
// ReSharper disable once LocalizableElement
Console.WriteLine($"Error sending message: {ex.Message}");
Console.WriteLine(ex.StackTrace);
});
break;
}
} else if (result == cancel) {
try {
await SecretMessage.SendSecretMessage(stream, handshake.Keys.tx, ClientShutdown.Instance, this.cancel.Token);
} catch (Exception ex) {
this.Dispatch(() => {
this.app.Window.AddSystemMessage("Error sending message.");
// ReSharper disable once LocalizableElement
Console.WriteLine($"Error sending message: {ex.Message}");
Console.WriteLine(ex.StackTrace);
});
}
break;
}
}
// at this point, we are disconnected, so log it
this.Dispatch(() => {
this.app.Window.AddSystemMessage("Disconnected");
});
// wait up to a second to send the shutdown packet
await Task.WhenAny(Task.Delay(1_000), SecretMessage.SendSecretMessage(stream, handshake.Keys.tx, ClientShutdown.Instance));
Close:
try {
this.client.Close();
} catch (ObjectDisposedException) { }
}
private async Task HandleIncoming(byte[] rawMessage) {
var type = (ServerOperation)rawMessage[0];
var payload = new byte[rawMessage.Length - 1];
Array.Copy(rawMessage, 1, payload, 0, payload.Length);
switch (type) {
case ServerOperation.Pong:
break;
case ServerOperation.Message:
var message = ServerMessage.Decode(payload);
this.Dispatch(() => {
this.app.Window.AddMessage(message);
});
break;
case ServerOperation.Shutdown:
this.Disconnect();
break;
case ServerOperation.PlayerData:
var playerData = payload.Length == 0 ? null : PlayerData.Decode(payload);
var visibility = playerData == null ? Visibility.Collapsed : Visibility.Visible;
this.Dispatch(() => {
var window = this.app.Window;
window.LoggedInAs.Content = playerData?.name;
window.LoggedInAs.Visibility = visibility;
window.LoggedInAsSeparator.Visibility = visibility;
window.CurrentWorld.Content = playerData?.currentWorld;
window.CurrentWorld.Visibility = visibility;
window.CurrentWorldSeparator.Visibility = visibility;
window.Location.Content = playerData?.location;
window.Location.Visibility = visibility;
});
break;
case ServerOperation.Availability:
break;
case ServerOperation.Channel:
var channel = ServerChannel.Decode(payload);
this.CurrentChannel = channel.name;
this.Dispatch(() => {
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.CurrentChannel)));
});
break;
case ServerOperation.Backlog:
var backlog = ServerBacklog.Decode(payload);
foreach (var msg in backlog.messages) {
this.Dispatch(() => {
this.app.Window.AddMessage(msg);
});
}
break;
case ServerOperation.PlayerList:
break;
case ServerOperation.LinkshellList:
break;
}
}
private void Dispatch(Action action) {
this.app.Dispatcher.BeginInvoke(action);
}
}
}