diff --git a/GC-local-server-rewrite.sln b/GC-local-server-rewrite.sln index c6258c5..d6b4c18 100644 --- a/GC-local-server-rewrite.sln +++ b/GC-local-server-rewrite.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedProject", "SharedProj EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MudAdmin", "MudAdmin\MudAdmin.csproj", "{DC8E30E9-F81E-4E28-A4D2-F4576C77FFBE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GCRelayServer", "GCRelayServer\GCRelayServer.csproj", "{268178DF-6345-4D9E-A389-9E809CBF39C9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {DC8E30E9-F81E-4E28-A4D2-F4576C77FFBE}.Debug|Any CPU.Build.0 = Debug|Any CPU {DC8E30E9-F81E-4E28-A4D2-F4576C77FFBE}.Release|Any CPU.ActiveCfg = Release|Any CPU {DC8E30E9-F81E-4E28-A4D2-F4576C77FFBE}.Release|Any CPU.Build.0 = Release|Any CPU + {268178DF-6345-4D9E-A389-9E809CBF39C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {268178DF-6345-4D9E-A389-9E809CBF39C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {268178DF-6345-4D9E-A389-9E809CBF39C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {268178DF-6345-4D9E-A389-9E809CBF39C9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/GCRelayServer/DictEntry.cs b/GCRelayServer/DictEntry.cs new file mode 100644 index 0000000..88d4af5 --- /dev/null +++ b/GCRelayServer/DictEntry.cs @@ -0,0 +1,23 @@ +using System.Net; + +namespace GCRelayServer; + +public class DictEntry +{ + public List EndPoints { get; set; } = new(); + + public DateTime LastAccessTime { get; set; } = DateTime.Now; + + public void AddEndpoint(EndPoint endPoint, bool shouldClear = false) + { + if (shouldClear) + { + EndPoints.Clear(); + } + if (EndPoints.Contains(endPoint)) + { + return; + } + EndPoints.Add(endPoint); + } +} \ No newline at end of file diff --git a/GCRelayServer/GCRelayServer.csproj b/GCRelayServer/GCRelayServer.csproj new file mode 100644 index 0000000..0fe1f1c --- /dev/null +++ b/GCRelayServer/GCRelayServer.csproj @@ -0,0 +1,16 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + diff --git a/GCRelayServer/Program.cs b/GCRelayServer/Program.cs new file mode 100644 index 0000000..c0883df --- /dev/null +++ b/GCRelayServer/Program.cs @@ -0,0 +1,59 @@ +using System.Net; +using Swan.Logging; + +namespace GCRelayServer +{ + + public static class Program + { + public static void Main(string[] args) + { +#if DEBUG + ConsoleLogger.Instance.LogLevel = LogLevel.Debug; +#endif + // UDP server port + var port = 3333; + if (args.Length > 0) + { + port = int.Parse(args[0]); + } + + $"UDP server port: {port}".Info(); + + // Create a new UDP echo server + var server = new RelayServer(IPAddress.Any, port); + + // Start the server + "Server starting...".Info(); + server.Start(); + "Server started".Info(); + + "Press Enter to stop the server or '!' to restart the server...".Info(); + + // Perform text input + for (;;) + { + var line = Console.ReadLine(); + if (string.IsNullOrEmpty(line)) + { + break; + } + + // Restart the server + if (line != "!") + { + continue; + } + "Server restarting...".Info(); + server.Restart(); + "Server restarted".Info(); + } + + // Stop the server + "Server stopping...".Info(); + server.Stop(); + "Server stopped, press any key to close".Info(); + Console.ReadKey(true); + } + } +} \ No newline at end of file diff --git a/GCRelayServer/RelayPacket.cs b/GCRelayServer/RelayPacket.cs new file mode 100644 index 0000000..8c257e6 --- /dev/null +++ b/GCRelayServer/RelayPacket.cs @@ -0,0 +1,57 @@ +using BinarySerialization; + +namespace GCRelayServer; + +public class RelayPacket +{ + [FieldOrder(0)] + [FieldEndianness(Endianness.Big)] + public ushort Magic; + + [FieldOrder(1)] + [FieldEndianness(Endianness.Big)] + public ushort RemainingSize; + + [FieldOrder(2)] + [FieldEndianness(Endianness.Big)] + public ushort RequestMainType; + + [FieldOrder(3)] + [FieldCount(6)] + public byte[] Unknown0 = Array.Empty(); + + [FieldOrder(4)] + [FieldEndianness(Endianness.Big)] + public ushort RequestSubType; + + [FieldOrder(5)] + [FieldEndianness(Endianness.Big)] + public ushort Unknown1; + + [FieldOrder(6)] + [FieldEndianness(Endianness.Big)] + public ushort DataSize; + + [FieldOrder(7)] + [FieldEndianness(Endianness.Big)] + public ushort Unknown2; + + [FieldOrder(8)] + [FieldEndianness(Endianness.Big)] + public uint MatchingId; + + [FieldOrder(9)] + [FieldEndianness(Endianness.Big)] + public uint EntryNo; + + [FieldOrder(10)] + [FieldEndianness(Endianness.Big)] + public uint MachineId; + + [FieldOrder(11)] + [FieldEndianness(Endianness.Big)] + public uint Unknown3; + + [FieldOrder(12)] + public byte[] Data = Array.Empty(); +} \ No newline at end of file diff --git a/GCRelayServer/RelayPacketTypes.cs b/GCRelayServer/RelayPacketTypes.cs new file mode 100644 index 0000000..71e7820 --- /dev/null +++ b/GCRelayServer/RelayPacketTypes.cs @@ -0,0 +1,14 @@ +namespace GCRelayServer; + +public static class RelayPacketTypes +{ + public const ushort HEART_BEAT = 0xB0; + + public const ushort HEART_BEAT_RESPONSE = 0xB1; + + public const ushort START_MATCHING = 0xA0; + + public const ushort START_MATCHING_RESPONSE = 0xA1; + + public const ushort REGISTER_MATCHING = 0xA6; +} \ No newline at end of file diff --git a/GCRelayServer/RelayServer.cs b/GCRelayServer/RelayServer.cs new file mode 100644 index 0000000..4efc36f --- /dev/null +++ b/GCRelayServer/RelayServer.cs @@ -0,0 +1,160 @@ +using System.Collections.Concurrent; +using System.Net; +using System.Net.Sockets; +using BinarySerialization; +using Swan.Logging; + +namespace GCRelayServer; + +public class RelayServer : NetCoreServer.UdpServer +{ + private ConcurrentDictionary matchingDictionary = new(); + public RelayServer(IPAddress address, int port) : base(address, port) {} + + + protected override void OnStarted() + { + // Start receive datagrams + ReceiveAsync(); + } + + protected override void OnReceived(EndPoint endpoint, byte[] buffer, long offset, long size) + { + var serializer = new BinarySerializer(); + var inputStream = new MemoryStream(buffer, (int)offset, (int)size, false); + var packet = serializer.Deserialize(inputStream); + + if (!IsValidPacket(packet)) + { + "Received malformed packet!".Warn(); + return; + } + + $"Received packet from {endpoint}, type is 0x{packet.RequestSubType:X2}".Info(); + + switch (packet.RequestSubType) + { + case RelayPacketTypes.HEART_BEAT: + { + for (var i = 0; i < 2; i++) + { + SendPacketSingle(endpoint, packet, RelayPacketTypes.HEART_BEAT_RESPONSE); + } + + break; + } + case RelayPacketTypes.START_MATCHING: + { + AddEntry(packet.MatchingId, endpoint); + for (var i = 0; i < 2; i++) + { + SendPacketSingle(endpoint, packet, RelayPacketTypes.START_MATCHING_RESPONSE); + } + break; + } + case RelayPacketTypes.REGISTER_MATCHING: + { + var entry = AddEntry(packet.MatchingId, endpoint); + SendPacketToOthers(entry.EndPoints, packet, endpoint); + break; + } + default: + { + var entry = GetEntry(packet.MatchingId); + if (entry is null) + { + break; + } + SendPacketToOthers(entry.EndPoints, packet, endpoint); + break; + } + } + ReceiveAsync(); + } + private void SendPacketSingle(EndPoint endpoint, RelayPacket packet, ushort subType) + { + packet.RequestSubType = subType; + var serializer = new BinarySerializer(); + var sendStream = new MemoryStream(1024); + serializer.Serialize(sendStream, packet); + + $"Send packet to {endpoint}, type is 0x{packet.RequestSubType:X2}".Info(); + SendAsync(endpoint, sendStream.GetBuffer(), 0, sendStream.Length); + } + + private void SendPacketToOthers(IEnumerable endPoints, RelayPacket packet, EndPoint owner) + { + if (owner is not IPEndPoint ipEndPoint) + { + "Endpoint is not IP endpoint! This should not happen!".Fatal(); + throw new ApplicationException(); + } + + foreach (var endPoint in endPoints.Where(endPoint => !ipEndPoint.Equals(endPoint))) + { + SendPacketSingle(endPoint, packet, packet.RequestSubType); + } + } + + protected override void OnError(SocketError error) + { + $"Relay server caught an error with code {error}".Error(); + } + + private static bool IsValidPacket(RelayPacket packet) + { + var totalSize = packet.Magic + packet.RemainingSize; + var actualSize = 36 + packet.Data.Length; + if (totalSize != actualSize) + { + return false; + } + + return packet.Data.Length == packet.DataSize; + } + + private DictEntry AddEntry(uint matchingId, EndPoint endPoint) + { + var now = DateTime.Now; + var entry = matchingDictionary.GetValueOrDefault(matchingId, new DictEntry()); + var shouldClear = false; + + if (entry.LastAccessTime <= now && now - entry.LastAccessTime >= TimeSpan.FromMinutes(10)) + { + $"Entry for matching id {matchingId:X8} has expired! Clients will be cleared!".Info(); + shouldClear = true; + } + if (entry.EndPoints.Count >= 4) + { + $"Entry for matching id {matchingId:X8} contains more than 4 clients! Clients will be cleared!".Warn(); + shouldClear = true; + } + entry.AddEndpoint(endPoint, shouldClear); + + entry.LastAccessTime = DateTime.Now; + matchingDictionary[matchingId] = entry; + + return entry; + } + + private DictEntry? GetEntry(uint matchingId) + { + var now = DateTime.Now; + + if (!matchingDictionary.ContainsKey(matchingId)) + { + $"Entry for matching id {matchingId:X8} does not exist!".Warn(); + return null; + } + + var entry = matchingDictionary[matchingId]; + if (entry.LastAccessTime <= now && now - entry.LastAccessTime >= TimeSpan.FromMinutes(10)) + { + $"Entry for matching id {matchingId:X8} has expired!".Warn(); + return null; + } + + entry.LastAccessTime = DateTime.Now; + return entry; + } +} \ No newline at end of file