431 lines
10 KiB
C#
431 lines
10 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Net.Http;
|
|
using System.Net.Quic;
|
|
using System.Net.Sockets;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Crysome.Common.Network;
|
|
using Crysome.Common.Network.Packets;
|
|
using Crysome.Common.Network.Transport;
|
|
using Crysome.Server.Data;
|
|
|
|
namespace Crysome.Server.Network;
|
|
|
|
public class CrysomeServer
|
|
{
|
|
public readonly PacketChannel PacketChannel;
|
|
|
|
private QuicListener _listener;
|
|
|
|
private CancellationTokenSource _cts;
|
|
|
|
private Task _acceptTask;
|
|
|
|
private X509Certificate2 _serverCert;
|
|
|
|
private UdpClient _legacySock;
|
|
|
|
private Task _legacyTask;
|
|
|
|
private readonly ConcurrentDictionary<CrysomeClient, string> _clientKeys = new ConcurrentDictionary<CrysomeClient, string>();
|
|
|
|
private readonly ConcurrentDictionary<uint, CrysomeClient> _legacySessions = new ConcurrentDictionary<uint, CrysomeClient>();
|
|
|
|
private readonly ConcurrentDictionary<uint, string> _legacySessionKeys = new ConcurrentDictionary<uint, string>();
|
|
|
|
private int _nextUdpToken = 1;
|
|
|
|
private readonly ConcurrentDictionary<uint, CrysomeClient> _udpSessionMap = new ConcurrentDictionary<uint, CrysomeClient>();
|
|
|
|
public ConcurrentDictionary<string, CrysomeClient> ConnectedClients { get; private set; }
|
|
|
|
public bool Listening { get; private set; }
|
|
|
|
public int Port { get; set; } = 7777;
|
|
|
|
public int QuicPort { get; set; } = 7778;
|
|
|
|
public AppDataStore Store { get; set; }
|
|
|
|
public int ClientCount => ConnectedClients.Count;
|
|
|
|
public event EventHandler<PacketEventArgs> PacketReceived;
|
|
|
|
public event EventHandler<ClientEventArgs> ClientConnected;
|
|
|
|
public event EventHandler<ClientEventArgs> ClientDisconnected;
|
|
|
|
public event EventHandler<UdpFrameEventArgs> UdpFrameReceived;
|
|
|
|
public CrysomeServer()
|
|
{
|
|
PacketChannel = new PacketChannel();
|
|
ConnectedClients = new ConcurrentDictionary<string, CrysomeClient>();
|
|
}
|
|
|
|
public uint RegisterUdpSession(CrysomeClient client)
|
|
{
|
|
uint num = (uint)Interlocked.Increment(ref _nextUdpToken);
|
|
_udpSessionMap[num] = client;
|
|
client.UdpSessionToken = num;
|
|
return num;
|
|
}
|
|
|
|
public void UnregisterUdpSession(CrysomeClient client)
|
|
{
|
|
if (client != null && client.UdpSessionToken != 0)
|
|
{
|
|
_udpSessionMap.TryRemove(client.UdpSessionToken, out var _);
|
|
}
|
|
}
|
|
|
|
public void Start()
|
|
{
|
|
if (Listening)
|
|
{
|
|
Stop();
|
|
}
|
|
bool num = QuicPort > 0;
|
|
bool flag = Port > 0;
|
|
if (!num && !flag)
|
|
{
|
|
throw new InvalidOperationException("At least one of Port (RUDP) or QuicPort (QUIC) must be greater than 0.");
|
|
}
|
|
_cts = new CancellationTokenSource();
|
|
if (num)
|
|
{
|
|
if (!QuicListener.IsSupported)
|
|
{
|
|
if (!flag)
|
|
{
|
|
throw new InvalidOperationException("QUIC listener is not supported. Use Windows 11 / Server 2022 or newer with MsQuic enabled, or set QuicPort to 0 and use RUDP only on Port.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_serverCert = QuicCertificateHelper.CreateServerCertificate();
|
|
_listener = QuicTransportSession.StartListenerAsync(QuicPort, _serverCert, _cts.Token).GetAwaiter().GetResult();
|
|
}
|
|
}
|
|
if (flag)
|
|
{
|
|
_legacySock = new UdpClient(Port);
|
|
_legacySock.Client.ReceiveBufferSize = 8388608;
|
|
_legacySock.Client.SendBufferSize = 8388608;
|
|
_legacyTask = Task.Run(() => LegacyReceiveLoop(_cts.Token));
|
|
}
|
|
Listening = true;
|
|
if (_listener != null)
|
|
{
|
|
_acceptTask = Task.Run(() => AcceptLoopAsync(_cts.Token));
|
|
}
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
Listening = false;
|
|
try
|
|
{
|
|
_cts?.Cancel();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
try
|
|
{
|
|
_listener?.DisposeAsync().AsTask().Wait(3000);
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
_listener = null;
|
|
try
|
|
{
|
|
_legacySock?.Close();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
_legacySock = null;
|
|
foreach (KeyValuePair<string, CrysomeClient> connectedClient in ConnectedClients)
|
|
{
|
|
try
|
|
{
|
|
connectedClient.Value.Disconnect();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
ConnectedClients.Clear();
|
|
_clientKeys.Clear();
|
|
_legacySessions.Clear();
|
|
_legacySessionKeys.Clear();
|
|
_udpSessionMap.Clear();
|
|
}
|
|
|
|
private async Task LegacyReceiveLoop(CancellationToken ct)
|
|
{
|
|
uint sessionId = default(uint);
|
|
byte b = default(byte);
|
|
uint num = default(uint);
|
|
uint num2 = default(uint);
|
|
ushort num3 = default(ushort);
|
|
ushort num4 = default(ushort);
|
|
byte b2 = default(byte);
|
|
while (!ct.IsCancellationRequested && Listening && _legacySock != null)
|
|
{
|
|
try
|
|
{
|
|
UdpReceiveResult udpReceiveResult = await _legacySock.ReceiveAsync().ConfigureAwait(continueOnCapturedContext: false);
|
|
byte[] buffer = udpReceiveResult.Buffer;
|
|
if (!RudpChannel.ParseHeader(buffer, buffer.Length, out sessionId, out b, out num, out num2, out num3, out num4, out b2))
|
|
{
|
|
continue;
|
|
}
|
|
if (_legacySessions.TryGetValue(sessionId, out var client))
|
|
{
|
|
goto IL_0275;
|
|
}
|
|
if ((b & 9) == 0 || sessionId == 0)
|
|
{
|
|
continue;
|
|
}
|
|
RudpChannel val = new RudpChannel(sessionId, udpReceiveResult.RemoteEndPoint, _legacySock);
|
|
val.UnreliableFrameReceived += delegate(byte typeId, byte[] data)
|
|
{
|
|
if (_legacySessions.TryGetValue(sessionId, out var value))
|
|
{
|
|
try
|
|
{
|
|
this.UdpFrameReceived?.Invoke(this, new UdpFrameEventArgs(value, typeId, data));
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
};
|
|
val.Disconnected += delegate
|
|
{
|
|
LegacyClientGone(sessionId);
|
|
};
|
|
val.Start();
|
|
client = new CrysomeClient(val);
|
|
string key = Guid.NewGuid().ToString("N");
|
|
_legacySessions[sessionId] = client;
|
|
_legacySessionKeys[sessionId] = key;
|
|
ConnectedClients[key] = client;
|
|
Task.Run(delegate
|
|
{
|
|
ClientReadLoop(key, client, ct);
|
|
}, ct);
|
|
OnClientConnected(client);
|
|
goto IL_0275;
|
|
IL_0275:
|
|
CrysomeClient obj = client;
|
|
if (obj != null)
|
|
{
|
|
RudpChannel rudpChannel = obj.GetRudpChannel();
|
|
if (rudpChannel != null)
|
|
{
|
|
rudpChannel.FeedDatagram(buffer);
|
|
}
|
|
}
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
break;
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
private void LegacyClientGone(uint sessionId)
|
|
{
|
|
if (_legacySessions.TryRemove(sessionId, out var value))
|
|
{
|
|
if (_legacySessionKeys.TryRemove(sessionId, out var value2))
|
|
{
|
|
ConnectedClients.TryRemove(value2, out var _);
|
|
}
|
|
UnregisterUdpSession(value);
|
|
OnClientDisconnected(value);
|
|
}
|
|
}
|
|
|
|
private async Task AcceptLoopAsync(CancellationToken ct)
|
|
{
|
|
while (!ct.IsCancellationRequested && Listening && _listener != null)
|
|
{
|
|
try
|
|
{
|
|
QuicConnection conn = await _listener.AcceptConnectionAsync(ct).ConfigureAwait(continueOnCapturedContext: false);
|
|
Task.Run(() => HandleConnectionAsync(conn, ct), ct);
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
break;
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task HandleConnectionAsync(QuicConnection conn, CancellationToken ct)
|
|
{
|
|
QuicTransportSession session = null;
|
|
CrysomeClient client = null;
|
|
string key = null;
|
|
try
|
|
{
|
|
session = await QuicTransportSession.AcceptServerAsync(conn, ct).ConfigureAwait(continueOnCapturedContext: false);
|
|
client = new CrysomeClient((ITransportSession)(object)session, true);
|
|
key = Guid.NewGuid().ToString("N");
|
|
_clientKeys[client] = key;
|
|
ConnectedClients[key] = client;
|
|
session.UnreliableFrameReceived += delegate(byte typeId, byte[] data)
|
|
{
|
|
try
|
|
{
|
|
this.UdpFrameReceived?.Invoke(this, new UdpFrameEventArgs(client, typeId, data));
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
};
|
|
Task.Run(delegate
|
|
{
|
|
ClientReadLoop(key, client, ct);
|
|
}, ct);
|
|
OnClientConnected(client);
|
|
}
|
|
catch
|
|
{
|
|
try
|
|
{
|
|
QuicTransportSession obj2 = session;
|
|
if (obj2 != null)
|
|
{
|
|
obj2.Dispose();
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
if (client != null && key != null)
|
|
{
|
|
ClientGone(client, key);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ClientGone(CrysomeClient client, string key)
|
|
{
|
|
if (client == null)
|
|
{
|
|
return;
|
|
}
|
|
CrysomeClient value;
|
|
string value2;
|
|
foreach (KeyValuePair<uint, CrysomeClient> legacySession in _legacySessions)
|
|
{
|
|
if (legacySession.Value == client)
|
|
{
|
|
_legacySessions.TryRemove(legacySession.Key, out value);
|
|
_legacySessionKeys.TryRemove(legacySession.Key, out value2);
|
|
break;
|
|
}
|
|
}
|
|
_clientKeys.TryRemove(client, out value2);
|
|
if (key != null)
|
|
{
|
|
ConnectedClients.TryRemove(key, out value);
|
|
}
|
|
UnregisterUdpSession(client);
|
|
OnClientDisconnected(client);
|
|
}
|
|
|
|
private void ClientReadLoop(string key, CrysomeClient client, CancellationToken ct)
|
|
{
|
|
try
|
|
{
|
|
while (!ct.IsCancellationRequested && client.IsConnected)
|
|
{
|
|
IPacket packet = client.ReadPacket();
|
|
OnPacketReceived(client, packet);
|
|
}
|
|
}
|
|
catch (IOException)
|
|
{
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
finally
|
|
{
|
|
ClientGone(client, key);
|
|
}
|
|
}
|
|
|
|
public void DisconnectClient(CrysomeClient client)
|
|
{
|
|
if (client == null)
|
|
{
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
client.Disconnect();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
public List<CrysomeClient> GetClientSnapshot()
|
|
{
|
|
return new List<CrysomeClient>(ConnectedClients.Values);
|
|
}
|
|
|
|
private void OnClientConnected(CrysomeClient client)
|
|
{
|
|
this.ClientConnected?.Invoke(this, new ClientEventArgs(client));
|
|
if (Store == null || !Store.Settings.TelegramEnabled || string.IsNullOrEmpty(Store.Settings.TelegramToken) || string.IsNullOrEmpty(Store.Settings.TelegramChatId))
|
|
{
|
|
return;
|
|
}
|
|
string ip = client.RemoteAddress?.Address?.ToString() ?? "Unknown";
|
|
Task.Run(async delegate
|
|
{
|
|
try
|
|
{
|
|
string stringToEscape = "\ud83d\udd14 *New Client Connected*\nIP: " + ip;
|
|
string requestUri = $"https://api.telegram.org/bot{Store.Settings.TelegramToken}/sendMessage?chat_id={Store.Settings.TelegramChatId}&text={Uri.EscapeDataString(stringToEscape)}&parse_mode=Markdown";
|
|
using HttpClient hc = new HttpClient();
|
|
await hc.GetAsync(requestUri);
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
});
|
|
}
|
|
|
|
private void OnClientDisconnected(CrysomeClient client)
|
|
{
|
|
this.ClientDisconnected?.Invoke(this, new ClientEventArgs(client));
|
|
}
|
|
|
|
private void OnPacketReceived(CrysomeClient client, IPacket packet)
|
|
{
|
|
this.PacketReceived?.Invoke(this, new PacketEventArgs(client, packet));
|
|
PacketChannel.HandlePacket(client, packet);
|
|
}
|
|
}
|