From 55c4fc8ae8dd6f0b42d642d33246abea4e5d4dfa Mon Sep 17 00:00:00 2001 From: Battlefield Duck Date: Fri, 19 Jan 2024 04:00:05 +0800 Subject: [PATCH] Targets multiple dotnet versions --- .github/workflows/dotnet-package.yml | 9 +- OpenGSQ/BinaryReaderExtensions.cs | 5 +- OpenGSQ/OpenGSQ.csproj | 13 +- OpenGSQ/ProtocolSocket.cs | 85 +-- OpenGSQ/Protocols/ASE.cs | 48 +- OpenGSQ/Protocols/Battlefield.cs | 28 +- OpenGSQ/Protocols/Doom3.cs | 96 ++-- OpenGSQ/Protocols/EOS.cs | 32 +- OpenGSQ/Protocols/FiveM.cs | 10 +- OpenGSQ/Protocols/GameSpy1.cs | 140 ++--- OpenGSQ/Protocols/GameSpy2.cs | 65 +-- OpenGSQ/Protocols/GameSpy3.cs | 162 +++--- OpenGSQ/Protocols/GameSpy4.cs | 2 +- OpenGSQ/Protocols/KillingFloor.cs | 61 +-- OpenGSQ/Protocols/Minecraft.cs | 97 ++-- OpenGSQ/Protocols/Quake1.cs | 65 ++- OpenGSQ/Protocols/Quake2.cs | 17 +- OpenGSQ/Protocols/Quake3.cs | 84 ++- OpenGSQ/Protocols/RakNet.cs | 66 +-- OpenGSQ/Protocols/Samp.cs | 115 ++-- OpenGSQ/Protocols/Satisfactory.cs | 36 +- OpenGSQ/Protocols/Scum.cs | 85 +-- OpenGSQ/Protocols/Source.cs | 489 +++++++++--------- OpenGSQ/Protocols/TeamSpeak3.cs | 48 +- OpenGSQ/Protocols/Unreal2.cs | 159 +++--- OpenGSQ/Protocols/Vcmp.cs | 80 +-- OpenGSQTests/OpenGSQTests.csproj | 2 +- OpenGSQTests/Protocols/KillingFloorTests.cs | 4 +- .../KillingFloorTests/GetDetailsTest.json | 16 +- .../KillingFloorTests/GetPlayersTest.json | 108 ++-- .../KillingFloorTests/GetRulesTest.json | 33 +- 31 files changed, 1152 insertions(+), 1108 deletions(-) diff --git a/.github/workflows/dotnet-package.yml b/.github/workflows/dotnet-package.yml index 5893472..fc799a6 100644 --- a/.github/workflows/dotnet-package.yml +++ b/.github/workflows/dotnet-package.yml @@ -9,17 +9,18 @@ on: jobs: build: - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest strategy: matrix: - os: [windows-latest] + dotnet: [ '6', '7', '8' ] + name: Dotnet ${{ matrix.dotnet }} steps: - uses: actions/checkout@v4 - - name: Setup .NET 7 + - name: Setup dotnet uses: actions/setup-dotnet@v3 with: - dotnet-version: 7 + dotnet-version: ${{ matrix.dotnet }} - name: Install dependencies run: dotnet restore OpenGSQ - name: Build diff --git a/OpenGSQ/BinaryReaderExtensions.cs b/OpenGSQ/BinaryReaderExtensions.cs index 42ddc77..79de678 100644 --- a/OpenGSQ/BinaryReaderExtensions.cs +++ b/OpenGSQ/BinaryReaderExtensions.cs @@ -41,7 +41,10 @@ public static bool IsEnd(this BinaryReader br) /// An I/O error occurs. public static string ReadStringEx(this BinaryReader br, byte[] charBytes) { - charBytes ??= new byte[] { 0 }; + if (charBytes == null) + { + charBytes = new byte[] { 0 }; + } var bytes = new List(); byte streamByte; diff --git a/OpenGSQ/OpenGSQ.csproj b/OpenGSQ/OpenGSQ.csproj index 2228e8d..756c7e2 100644 --- a/OpenGSQ/OpenGSQ.csproj +++ b/OpenGSQ/OpenGSQ.csproj @@ -2,10 +2,10 @@ OpenGSQ - 2.0.0 - 2.0.0 + 2.0.1 + 2.0.1 OpenGSQ, BattlefieldDuck - netstandard2.1 + $(NetCoreAppCurrent);$(NetCoreAppPrevious);$(NetCoreAppMinimum);netstandard2.1;netstandard2.0;$(NetFrameworkMinimum) OpenGSQ OpenGSQ icon.png @@ -19,6 +19,13 @@ true + + net8.0 + net7.0 + net6.0 + net462 + + diff --git a/OpenGSQ/ProtocolSocket.cs b/OpenGSQ/ProtocolSocket.cs index 1461b03..c4e7e21 100644 --- a/OpenGSQ/ProtocolSocket.cs +++ b/OpenGSQ/ProtocolSocket.cs @@ -1,46 +1,53 @@ using System; using System.Net.Sockets; -using System.Threading; using System.Threading.Tasks; namespace OpenGSQ { /// - /// A class that extends the UdpClient class with a Communicate method. + /// A class for handling UDP client communication. /// - public static class UdpClientExtensions + public static class UdpClient { /// /// Sends data to a connected UdpClient and returns the response. /// - /// The UdpClient to communicate with. /// The protocol information. /// The data to send. /// A task that represents the asynchronous operation. The task result contains the response data. /// Thrown when the operation times out. - public static async Task CommunicateAsync(this UdpClient udpClient, ProtocolBase protocolBase, byte[] data) + public static async Task CommunicateAsync(ProtocolBase protocolBase, byte[] data) { - // Set the timeout - udpClient.Client.SendTimeout = protocolBase.Timeout; - udpClient.Client.ReceiveTimeout = protocolBase.Timeout; + using (var udpClient = new System.Net.Sockets.UdpClient()) + { + // Set the timeout + udpClient.Client.SendTimeout = protocolBase.Timeout; + udpClient.Client.ReceiveTimeout = protocolBase.Timeout; - // Connect to the server - udpClient.Connect(protocolBase.Host, protocolBase.Port); + // Connect to the server + udpClient.Connect(protocolBase.Host, protocolBase.Port); - // Send the data - await udpClient.SendAsync(data, data.Length); + // Send the data + await udpClient.SendAsync(data, data.Length); - // Receive the data - return await udpClient.ReceiveAsyncWithTimeout(); + // Receive the data + return await udpClient.ReceiveAsyncWithTimeout(); + } } + } + /// + /// A class that extends the UdpClient class with a Communicate method. + /// + public static class UdpClientExtensions + { /// /// Receives a UDP datagram asynchronously with a timeout. /// /// The UdpClient to receive from. /// A byte array containing the received datagram. /// Thrown when the operation times out. - public static async Task ReceiveAsyncWithTimeout(this UdpClient udpClient) + public static async Task ReceiveAsyncWithTimeout(this System.Net.Sockets.UdpClient udpClient) { Task receiveTask = udpClient.ReceiveAsync(); @@ -58,40 +65,48 @@ public static async Task ReceiveAsyncWithTimeout(this UdpClient udpClien } /// - /// A class that extends the TcpClient class with a Communicate method. + /// A class for handling TCP client communication. /// - public static class TcpClientExtensions + public static class TcpClient { /// /// Sends data to a connected TcpClient and returns the response. /// - /// The TcpClient to communicate with. /// The protocol information. /// The data to send. /// A task that represents the asynchronous operation. The task result contains the response data. /// Thrown when the operation times out. - public static async Task CommunicateAsync(this TcpClient tcpClient, ProtocolBase protocolBase, byte[] data) + public static async Task CommunicateAsync(ProtocolBase protocolBase, byte[] data) { - // Set the timeout - tcpClient.SendTimeout = protocolBase.Timeout; - tcpClient.ReceiveTimeout = protocolBase.Timeout; + using (var tcpClient = new System.Net.Sockets.TcpClient()) + { + // Set the timeout + tcpClient.SendTimeout = protocolBase.Timeout; + tcpClient.ReceiveTimeout = protocolBase.Timeout; - // Connect to the server - await tcpClient.ConnectAsync(protocolBase.Host, protocolBase.Port); + // Connect to the server + await tcpClient.ConnectAsync(protocolBase.Host, protocolBase.Port); - // Send the data - await tcpClient.SendAsync(data); + // Send the data + await tcpClient.SendAsync(data); - return await tcpClient.ReceiveAsync(); + return await tcpClient.ReceiveAsync(); + } } + } + /// + /// A class that extends the TcpClient class with a Communicate method. + /// + public static class TcpClientExtensions + { /// /// Sends data to a connected TcpClient. /// /// The TcpClient to send data to. /// The data to send. /// A task that represents the asynchronous operation. - public static async Task SendAsync(this TcpClient tcpClient, byte[] data) + public static async Task SendAsync(this System.Net.Sockets.TcpClient tcpClient, byte[] data) { // Get the stream object for writing and reading NetworkStream stream = tcpClient.GetStream(); @@ -106,20 +121,20 @@ public static async Task SendAsync(this TcpClient tcpClient, byte[] data) /// The TcpClient to receive data from. /// A task that represents the asynchronous operation. The task result contains the received data. /// Thrown when the operation times out. - public static async Task ReceiveAsync(this TcpClient tcpClient) + public static async Task ReceiveAsync(this System.Net.Sockets.TcpClient tcpClient) { - var cts = new CancellationTokenSource(tcpClient.Client.ReceiveTimeout); var buffer = new byte[tcpClient.Client.ReceiveBufferSize]; var segment = new ArraySegment(buffer); + var receiveTask = tcpClient.Client.ReceiveAsync(segment, SocketFlags.None); - try + if (await Task.WhenAny(receiveTask, Task.Delay(tcpClient.Client.ReceiveTimeout)) == receiveTask) { - var result = await tcpClient.Client.ReceiveAsync(segment, SocketFlags.None, cts.Token); - var receivedBytes = new byte[result]; - Array.Copy(buffer, receivedBytes, result); + // Task completed within timeout. + var receivedBytes = new byte[receiveTask.Result]; + Array.Copy(buffer, receivedBytes, receiveTask.Result); return receivedBytes; } - catch (OperationCanceledException) + else { // Task timed out. throw new TimeoutException("The operation has timed out."); diff --git a/OpenGSQ/Protocols/ASE.cs b/OpenGSQ/Protocols/ASE.cs index ba1f7bf..486380f 100644 --- a/OpenGSQ/Protocols/ASE.cs +++ b/OpenGSQ/Protocols/ASE.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net.Sockets; using System.Text; using System.Threading.Tasks; @@ -35,33 +34,34 @@ public ASE(string host, int port, int timeout = 5000) : base(host, port, timeout /// A dictionary containing the server status. public async Task> GetStatus() { - using var udpClient = new UdpClient(); - byte[] response = await udpClient.CommunicateAsync(this, _request); - byte[] header = response[..4]; + byte[] response = await UdpClient.CommunicateAsync(this, _request); - if (!header.SequenceEqual(_response)) + using (var br = new BinaryReader(new MemoryStream(response), Encoding.UTF8)) { - throw new InvalidPacketException($"Packet header mismatch. Received: {BitConverter.ToString(header)}. Expected: {BitConverter.ToString(_response)}."); - } - - using var br = new BinaryReader(new MemoryStream(response[4..]), Encoding.UTF8); + byte[] header = br.ReadBytes(4); - var result = new Dictionary - { - ["gamename"] = ReadString(br), - ["gameport"] = ReadString(br), - ["hostname"] = ReadString(br), - ["gametype"] = ReadString(br), - ["map"] = ReadString(br), - ["version"] = ReadString(br), - ["password"] = ReadString(br), - ["numplayers"] = ReadString(br), - ["maxplayers"] = ReadString(br), - ["rules"] = ParseRules(br), - ["players"] = ParsePlayers(br) - }; + if (!header.SequenceEqual(_response)) + { + throw new InvalidPacketException($"Packet header mismatch. Received: {BitConverter.ToString(header)}. Expected: {BitConverter.ToString(_response)}."); + } - return result; + var result = new Dictionary + { + ["gamename"] = ReadString(br), + ["gameport"] = ReadString(br), + ["hostname"] = ReadString(br), + ["gametype"] = ReadString(br), + ["map"] = ReadString(br), + ["version"] = ReadString(br), + ["password"] = ReadString(br), + ["numplayers"] = ReadString(br), + ["maxplayers"] = ReadString(br), + ["rules"] = ParseRules(br), + ["players"] = ParsePlayers(br) + }; + + return result; + } } private Dictionary ParseRules(BinaryReader br) diff --git a/OpenGSQ/Protocols/Battlefield.cs b/OpenGSQ/Protocols/Battlefield.cs index b71f510..20a1557 100644 --- a/OpenGSQ/Protocols/Battlefield.cs +++ b/OpenGSQ/Protocols/Battlefield.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net.Sockets; using System.Threading.Tasks; namespace OpenGSQ.Protocols @@ -141,22 +140,23 @@ public async Task>> GetPlayers() private async Task> GetData(byte[] request) { - using var tcpClient = new TcpClient(); - byte[] response = await tcpClient.CommunicateAsync(this, request); + byte[] response = await TcpClient.CommunicateAsync(this, request); - var br = new BinaryReader(new MemoryStream(response)); - br.ReadInt32(); // header - br.ReadInt32(); // packet length - var count = br.ReadInt32(); // string count - var data = new List(); - - for (var i = 0; i < count; i++) + using (var br = new BinaryReader(new MemoryStream(response))) { - br.ReadInt32(); // length of the string - data.Add(br.ReadStringEx()); - } + br.ReadInt32(); // header + br.ReadInt32(); // packet length + var count = br.ReadInt32(); // string count + var data = new List(); + + for (var i = 0; i < count; i++) + { + br.ReadInt32(); // length of the string + data.Add(br.ReadStringEx()); + } - return data.GetRange(1, data.Count - 1); + return data.GetRange(1, data.Count - 1); + } } } } \ No newline at end of file diff --git a/OpenGSQ/Protocols/Doom3.cs b/OpenGSQ/Protocols/Doom3.cs index 0b8f23f..dc3de45 100644 --- a/OpenGSQ/Protocols/Doom3.cs +++ b/OpenGSQ/Protocols/Doom3.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net.Sockets; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -41,70 +40,73 @@ public Doom3(string host, int port, int timeout = 5000) : base(host, port, timeo public async Task> GetInfo(bool stripColor = true) { byte[] request = new byte[] { 0xFF, 0xFF, 0x67, 0x65, 0x74, 0x49, 0x6E, 0x66, 0x6F, 0x00, 0x6F, 0x67, 0x73, 0x71, 0x00 }; - using var udpClient = new UdpClient(); - byte[] response = await udpClient.CommunicateAsync(this, request); + byte[] response = await UdpClient.CommunicateAsync(this, request); - using var br = new BinaryReader(new MemoryStream(response.Skip(2).ToArray())); - string header = br.ReadStringEx(); - - if (header != "infoResponse") + using (var br = new BinaryReader(new MemoryStream(response))) { - throw new InvalidPacketException($"Packet header mismatch. Received: {header}. Expected: infoResponse."); - } + br.ReadBytes(2); // Skip 2 bytes - // Read challenge - br.ReadBytes(4); + string header = br.ReadStringEx(); - if (!br.ReadBytes(4).SequenceEqual(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF })) - { - br.BaseStream.Position -= 4; - } + if (header != "infoResponse") + { + throw new InvalidPacketException($"Packet header mismatch. Received: {header}. Expected: infoResponse."); + } - Dictionary info = new Dictionary(); + // Read challenge + br.ReadBytes(4); - // Read protocol version - ushort minor = br.ReadUInt16(); - ushort major = br.ReadUInt16(); - info["version"] = $"{major}.{minor}"; + if (!br.ReadBytes(4).SequenceEqual(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF })) + { + br.BaseStream.Position -= 4; + } - // Read packet size - if (br.ReadInt32() != br.RemainingBytes()) - { - br.BaseStream.Position -= 4; - } + Dictionary info = new Dictionary(); - // Key / value pairs, delimited by an empty pair - while (br.BaseStream.Length > 0) - { - string key = br.ReadStringEx().Trim(); - string val = br.ReadStringEx().Trim(); + // Read protocol version + ushort minor = br.ReadUInt16(); + ushort major = br.ReadUInt16(); + info["version"] = $"{major}.{minor}"; - if (key == "" && val == "") + // Read packet size + if (br.ReadInt32() != br.RemainingBytes()) { - break; + br.BaseStream.Position -= 4; } - info[key] = stripColor ? StripColors(val) : val; - } + // Key / value pairs, delimited by an empty pair + while (br.BaseStream.Length > 0) + { + string key = br.ReadStringEx().Trim(); + string val = br.ReadStringEx().Trim(); - long streamPosition = br.BaseStream.Position; + if (key == "" && val == "") + { + break; + } - // Try parse the fields - foreach (string mod in _playerFields.Keys) - { - try - { - info["players"] = ParsePlayer(br, _playerFields[mod], stripColor); - break; + info[key] = stripColor ? StripColors(val) : val; } - catch (Exception) + + long streamPosition = br.BaseStream.Position; + + // Try parse the fields + foreach (string mod in _playerFields.Keys) { - info["players"] = new List(); - br.BaseStream.Position = streamPosition; + try + { + info["players"] = ParsePlayer(br, _playerFields[mod], stripColor); + break; + } + catch (Exception) + { + info["players"] = new List(); + br.BaseStream.Position = streamPosition; + } } - } - return info; + return info; + } } private static List> ParsePlayer(BinaryReader br, List fields, bool stripColor) diff --git a/OpenGSQ/Protocols/EOS.cs b/OpenGSQ/Protocols/EOS.cs index beabfda..cd1a72a 100644 --- a/OpenGSQ/Protocols/EOS.cs +++ b/OpenGSQ/Protocols/EOS.cs @@ -57,21 +57,22 @@ protected async Task GetAccessTokenAsync() string authInfo = $"{_clientId}:{_clientSecret}"; authInfo = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo)); - using HttpClient client = new HttpClient() + using (var client = new HttpClient() { BaseAddress = new Uri(url), DefaultRequestHeaders = { Authorization = new AuthenticationHeaderValue("Basic", authInfo) } - }; - - HttpResponseMessage response = await client.PostAsync(url, new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded")); - response.EnsureSuccessStatusCode(); + }) + { + HttpResponseMessage response = await client.PostAsync(url, new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded")); + response.EnsureSuccessStatusCode(); - var data = await response.Content.ReadFromJsonAsync>(); + var data = await response.Content.ReadFromJsonAsync>(); - return data["access_token"].ToString(); + return data["access_token"].ToString(); + } } /// @@ -93,23 +94,24 @@ public async Task GetMatchmakingAsync(Dictionary da string url = $"{_apiUrl}/matchmaking/v1/{_deploymentId}/filter"; - using HttpClient client = new HttpClient() + using (var client = new HttpClient() { BaseAddress = new Uri(url), DefaultRequestHeaders = { Authorization = new AuthenticationHeaderValue("Bearer", _accessToken) } - }; - - client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + }) + { + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - HttpResponseMessage response = await client.PostAsync(url, new StringContent(JsonSerializer.Serialize(data), Encoding.UTF8, "application/json")); - response.EnsureSuccessStatusCode(); + HttpResponseMessage response = await client.PostAsync(url, new StringContent(JsonSerializer.Serialize(data), Encoding.UTF8, "application/json")); + response.EnsureSuccessStatusCode(); - var responseData = await response.Content.ReadFromJsonAsync(); + var responseData = await response.Content.ReadFromJsonAsync(); - return responseData; + return responseData; + } } /// diff --git a/OpenGSQ/Protocols/FiveM.cs b/OpenGSQ/Protocols/FiveM.cs index d3c0bf3..54dafc9 100644 --- a/OpenGSQ/Protocols/FiveM.cs +++ b/OpenGSQ/Protocols/FiveM.cs @@ -28,11 +28,13 @@ private async Task GetAsync(string filename) { string url = $"http://{Host}:{Port}/{filename}.json?v={new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds()}"; - using HttpClient client = new HttpClient(); - HttpResponseMessage response = await client.GetAsync(url); - response.EnsureSuccessStatusCode(); + using (var client = new HttpClient()) + { + HttpResponseMessage response = await client.GetAsync(url); + response.EnsureSuccessStatusCode(); - return await response.Content.ReadFromJsonAsync(); + return await response.Content.ReadFromJsonAsync(); + } } /// diff --git a/OpenGSQ/Protocols/GameSpy1.cs b/OpenGSQ/Protocols/GameSpy1.cs index 0d6d64a..57fb107 100644 --- a/OpenGSQ/Protocols/GameSpy1.cs +++ b/OpenGSQ/Protocols/GameSpy1.cs @@ -80,62 +80,64 @@ public Task>> GetPlayers(bool XServerQuery = tru /// Thrown when a socket error occurs. public async Task GetStatus(bool XServerQuery = true) { - var responseData = await ConnectAndSend("\\status\\" + (XServerQuery ? "xserverquery" : string.Empty)); - using var br = new BinaryReader(new MemoryStream(responseData), Encoding.UTF8); + byte[] response = await ConnectAndSend("\\status\\" + (XServerQuery ? "xserverquery" : string.Empty)); - var status = new Status + using (var br = new BinaryReader(new MemoryStream(response))) { - KeyValues = new Dictionary() - }; - - long position = 0; - - // Read key until "player_#" or "Player_#" - while (br.BaseStream.Position < br.BaseStream.Length - && br.TryReadStringEx(out var key, _delimiter) - && !key.ToLower().StartsWith("player_")) - { - // Save key and value - status.KeyValues[key] = br.ReadStringEx(_delimiter); + var status = new Status + { + KeyValues = new Dictionary() + }; - // Save the position after read the value - position = br.BaseStream.Position; - } + long position = 0; - // Reset the position - br.BaseStream.Position = position; + // Read key until "player_#" or "Player_#" + while (br.BaseStream.Position < br.BaseStream.Length + && br.TryReadStringEx(out var key, _delimiter) + && !key.ToLower().StartsWith("player_")) + { + // Save key and value + status.KeyValues[key] = br.ReadStringEx(_delimiter); - // Save players - status.Players = ParseObject(br); + // Save the position after read the value + position = br.BaseStream.Position; + } - if (status.IsXServerQuery) - { - // Save teams if it is XServerQuery response - status.Teams = new List>(); + // Reset the position + br.BaseStream.Position = position; - var teams = status.KeyValues.Where(x => x.Key.Contains('_')); + // Save players + status.Players = ParseObject(br); - foreach (KeyValuePair keyValue in teams) + if (status.IsXServerQuery) { - // Split key and index - string[] subs = keyValue.Key.Split(new char[] { '_' }, 2); - (string key, int index) = (subs[0], int.Parse(subs[1])); + // Save teams if it is XServerQuery response + status.Teams = new List>(); - // Create a new team if not exists - if (status.Teams.Count <= index) + var teams = status.KeyValues.Where(x => x.Key.Contains('_')); + + foreach (KeyValuePair keyValue in teams) { - status.Teams.Add(new Dictionary()); - } + // Split key and index + string[] subs = keyValue.Key.Split(new char[] { '_' }, 2); + (string key, int index) = (subs[0], int.Parse(subs[1])); - // Set the value - status.Teams[index][key] = keyValue.Value; + // Create a new team if not exists + if (status.Teams.Count <= index) + { + status.Teams.Add(new Dictionary()); + } - // Remove the key - status.KeyValues.Remove(keyValue.Key); + // Set the value + status.Teams[index][key] = keyValue.Value; + + // Remove the key + status.KeyValues.Remove(keyValue.Key); + } } - } - return status; + return status; + } } /// @@ -197,57 +199,63 @@ private List> ParseObject(BinaryReader br) private async Task> SendAndParseKeyValue(string request) { - var responseData = await ConnectAndSend(request); - using var br = new BinaryReader(new MemoryStream(responseData), Encoding.UTF8); + byte[] response = await ConnectAndSend(request); - return ParseKeyValue(br); + using (var br = new BinaryReader(new MemoryStream(response))) + { + return ParseKeyValue(br); + } } private async Task>> SendAndParseObject(string request) { - var responseData = await ConnectAndSend(request); - using var br = new BinaryReader(new MemoryStream(responseData), Encoding.UTF8); + byte[] response = await ConnectAndSend(request); - return ParseObject(br); + using (var br = new BinaryReader(new MemoryStream(response))) + { + return ParseObject(br); + } } private async Task ConnectAndSend(string request) { - using var udpClient = new UdpClient(); + using (var udpClient = new System.Net.Sockets.UdpClient()) + { + udpClient.Client.SendTimeout = Timeout; + udpClient.Client.ReceiveTimeout = Timeout; - // Connect to remote host - udpClient.Connect(Host, Port); - udpClient.Client.SendTimeout = Timeout; - udpClient.Client.ReceiveTimeout = Timeout; + // Connect to remote host + udpClient.Connect(Host, Port); - // Send Request - var requestData = Encoding.ASCII.GetBytes(request); - udpClient.Send(requestData, requestData.Length); + // Send Request + byte[] datagram = Encoding.ASCII.GetBytes(request); + await udpClient.SendAsync(datagram, datagram.Length); - // Server response - var responseData = await Receive(udpClient); + // Server response + byte[] response = await Receive(udpClient); - // Remove - return responseData; + // Remove + return response; + } } - private async Task Receive(UdpClient udpClient) + private async Task Receive(System.Net.Sockets.UdpClient udpClient) { int totalPackets = -1, packetId; var payloads = new SortedDictionary(); do { - var responseData = await udpClient.ReceiveAsyncWithTimeout(); + byte[] response = await udpClient.ReceiveAsyncWithTimeout(); // Try read "queryid" value, if it is the last packet, it cannot read the "queryid" value directly - if (!ReadStringReverse(responseData, responseData.Length, out var endIndex, out var queryId)) + if (!ReadStringReverse(response, response.Length, out var endIndex, out var queryId)) { // Read "final" string - ReadStringReverse(responseData, endIndex, out endIndex, out _); + ReadStringReverse(response, endIndex, out endIndex, out _); // Read "queryid" value - ReadStringReverse(responseData, endIndex, out endIndex, out queryId); + ReadStringReverse(response, endIndex, out endIndex, out queryId); // Save total packet totalPackets = int.Parse(queryId.Split('.')[1]); @@ -257,10 +265,10 @@ private async Task Receive(UdpClient udpClient) packetId = int.Parse(queryId.Split('.')[1]); // Read "queryid" string - ReadStringReverse(responseData, endIndex, out endIndex, out _); + ReadStringReverse(response, endIndex, out endIndex, out _); // Save the payload - byte[] payload = responseData.Take(endIndex).Skip(1).Concat(new byte[] { _delimiter }).ToArray(); + byte[] payload = response.Take(endIndex).Skip(1).Concat(new byte[] { _delimiter }).ToArray(); payloads.Add(packetId, payload); } while (totalPackets == -1 || payloads.Count < totalPackets); diff --git a/OpenGSQ/Protocols/GameSpy2.cs b/OpenGSQ/Protocols/GameSpy2.cs index e210e1e..5b7d121 100644 --- a/OpenGSQ/Protocols/GameSpy2.cs +++ b/OpenGSQ/Protocols/GameSpy2.cs @@ -3,7 +3,6 @@ using System.IO; using System.Linq; using System.Net.Sockets; -using System.Text; using System.Threading.Tasks; using OpenGSQ.Responses.GameSpy2; @@ -31,46 +30,50 @@ public GameSpy2(string host, int port, int timeout = 5000) : base(host, port, ti /// /// Retrieves information about the server including Info, Players, and Teams. /// - /// The type of information to request. + /// The type of information to request. /// A Status object containing the requested information. /// Thrown when a socket error occurs. - public async Task GetStatus(Request request = Request.Info | Request.Players | Request.Teams) + public async Task GetStatus(RequestHeader requestHeader = RequestHeader.Info | RequestHeader.Players | RequestHeader.Teams) { - using var udpClient = new UdpClient(); - var requestData = new byte[] { 0xFE, 0xFD, 0x00, 0x04, 0x05, 0x06, 0x07 }.Concat(GetRequestBytes(request)).ToArray(); - var responseData = await udpClient.CommunicateAsync(this, requestData); + var request = new byte[] { 0xFE, 0xFD, 0x00, 0x04, 0x05, 0x06, 0x07 }.Concat(GetRequestBytes(requestHeader)).ToArray(); + var response = await UdpClient.CommunicateAsync(this, request); - using var br = new BinaryReader(new MemoryStream(responseData.Skip(5).ToArray()), Encoding.UTF8); - var status = new Status(); - - // Save Response Info - if (request.HasFlag(Request.Info)) + using (var br = new BinaryReader(new MemoryStream(response))) { - status.Info = GetInfo(br); - } + // Skip first 5 bytes + br.ReadBytes(5); - // Save Response Players - if (request.HasFlag(Request.Players)) - { - status.Players = GetPlayers(br); - } + var status = new Status(); - // Save Response Teams - if (request.HasFlag(Request.Teams)) - { - status.Teams = GetTeams(br); - } + // Save Response Info + if (requestHeader.HasFlag(RequestHeader.Info)) + { + status.Info = GetInfo(br); + } - return status; - } + // Save Response Players + if (requestHeader.HasFlag(RequestHeader.Players)) + { + status.Players = GetPlayers(br); + } + + // Save Response Teams + if (requestHeader.HasFlag(RequestHeader.Teams)) + { + status.Teams = GetTeams(br); + } + return status; + } + } - private byte[] GetRequestBytes(Request request) + private byte[] GetRequestBytes(RequestHeader request) { - return new byte[] { - (byte)(request.HasFlag(Request.Info) ? 0xFF : 0x00), - (byte)(request.HasFlag(Request.Players) ? 0xFF : 0x00), - (byte)(request.HasFlag(Request.Teams) ? 0xFF : 0x00), + return new byte[] + { + (byte)(request.HasFlag(RequestHeader.Info) ? 0xFF : 0x00), + (byte)(request.HasFlag(RequestHeader.Players) ? 0xFF : 0x00), + (byte)(request.HasFlag(RequestHeader.Teams) ? 0xFF : 0x00), }; } @@ -155,7 +158,7 @@ private List> GetTeams(BinaryReader br) /// Represents the types of requests that can be sent. /// [Flags] - public enum Request : short + public enum RequestHeader : short { /// /// A request for information. diff --git a/OpenGSQ/Protocols/GameSpy3.cs b/OpenGSQ/Protocols/GameSpy3.cs index a5d0298..00d3061 100644 --- a/OpenGSQ/Protocols/GameSpy3.cs +++ b/OpenGSQ/Protocols/GameSpy3.cs @@ -20,7 +20,7 @@ public class GameSpy3 : ProtocolBase /// /// A boolean indicating whether to use the challenge method. /// - protected bool _Challenge; + protected bool Challenge; /// /// Initializes a new instance of the GameSpy3 class. @@ -40,124 +40,130 @@ public GameSpy3(string host, int port, int timeout = 5000) : base(host, port, ti /// Thrown when a socket error occurs. public async Task GetStatus() { - using var udpClient = new UdpClient(); - var responseData = await ConnectAndSendPackets(udpClient); - using var br = new BinaryReader(new MemoryStream(responseData), Encoding.UTF8); + byte[] response = await ConnectAndSendPackets(); - return new Status + using (var br = new BinaryReader(new MemoryStream(response))) { - // Save Status Info - Info = GetInfo(br), + return new Status + { + // Save Status Info + Info = GetInfo(br), - // Save Status Players - Players = GetPlayers(br), + // Save Status Players + Players = GetPlayers(br), - // Save Status Teams - Teams = GetTeams(br) - }; + // Save Status Teams + Teams = GetTeams(br) + }; + } } - private async Task ConnectAndSendPackets(UdpClient udpClient) + private async Task ConnectAndSendPackets() { - // Connect to remote host - udpClient.Connect(Host, Port); - udpClient.Client.SendTimeout = Timeout; - udpClient.Client.ReceiveTimeout = Timeout; - - // Packet 1: Initial request - byte[] responseData, challenge = new byte[] { }, requestData = new byte[] { 0xFE, 0xFD, 0x09, 0x04, 0x05, 0x06, 0x07 }; - - if (_Challenge) + using (var udpClient = new System.Net.Sockets.UdpClient()) { - await udpClient.SendAsync(requestData, requestData.Length); + // Connect to remote host + udpClient.Connect(Host, Port); + udpClient.Client.SendTimeout = Timeout; + udpClient.Client.ReceiveTimeout = Timeout; - // Packet 2: First response - responseData = await udpClient.ReceiveAsyncWithTimeout(); + // Packet 1: Initial request + byte[] responseData, challenge = new byte[] { }, requestData = new byte[] { 0xFE, 0xFD, 0x09, 0x04, 0x05, 0x06, 0x07 }; - // Get challenge - if (int.TryParse(Encoding.ASCII.GetString(responseData.Skip(5).ToArray()).Trim(), out int result) && result != 0) + if (Challenge) { - challenge = BitConverter.GetBytes(result); + await udpClient.SendAsync(requestData, requestData.Length); + + // Packet 2: First response + responseData = await udpClient.ReceiveAsyncWithTimeout(); - if (BitConverter.IsLittleEndian) + // Get challenge + if (int.TryParse(Encoding.ASCII.GetString(responseData.Skip(5).ToArray()).Trim(), out int result) && result != 0) { - Array.Reverse(challenge); + challenge = BitConverter.GetBytes(result); + + if (BitConverter.IsLittleEndian) + { + Array.Reverse(challenge); + } } } - } - // Packet 3: Second request - requestData[2] = 0x00; - requestData = requestData.Concat(challenge).Concat(new byte[] { 0xFF, 0xFF, 0xFF, 0x01 }).ToArray(); - udpClient.Send(requestData, requestData.Length); + // Packet 3: Second request + requestData[2] = 0x00; + requestData = requestData.Concat(challenge).Concat(new byte[] { 0xFF, 0xFF, 0xFF, 0x01 }).ToArray(); + udpClient.Send(requestData, requestData.Length); - // Packet 4: Server response - responseData = await Receive(udpClient); + // Packet 4: Server response + responseData = await Receive(udpClient); - return responseData; + return responseData; + } } - private async Task Receive(UdpClient udpClient) + private async Task Receive(System.Net.Sockets.UdpClient udpClient) { int totalPackets = -1; var payloads = new SortedDictionary(); do { - var responseData = await udpClient.ReceiveAsyncWithTimeout(); + byte[] response = await udpClient.ReceiveAsyncWithTimeout(); - using var br = new BinaryReader(new MemoryStream(responseData), Encoding.UTF8); - var header = br.ReadByte(); - - if (header != 0) + using (var br = new BinaryReader(new MemoryStream(response))) { - throw new Exception($"Packet header mismatch. Received: {header}. Expected: 0."); - } + var header = br.ReadByte(); - // Skip the timestamp and splitnum - br.ReadBytes(13); - - // The 'numPackets' byte - var numPackets = br.ReadByte(); + if (header != 0) + { + throw new Exception($"Packet header mismatch. Received: {header}. Expected: 0."); + } - // The low 7 bits are the packet index (starting at zero) - var number = numPackets & 0x7F; + // Skip the timestamp and splitnum + br.ReadBytes(13); - // The high bit is whether or not this is the last packet - var isLastPacket = numPackets >> 7 == 1; + // The 'numPackets' byte + var numPackets = br.ReadByte(); - // Save totalPackets as packet number + 1 - if (isLastPacket) - { - totalPackets = number + 1; - } + // The low 7 bits are the packet index (starting at zero) + var number = numPackets & 0x7F; - // The object id. Example: \x01 - var objectId = br.ReadByte(); + // The high bit is whether or not this is the last packet + var isLastPacket = numPackets >> 7 == 1; - // The object header - byte[] objectHeader = new byte[] { }; + // Save totalPackets as packet number + 1 + if (isLastPacket) + { + totalPackets = number + 1; + } - if (objectId >= 1) - { - // The object name. Example: "player_" - string objectName = br.ReadStringEx(); + // The object id. Example: \x01 + var objectId = br.ReadByte(); - // The object items appear count - int count = br.ReadByte(); + // The object header + byte[] objectHeader = new byte[] { }; - // If the object item doesn't appear before, set the header back - if (count == 0) + if (objectId >= 1) { - // Set the header. Example: \x00\x01player_\x00\x00 - objectHeader = new byte[] { 0x00, objectId }.Concat(Encoding.UTF8.GetBytes(objectName)).Concat(new byte[] { 0x00, 0x00 }).ToArray(); + // The object name. Example: "player_" + string objectName = br.ReadStringEx(); + + // The object items appear count + int count = br.ReadByte(); + + // If the object item doesn't appear before, set the header back + if (count == 0) + { + // Set the header. Example: \x00\x01player_\x00\x00 + objectHeader = new byte[] { 0x00, objectId }.Concat(Encoding.UTF8.GetBytes(objectName)).Concat(new byte[] { 0x00, 0x00 }).ToArray(); + } } - } - // Save the payload - byte[] payload = objectHeader.Concat(responseData.Skip((int)br.BaseStream.Position)).ToArray(); + // Save the payload + byte[] payload = objectHeader.Concat(response.Skip((int)br.BaseStream.Position)).ToArray(); - payloads.Add(number, TrimPayload(payload)); + payloads.Add(number, TrimPayload(payload)); + } } while (totalPackets == -1 || payloads.Count < totalPackets); // Combine the payloads diff --git a/OpenGSQ/Protocols/GameSpy4.cs b/OpenGSQ/Protocols/GameSpy4.cs index 446fac1..65db295 100644 --- a/OpenGSQ/Protocols/GameSpy4.cs +++ b/OpenGSQ/Protocols/GameSpy4.cs @@ -16,7 +16,7 @@ public class GameSpy4 : GameSpy3 /// The timeout for the connection in milliseconds. public GameSpy4(string host, int port, int timeout = 5000) : base(host, port, timeout) { - _Challenge = true; + Challenge = true; } } } diff --git a/OpenGSQ/Protocols/KillingFloor.cs b/OpenGSQ/Protocols/KillingFloor.cs index 8c27b2a..f49d9e4 100644 --- a/OpenGSQ/Protocols/KillingFloor.cs +++ b/OpenGSQ/Protocols/KillingFloor.cs @@ -1,8 +1,6 @@ using System.IO; -using System.Linq; -using System.Net.Sockets; using System.Threading.Tasks; -using Status = OpenGSQ.Responses.KillingFloor.Status; +using OpenGSQ.Responses.KillingFloor; namespace OpenGSQ.Protocols { @@ -28,37 +26,40 @@ public KillingFloor(string host, int port, int timeout = 5000) : base(host, port /// Thrown when the packet header does not match the expected value. public new async Task GetDetails() { - using var udpClient = new UdpClient(); - byte[] response = await udpClient.CommunicateAsync(this, new byte[] { 0x79, 0x00, 0x00, 0x00, _DETAILS }); + byte[] response = await UdpClient.CommunicateAsync(this, new byte[] { 0x79, 0x00, 0x00, 0x00, _DETAILS }); - // Remove the first 4 bytes \x80\x00\x00\x00 - BinaryReader br = new BinaryReader(new MemoryStream(response.Skip(4).ToArray())); - byte header = br.ReadByte(); - - if (header != _DETAILS) + using (var br = new BinaryReader(new MemoryStream(response))) { - throw new InvalidPacketException($"Packet header mismatch. Received: {header}. Expected: {_DETAILS}."); - } + // Remove the first 4 bytes \x80\x00\x00\x00 + br.ReadBytes(4); - var details = new Status - { - ServerId = br.ReadInt32(), - ServerIP = br.ReadString(), - GamePort = br.ReadInt32(), - QueryPort = br.ReadInt32(), - ServerName = ReadString(br), - MapName = ReadString(br), - GameType = ReadString(br), - NumPlayers = br.ReadInt32(), - MaxPlayers = br.ReadInt32(), - WaveCurrent = br.ReadInt32(), - WaveTotal = br.ReadInt32(), - Ping = br.ReadInt32(), - Flags = br.ReadInt32(), - Skill = ReadString(br) - }; + byte header = br.ReadByte(); - return details; + if (header != _DETAILS) + { + throw new InvalidPacketException($"Packet header mismatch. Received: {header}. Expected: {_DETAILS}."); + } + + var details = new Status + { + ServerId = br.ReadInt32(), + ServerIP = br.ReadString(), + GamePort = br.ReadInt32(), + QueryPort = br.ReadInt32(), + ServerName = ReadString(br), + MapName = ReadString(br), + GameType = ReadString(br), + NumPlayers = br.ReadInt32(), + MaxPlayers = br.ReadInt32(), + WaveCurrent = br.ReadInt32(), + WaveTotal = br.ReadInt32(), + Ping = br.ReadInt32(), + Flags = br.ReadInt32(), + Skill = ReadString(br) + }; + + return details; + } } } } \ No newline at end of file diff --git a/OpenGSQ/Protocols/Minecraft.cs b/OpenGSQ/Protocols/Minecraft.cs index b449220..0fd90ab 100644 --- a/OpenGSQ/Protocols/Minecraft.cs +++ b/OpenGSQ/Protocols/Minecraft.cs @@ -6,7 +6,6 @@ using System.Linq; using System.IO; using System.Text.RegularExpressions; -using System.Net.Sockets; namespace OpenGSQ.Protocols { @@ -41,31 +40,38 @@ public async Task> GetStatus(int version = 47) var request = new byte[] { 0x00 }.Concat(protocol).Concat(PackVarint(address.Length)).Concat(address).Concat(BitConverter.GetBytes((short)Port)).Concat(new byte[] { 0x01 }).ToArray(); request = PackVarint(request.Length).Concat(request).Concat(new byte[] { 0x01, 0x00 }).ToArray(); - using var tcpClient = new TcpClient(); - tcpClient.ReceiveTimeout = Timeout; - await tcpClient.ConnectAsync(Host, Port); - await tcpClient.SendAsync(request); - - var response = await tcpClient.ReceiveAsync(); - using var br1 = new BinaryReader(new MemoryStream(response)); - var length = UnpackVarint(br1); - - // Keep receiving until reach packet length - while (response.Length < length) + using (var tcpClient = new System.Net.Sockets.TcpClient()) { - response = response.Concat(await tcpClient.ReceiveAsync()).ToArray(); + tcpClient.ReceiveTimeout = Timeout; + await tcpClient.ConnectAsync(Host, Port); + await tcpClient.SendAsync(request); + + byte[] response = await tcpClient.ReceiveAsync(); + + using (var br = new BinaryReader(new MemoryStream(response))) + { + var length = UnpackVarint(br); + + // Keep receiving until reach packet length + while (response.Length < length) + { + response = response.Concat(await tcpClient.ReceiveAsync()).ToArray(); + } + } + + // Read full response + using (var br = new BinaryReader(new MemoryStream(response))) + { + UnpackVarint(br); // packet length + UnpackVarint(br); // packet id + var count = UnpackVarint(br); // json length + + // The packet may respond with two json objects, so we need to get the json length exactly + var data = JsonSerializer.Deserialize>(Encoding.UTF8.GetString(br.ReadBytes(count))); + + return data; + } } - - // Read full response - using var br = new BinaryReader(new MemoryStream(response)); - UnpackVarint(br); // packet length - UnpackVarint(br); // packet id - var count = UnpackVarint(br); // json length - - // The packet may respond with two json objects, so we need to get the json length exactly - var data = JsonSerializer.Deserialize>(Encoding.UTF8.GetString(br.ReadBytes(count))); - - return data; } /// @@ -74,30 +80,31 @@ public async Task> GetStatus(int version = 47) /// A task that represents the asynchronous operation. The task result contains the server status. public async Task> GetStatusPre17() { - using var tcpClient = new TcpClient(); - var response = await tcpClient.CommunicateAsync(this, new byte[] { 0xFE, 0x01 }); + byte[] response = await TcpClient.CommunicateAsync(this, new byte[] { 0xFE, 0x01 }); - using var br = new BinaryReader(new MemoryStream(response)); - var header = br.ReadByte(); - - if (header != 0xFF) + using (var br = new BinaryReader(new MemoryStream(response))) { - throw new InvalidPacketException($"Packet header mismatch. Received: {header}. Expected: {0xFF}."); + var header = br.ReadByte(); + + if (header != 0xFF) + { + throw new InvalidPacketException($"Packet header mismatch. Received: {header}. Expected: {0xFF}."); + } + + br.ReadBytes(2); // length of the following string + var items = Encoding.BigEndianUnicode.GetString(br.ReadBytes(response.Length - 2)).Split('\0'); + + var result = new Dictionary + { + ["protocol"] = items[1], + ["version"] = items[2], + ["motd"] = items[3], + ["numplayers"] = int.Parse(items[4]), + ["maxplayers"] = int.Parse(items[5]) + }; + + return result; } - - br.ReadBytes(2); // length of the following string - var items = Encoding.BigEndianUnicode.GetString(br.ReadBytes(response.Length - 2)).Split('\0'); - - var result = new Dictionary - { - ["protocol"] = items[1], - ["version"] = items[2], - ["motd"] = items[3], - ["numplayers"] = int.Parse(items[4]), - ["maxplayers"] = int.Parse(items[5]) - }; - - return result; } /// diff --git a/OpenGSQ/Protocols/Quake1.cs b/OpenGSQ/Protocols/Quake1.cs index 71b158e..bfc7044 100644 --- a/OpenGSQ/Protocols/Quake1.cs +++ b/OpenGSQ/Protocols/Quake1.cs @@ -15,28 +15,28 @@ namespace OpenGSQ.Protocols /// public class Quake1 : ProtocolBase { + /// + public override string FullName => "Quake1 Query Protocol"; + /// /// The ASCII value of the backslash character used as a delimiter. /// - protected byte _Delimiter1 = Encoding.ASCII.GetBytes("\\")[0]; + protected byte Delimiter1 = Encoding.ASCII.GetBytes("\\")[0]; /// /// The ASCII value of the newline character used as a delimiter. /// - protected byte _Delimiter2 = Encoding.ASCII.GetBytes("\n")[0]; + protected byte Delimiter2 = Encoding.ASCII.GetBytes("\n")[0]; /// /// The header of the request. /// - protected string _RequestHeader; + protected string RequestHeader; /// /// The header of the response. /// - protected string _ResponseHeader; - - /// - public override string FullName => "Quake1 Query Protocol"; + protected string ResponseHeader; /// /// Initializes a new instance of the Quake1 class. @@ -46,8 +46,8 @@ public class Quake1 : ProtocolBase /// The timeout for the connection in milliseconds. public Quake1(string host, int port, int timeout = 5000) : base(host, port, timeout) { - _RequestHeader = "status"; - _ResponseHeader = "n"; + RequestHeader = "status"; + ResponseHeader = "n"; } /// @@ -57,13 +57,14 @@ public Quake1(string host, int port, int timeout = 5000) : base(host, port, time /// Thrown when a socket error occurs. public async Task GetStatus() { - using var br = await GetResponseBinaryReader(); - - return new Status + using (var br = await GetResponseBinaryReader()) { - Info = ParseInfo(br), - Players = ParsePlayers(br), - }; + return new Status + { + Info = ParseInfo(br), + Players = ParsePlayers(br), + }; + } } /// @@ -73,14 +74,14 @@ public async Task GetStatus() /// Thrown when the packet header does not match the expected header. protected async Task GetResponseBinaryReader() { - var responseData = await ConnectAndSend(_RequestHeader); + byte[] response = await ConnectAndSend(RequestHeader); - var br = new BinaryReader(new MemoryStream(responseData), Encoding.UTF8); - var header = br.ReadStringEx(_Delimiter1); + var br = new BinaryReader(new MemoryStream(response)); + var header = br.ReadStringEx(Delimiter1); - if (header != _ResponseHeader) + if (header != ResponseHeader) { - throw new Exception($"Packet header mismatch. Received: {header}. Expected: {_ResponseHeader}."); + throw new Exception($"Packet header mismatch. Received: {header}. Expected: {ResponseHeader}."); } return br; @@ -96,13 +97,13 @@ protected Dictionary ParseInfo(BinaryReader br) var info = new Dictionary(); // Read all key values until meet \n - while (br.TryReadStringEx(out var key, _Delimiter1)) + while (br.TryReadStringEx(out var key, Delimiter1)) { - info[key] = br.ReadStringEx(new byte[] { _Delimiter1, _Delimiter2 }); + info[key] = br.ReadStringEx(new byte[] { Delimiter1, Delimiter2 }); br.BaseStream.Position--; - if (br.ReadByte() == _Delimiter2) + if (br.ReadByte() == Delimiter2) { break; } @@ -151,7 +152,7 @@ protected List GetPlayerMatchCollections(BinaryReader br) var regex = new Regex("\"(\\\"|[^\"])*?\"|[^\\s]+"); // Read all players - while (br.BaseStream.Position < br.BaseStream.Length && br.TryReadStringEx(out var playerInfo, _Delimiter2)) + while (br.BaseStream.Position < br.BaseStream.Length && br.TryReadStringEx(out var playerInfo, Delimiter2)) { matchCollections.Add(regex.Matches(playerInfo)); } @@ -166,29 +167,27 @@ protected List GetPlayerMatchCollections(BinaryReader br) /// The response data received from the server. protected async Task ConnectAndSend(string request) { - using var udpClient = new UdpClient(); - var header = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }; - // Send Request + var header = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }; var requestData = new byte[0].Concat(header).Concat(Encoding.ASCII.GetBytes(request)).Concat(new byte[] { 0x00 }).ToArray(); // Server response - var responseData = await udpClient.CommunicateAsync(this, requestData); + byte[] response = await UdpClient.CommunicateAsync(this, requestData); // Remove the last 0x00 if exists (Only if Quake1) - if (responseData[responseData.Length - 1] == 0) + if (response[response.Length - 1] == 0) { - responseData = responseData.Take(responseData.Length - 1).ToArray(); + response = response.Take(response.Length - 1).ToArray(); } // Add \n at the last of responseData if not exists - if (responseData[responseData.Length - 1] != _Delimiter2) + if (response[response.Length - 1] != Delimiter2) { - responseData = responseData.Concat(new byte[] { _Delimiter2 }).ToArray(); + response = response.Concat(new byte[] { Delimiter2 }).ToArray(); } // Remove the first four 0xFF - return responseData.Skip(header.Length).ToArray(); + return response.Skip(header.Length).ToArray(); } } } diff --git a/OpenGSQ/Protocols/Quake2.cs b/OpenGSQ/Protocols/Quake2.cs index 4253871..31ecf2c 100644 --- a/OpenGSQ/Protocols/Quake2.cs +++ b/OpenGSQ/Protocols/Quake2.cs @@ -22,8 +22,8 @@ public class Quake2 : Quake1 /// The timeout for the connection in milliseconds. public Quake2(string host, int port, int timeout = 5000) : base(host, port, timeout) { - _RequestHeader = "status"; - _ResponseHeader = "print\n"; + RequestHeader = "status"; + ResponseHeader = "print\n"; } /// @@ -33,13 +33,14 @@ public Quake2(string host, int port, int timeout = 5000) : base(host, port, time /// Thrown when a socket error occurs. public new async Task GetStatus() { - using var br = await GetResponseBinaryReader(); - - return new Status + using (var br = await GetResponseBinaryReader()) { - Info = ParseInfo(br), - Players = ParsePlayers(br), - }; + return new Status + { + Info = ParseInfo(br), + Players = ParsePlayers(br), + }; + } } /// diff --git a/OpenGSQ/Protocols/Quake3.cs b/OpenGSQ/Protocols/Quake3.cs index 65fb26a..db2e2df 100644 --- a/OpenGSQ/Protocols/Quake3.cs +++ b/OpenGSQ/Protocols/Quake3.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Net.Sockets; -using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using OpenGSQ.Responses.Quake2; @@ -25,8 +24,8 @@ public class Quake3 : Quake2 /// The timeout for the connection in milliseconds. public Quake3(string host, int port, int timeout = 5000) : base(host, port, timeout) { - _RequestHeader = "getstatus"; - _ResponseHeader = "statusResponse\n"; + RequestHeader = "getstatus"; + ResponseHeader = "statusResponse\n"; } /// @@ -37,30 +36,30 @@ public Quake3(string host, int port, int timeout = 5000) : base(host, port, time /// Thrown when a socket error occurs. public async Task> GetInfo(bool stripColor = true) { - var responseData = await ConnectAndSend("getinfo"); + byte[] response = await ConnectAndSend("getinfo"); - using var br = new BinaryReader(new MemoryStream(responseData), Encoding.UTF8); - var header = br.ReadStringEx(_Delimiter1); - string infoResponseHeader = "infoResponse\n"; - - if (header != infoResponseHeader) + using (var br = new BinaryReader(new MemoryStream(response))) { - throw new Exception($"Packet header mismatch. Received: {header}. Expected: {infoResponseHeader}."); - } + var header = br.ReadStringEx(Delimiter1); + string infoResponseHeader = "infoResponse\n"; - var info = ParseInfo(br); + if (header != infoResponseHeader) + { + throw new Exception($"Packet header mismatch. Received: {header}. Expected: {infoResponseHeader}."); + } - if (!stripColor) - { - return info; - } + var info = ParseInfo(br); - if (info.ContainsKey("hostname")) - { - info["hostname"] = StripColors(info["hostname"]); - } + if (stripColor) + { + if (info.ContainsKey("hostname")) + { + info["hostname"] = StripColors(info["hostname"]); + } + } - return info; + return info; + } } /// @@ -71,33 +70,32 @@ public async Task> GetInfo(bool stripColor = true) /// Thrown when a socket error occurs. public async Task GetStatus(bool stripColor = true) { - using var br = await GetResponseBinaryReader(); - - var status = new Status - { - Info = ParseInfo(br), - Players = ParsePlayers(br), - }; - - if (!stripColor) - { - return status; - } - - if (status.Info.ContainsKey("sv_hostname")) + using (var br = await GetResponseBinaryReader()) { - status.Info["sv_hostname"] = StripColors(status.Info["sv_hostname"]); - } + var status = new Status + { + Info = ParseInfo(br), + Players = ParsePlayers(br), + }; - foreach (var player in status.Players) - { - if (player.Name != null) + if (stripColor) { - player.Name = StripColors(player.Name); + if (status.Info.ContainsKey("sv_hostname")) + { + status.Info["sv_hostname"] = StripColors(status.Info["sv_hostname"]); + } + + foreach (var player in status.Players) + { + if (player.Name != null) + { + player.Name = StripColors(player.Name); + } + } } - } - return status; + return status; + } } /// diff --git a/OpenGSQ/Protocols/RakNet.cs b/OpenGSQ/Protocols/RakNet.cs index 05898ea..6b46df0 100644 --- a/OpenGSQ/Protocols/RakNet.cs +++ b/OpenGSQ/Protocols/RakNet.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using System.Linq; using System.IO; -using System.Net.Sockets; using OpenGSQ.Responses.RakNet; namespace OpenGSQ.Protocols @@ -37,45 +36,46 @@ public RakNet(string host, int port, int timeout = 5000) : base(host, port, time /// A task that represents the asynchronous operation. The task result contains the server status. public async Task GetStatus() { - var request = ID_UNCONNECTED_PING.Concat(TIMESTAMP).Concat(OFFLINE_MESSAGE_DATA_ID).Concat(CLIENT_GUID).ToArray(); - using var udpClient = new UdpClient(); - var response = await udpClient.CommunicateAsync(this, request); + byte[] request = ID_UNCONNECTED_PING.Concat(TIMESTAMP).Concat(OFFLINE_MESSAGE_DATA_ID).Concat(CLIENT_GUID).ToArray(); + byte[] response = await UdpClient.CommunicateAsync(this, request); - using var br = new BinaryReader(new MemoryStream(response)); - var header = br.ReadByte(); - - if (header != ID_UNCONNECTED_PONG[0]) + using (var br = new BinaryReader(new MemoryStream(response))) { - throw new InvalidPacketException($"Packet header mismatch. Received: {header}. Expected: {ID_UNCONNECTED_PONG[0]}."); - } + var header = br.ReadByte(); - br.ReadBytes(TIMESTAMP.Length + CLIENT_GUID.Length); // skip timestamp and guid - var magic = br.ReadBytes(OFFLINE_MESSAGE_DATA_ID.Length); + if (header != ID_UNCONNECTED_PONG[0]) + { + throw new InvalidPacketException($"Packet header mismatch. Received: {header}. Expected: {ID_UNCONNECTED_PONG[0]}."); + } - if (!magic.SequenceEqual(OFFLINE_MESSAGE_DATA_ID)) - { - throw new InvalidPacketException($"Magic value mismatch. Received: {magic}. Expected: {OFFLINE_MESSAGE_DATA_ID}."); - } + br.ReadBytes(TIMESTAMP.Length + CLIENT_GUID.Length); // skip timestamp and guid + var magic = br.ReadBytes(OFFLINE_MESSAGE_DATA_ID.Length); - br.ReadInt16(); // skip remaining packet length + if (!magic.SequenceEqual(OFFLINE_MESSAGE_DATA_ID)) + { + throw new InvalidPacketException($"Magic value mismatch. Received: {magic}. Expected: {OFFLINE_MESSAGE_DATA_ID}."); + } - byte[] delimiter = { (byte)';' }; + br.ReadInt16(); // skip remaining packet length - return new Status - { - Edition = br.ReadStringEx(delimiter), - MotdLine1 = br.ReadStringEx(delimiter), - ProtocolVersion = int.Parse(br.ReadStringEx(delimiter)), - VersionName = br.ReadStringEx(delimiter), - NumPlayers = int.Parse(br.ReadStringEx(delimiter)), - MaxPlayers = int.Parse(br.ReadStringEx(delimiter)), - ServerUniqueId = br.ReadStringEx(delimiter), - MotdLine2 = br.ReadStringEx(delimiter), - GameMode = br.ReadStringEx(delimiter), - GameModeNumeric = int.Parse(br.ReadStringEx(delimiter)), - PortIPv4 = int.Parse(br.ReadStringEx(delimiter)), - PortIPv6 = int.Parse(br.ReadStringEx(delimiter)) - }; + byte[] delimiter = { (byte)';' }; + + return new Status + { + Edition = br.ReadStringEx(delimiter), + MotdLine1 = br.ReadStringEx(delimiter), + ProtocolVersion = int.Parse(br.ReadStringEx(delimiter)), + VersionName = br.ReadStringEx(delimiter), + NumPlayers = int.Parse(br.ReadStringEx(delimiter)), + MaxPlayers = int.Parse(br.ReadStringEx(delimiter)), + ServerUniqueId = br.ReadStringEx(delimiter), + MotdLine2 = br.ReadStringEx(delimiter), + GameMode = br.ReadStringEx(delimiter), + GameModeNumeric = int.Parse(br.ReadStringEx(delimiter)), + PortIPv4 = int.Parse(br.ReadStringEx(delimiter)), + PortIPv6 = int.Parse(br.ReadStringEx(delimiter)) + }; + } } } } diff --git a/OpenGSQ/Protocols/Samp.cs b/OpenGSQ/Protocols/Samp.cs index bf9aa15..e624486 100644 --- a/OpenGSQ/Protocols/Samp.cs +++ b/OpenGSQ/Protocols/Samp.cs @@ -1,8 +1,5 @@ -using System; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Net.Sockets; using System.Text; using System.Threading.Tasks; using OpenGSQ.Responses.Samp; @@ -12,14 +9,11 @@ namespace OpenGSQ.Protocols /// /// San Andreas Multiplayer Protocol /// - public class Samp : ProtocolBase + public class Samp : Vcmp { /// public override string FullName => "San Andreas Multiplayer Protocol"; - private static readonly byte[] _requestHeader = Encoding.UTF8.GetBytes("SAMP"); - private static readonly byte[] _responseHeader = Encoding.UTF8.GetBytes("SAMP"); - /// /// Initializes a new instance of the Samp class. /// @@ -28,52 +22,58 @@ public class Samp : ProtocolBase /// The timeout for the connection in milliseconds. public Samp(string host, int port, int timeout = 5000) : base(host, port, timeout) { + RequestHeader = Encoding.UTF8.GetBytes("SAMP"); + ResponseHeader = Encoding.UTF8.GetBytes("SAMP"); } /// /// Asynchronously gets the status of the server. /// /// A task that represents the asynchronous operation. The task result contains a StatusResponse object with the server status. - public async Task GetStatus() + public new async Task GetStatus() { var response = await SendAndReceive(new byte[] { (byte)'i' }); - using var br = new BinaryReader(new MemoryStream(response), Encoding.UTF8); - return new Status + using (var br = new BinaryReader(new MemoryStream(response))) { - Password = br.ReadByte() == 1, - NumPlayers = br.ReadInt16(), - MaxPlayers = br.ReadInt16(), - ServerName = ReadString(br, 4), - GameType = ReadString(br, 4), - Language = ReadString(br, 4), - }; + return new Status + { + Password = br.ReadByte() == 1, + NumPlayers = br.ReadInt16(), + MaxPlayers = br.ReadInt16(), + ServerName = ReadString(br, 4), + GameType = ReadString(br, 4), + Language = ReadString(br, 4), + }; + } } /// /// Asynchronously gets the list of players from the server. /// /// A task that represents the asynchronous operation. The task result contains a list of Player objects. - public async Task> GetPlayers() + public new async Task> GetPlayers() { var response = await SendAndReceive(new byte[] { (byte)'d' }); - var players = new List(); - - using var br = new BinaryReader(new MemoryStream(response), Encoding.UTF8); - var numplayers = br.ReadInt16(); - for (var i = 0; i < numplayers; i++) + using (var br = new BinaryReader(new MemoryStream(response))) { - players.Add(new Player + var numplayers = br.ReadInt16(); + var players = new List(); + + for (var i = 0; i < numplayers; i++) { - Id = br.ReadByte(), - Name = ReadString(br), - Score = br.ReadInt32(), - Ping = br.ReadInt32() - }); + players.Add(new Player + { + Id = br.ReadByte(), + Name = ReadString(br), + Score = br.ReadInt32(), + Ping = br.ReadInt32() + }); + } + + return players; } - - return players; } /// @@ -84,55 +84,18 @@ public async Task> GetRules() { var response = await SendAndReceive(new byte[] { (byte)'r' }); - using var br = new BinaryReader(new MemoryStream(response), Encoding.UTF8); - var numrules = br.ReadInt16(); - - var rules = new Dictionary(); - - for (int i = 0; i < numrules; i++) + using (var br = new BinaryReader(new MemoryStream(response))) { - rules.Add(ReadString(br), ReadString(br)); - } - - return rules; - } - - private async Task SendAndReceive(byte[] data) - { - // Format the address - var host = (await GetIPEndPoint()).Address.ToString(); + var numrules = br.ReadInt16(); + var rules = new Dictionary(); - byte[] headerData = new byte[6]; - string[] splitIp = host.Split('.'); - - for (int i = 0; i < splitIp.Length; i++) - { - headerData[i] = Convert.ToByte(int.Parse(splitIp[i])); - } - - headerData[4] = (byte)(Port >> 8); - headerData[5] = (byte)Port; - - byte[] packetHeader = headerData.Concat(data).ToArray(); - var request = _requestHeader.Concat(packetHeader).ToArray(); - - // Validate the response - using var udpClient = new UdpClient(); - var response = await udpClient.CommunicateAsync(this, request); - var header = response[.._responseHeader.Length]; + for (int i = 0; i < numrules; i++) + { + rules.Add(ReadString(br), ReadString(br)); + } - if (!header.SequenceEqual(_responseHeader)) - { - throw new InvalidPacketException($"Packet header mismatch. Received: {BitConverter.ToString(header)}. Expected: {BitConverter.ToString(_responseHeader)}."); + return rules; } - - return response[(_responseHeader.Length + packetHeader.Length)..]; - } - - private string ReadString(BinaryReader br, int readOffset = 1) - { - var length = readOffset == 1 ? br.ReadByte() : br.ReadInt32(); - return Encoding.UTF8.GetString(br.ReadBytes(length)); } } } diff --git a/OpenGSQ/Protocols/Satisfactory.cs b/OpenGSQ/Protocols/Satisfactory.cs index 82f9032..1dca675 100644 --- a/OpenGSQ/Protocols/Satisfactory.cs +++ b/OpenGSQ/Protocols/Satisfactory.cs @@ -1,7 +1,6 @@ using System; using System.IO; using System.Linq; -using System.Net.Sockets; using System.Text; using System.Threading.Tasks; using OpenGSQ.Responses.Satisfactory; @@ -34,28 +33,29 @@ public async Task GetStatus() { // https://github.com/dopeghoti/SF-Tools/blob/main/Protocol.md byte[] request = new byte[] { 0, 0 }.Concat(Encoding.ASCII.GetBytes("opengsq")).ToArray(); + byte[] response = await UdpClient.CommunicateAsync(this, request); - using var udpClient = new UdpClient(); - byte[] response = await udpClient.CommunicateAsync(this, request); - using var br = new BinaryReader(new MemoryStream(response), Encoding.UTF8); - byte header = br.ReadByte(); - - if (header != 1) + using (var br = new BinaryReader(new MemoryStream(response))) { - throw new Exception($"Packet header mismatch. Received: {header}. Expected: 1."); - } + byte header = br.ReadByte(); - br.ReadByte(); // Protocol version - br.ReadBytes(8); // Request data + if (header != 1) + { + throw new Exception($"Packet header mismatch. Received: {header}. Expected: 1."); + } - var result = new Status - { - State = br.ReadByte(), - Version = br.ReadInt32(), - BeaconPort = br.ReadInt16() - }; + br.ReadByte(); // Protocol version + br.ReadBytes(8); // Request data - return result; + var result = new Status + { + State = br.ReadByte(), + Version = br.ReadInt32(), + BeaconPort = br.ReadInt16() + }; + + return result; + } } } } diff --git a/OpenGSQ/Protocols/Scum.cs b/OpenGSQ/Protocols/Scum.cs index aa2bea5..e7af188 100644 --- a/OpenGSQ/Protocols/Scum.cs +++ b/OpenGSQ/Protocols/Scum.cs @@ -44,7 +44,10 @@ public async Task GetStatus(List masterServers = null) { var ip = (await GetIPEndPoint()).Address.ToString(); - masterServers ??= await QueryMasterServers(); + if (masterServers == null) + { + masterServers = await QueryMasterServers(); + } foreach (var server in masterServers) { @@ -68,52 +71,56 @@ public static async Task> QueryMasterServers() { try { - using var tcpClient = new TcpClient(); - tcpClient.ReceiveTimeout = 5000; - await tcpClient.ConnectAsync(host, port); - await tcpClient.SendAsync(new byte[] { 0x04, 0x03, 0x00, 0x00 }); - - var total = -1; - var response = new byte[0]; - var servers = new List(); - - while (total == -1 || servers.Count < total) + using (var tcpClient = new System.Net.Sockets.TcpClient()) { - response = response.Concat(await tcpClient.ReceiveAsync()).ToArray(); - using var br = new BinaryReader(new MemoryStream(response)); + tcpClient.ReceiveTimeout = 5000; + await tcpClient.ConnectAsync(host, port); + await tcpClient.SendAsync(new byte[] { 0x04, 0x03, 0x00, 0x00 }); - // first packet return the total number of servers - if (total == -1) - { - total = br.ReadInt16(); - } + var total = -1; + var response = new byte[0]; + var servers = new List(); - // server bytes length always 127 - while (br.BaseStream.Length - br.BaseStream.Position >= 127) + while (total == -1 || servers.Count < total) { - var statusResponse = new Status + response = response.Concat(await tcpClient.ReceiveAsync()).ToArray(); + + using (var br = new BinaryReader(new MemoryStream(response))) { - Ip = string.Join(".", br.ReadBytes(4).Reverse().Select(b => b.ToString())), - Port = br.ReadInt16(), - Name = Encoding.UTF8.GetString(br.ReadBytes(100).TakeWhile(b => b != 0).ToArray()) - }; - br.ReadByte(); // skip - statusResponse.NumPlayers = br.ReadByte(); - statusResponse.MaxPlayers = br.ReadByte(); - statusResponse.Time = br.ReadByte(); - br.ReadByte(); // skip - statusResponse.Password = ((br.ReadByte() >> 1) & 1) == 1; - br.ReadBytes(7); // skip - var v = br.ReadBytes(8).Reverse().Select(b => Convert.ToInt32(b).ToString("X").PadLeft(2, '0')).ToList(); - statusResponse.Version = $"{Convert.ToInt32(v[0], 16)}.{Convert.ToInt32(v[1], 16)}.{Convert.ToInt32(v[2] + v[3], 16)}.{Convert.ToInt32(v[4] + v[5] + v[6] + v[7], 16)}"; - servers.Add(statusResponse); + // first packet return the total number of servers + if (total == -1) + { + total = br.ReadInt16(); + } + + // server bytes length always 127 + while (br.BaseStream.Length - br.BaseStream.Position >= 127) + { + var statusResponse = new Status + { + Ip = string.Join(".", br.ReadBytes(4).Reverse().Select(b => b.ToString())), + Port = br.ReadInt16(), + Name = Encoding.UTF8.GetString(br.ReadBytes(100).TakeWhile(b => b != 0).ToArray()) + }; + br.ReadByte(); // skip + statusResponse.NumPlayers = br.ReadByte(); + statusResponse.MaxPlayers = br.ReadByte(); + statusResponse.Time = br.ReadByte(); + br.ReadByte(); // skip + statusResponse.Password = ((br.ReadByte() >> 1) & 1) == 1; + br.ReadBytes(7); // skip + var v = br.ReadBytes(8).Reverse().Select(b => Convert.ToInt32(b).ToString("X").PadLeft(2, '0')).ToList(); + statusResponse.Version = $"{Convert.ToInt32(v[0], 16)}.{Convert.ToInt32(v[1], 16)}.{Convert.ToInt32(v[2] + v[3], 16)}.{Convert.ToInt32(v[4] + v[5] + v[6] + v[7], 16)}"; + servers.Add(statusResponse); + } + + // if the length is less than 127, save the unused bytes for next loop + response = br.ReadBytes((int)(br.BaseStream.Length - br.BaseStream.Position)); + } } - // if the length is less than 127, save the unused bytes for next loop - response = br.ReadBytes((int)(br.BaseStream.Length - br.BaseStream.Position)); + return servers; } - - return servers; } catch (SocketException) { diff --git a/OpenGSQ/Protocols/Source.cs b/OpenGSQ/Protocols/Source.cs index 97db7dd..22c2013 100644 --- a/OpenGSQ/Protocols/Source.cs +++ b/OpenGSQ/Protocols/Source.cs @@ -55,112 +55,114 @@ public Source(string host, int port = 27015, int timeout = 5000) : base(host, po /// public async Task GetInfo() { - var responseData = await ConnectAndSendChallenge(A2S_INFO); + byte[] response = await ConnectAndSendChallenge(A2S_INFO); - using var br = new BinaryReader(new MemoryStream(responseData), Encoding.UTF8); - var header = br.ReadByte(); - - if (header != (byte)QueryResponse.S2A_INFO_SRC && header != (byte)QueryResponse.S2A_INFO_DETAILED) + using (var br = new BinaryReader(new MemoryStream(response))) { - throw new Exception($"Packet header mismatch. Received: {header}. Expected: {QueryResponse.S2A_INFO_SRC} or {QueryResponse.S2A_INFO_DETAILED}."); - } + var header = br.ReadByte(); - if (header == (byte)QueryResponse.S2A_INFO_SRC) - { - var source = new SourceInfo - { - Protocol = br.ReadByte(), - Name = br.ReadStringEx(), - Map = br.ReadStringEx(), - Folder = br.ReadStringEx(), - Game = br.ReadStringEx(), - ID = br.ReadInt16(), - Players = br.ReadByte(), - MaxPlayers = br.ReadByte(), - Bots = br.ReadByte(), - ServerType = (ServerType)br.ReadByte(), - Environment = GetEnvironment(br.ReadByte()), - Visibility = (Visibility)br.ReadByte(), - VAC = (VAC)br.ReadByte() - }; - - if (source.ID == 2400) + if (header != (byte)QueryResponse.S2A_INFO_SRC && header != (byte)QueryResponse.S2A_INFO_DETAILED) { - source.Mode = br.ReadByte(); - source.Witnesses = br.ReadByte(); - source.Duration = br.ReadByte(); + throw new Exception($"Packet header mismatch. Received: {header}. Expected: {QueryResponse.S2A_INFO_SRC} or {QueryResponse.S2A_INFO_DETAILED}."); } - source.Version = br.ReadStringEx(); - - if (br.BaseStream.Position < br.BaseStream.Length) + if (header == (byte)QueryResponse.S2A_INFO_SRC) { - source.EDF = (ExtraDataFlag)br.ReadByte(); - - var edf = (ExtraDataFlag)source.EDF; - - if (edf.HasFlag(ExtraDataFlag.Port)) + var source = new SourceInfo { - source.Port = br.ReadInt16(); - } - - if (edf.HasFlag(ExtraDataFlag.SteamID)) + Protocol = br.ReadByte(), + Name = br.ReadStringEx(), + Map = br.ReadStringEx(), + Folder = br.ReadStringEx(), + Game = br.ReadStringEx(), + ID = br.ReadInt16(), + Players = br.ReadByte(), + MaxPlayers = br.ReadByte(), + Bots = br.ReadByte(), + ServerType = (ServerType)br.ReadByte(), + Environment = GetEnvironment(br.ReadByte()), + Visibility = (Visibility)br.ReadByte(), + VAC = (VAC)br.ReadByte() + }; + + if (source.ID == 2400) { - source.SteamID = br.ReadUInt64(); + source.Mode = br.ReadByte(); + source.Witnesses = br.ReadByte(); + source.Duration = br.ReadByte(); } - if (edf.HasFlag(ExtraDataFlag.Spectator)) - { - source.SpectatorPort = br.ReadInt16(); - source.SpectatorName = br.ReadStringEx(); - } + source.Version = br.ReadStringEx(); - if (edf.HasFlag(ExtraDataFlag.Keywords)) + if (br.BaseStream.Position < br.BaseStream.Length) { - source.Keywords = br.ReadStringEx(); - } + source.EDF = (ExtraDataFlag)br.ReadByte(); - if (edf.HasFlag(ExtraDataFlag.SteamID)) - { - source.GameID = br.ReadUInt64(); + var edf = (ExtraDataFlag)source.EDF; + + if (edf.HasFlag(ExtraDataFlag.Port)) + { + source.Port = br.ReadInt16(); + } + + if (edf.HasFlag(ExtraDataFlag.SteamID)) + { + source.SteamID = br.ReadUInt64(); + } + + if (edf.HasFlag(ExtraDataFlag.Spectator)) + { + source.SpectatorPort = br.ReadInt16(); + source.SpectatorName = br.ReadStringEx(); + } + + if (edf.HasFlag(ExtraDataFlag.Keywords)) + { + source.Keywords = br.ReadStringEx(); + } + + if (edf.HasFlag(ExtraDataFlag.SteamID)) + { + source.GameID = br.ReadUInt64(); + } } - } - return source; - } - else - { - var goldSource = new GoldSourceInfo - { - Address = br.ReadStringEx(), - Name = br.ReadStringEx(), - Map = br.ReadStringEx(), - Folder = br.ReadStringEx(), - Game = br.ReadStringEx(), - Players = br.ReadByte(), - MaxPlayers = br.ReadByte(), - Protocol = br.ReadByte(), - ServerType = (ServerType)char.ToLower(Convert.ToChar(br.ReadByte())), - Environment = (Environment)char.ToLower(Convert.ToChar(br.ReadByte())), - Visibility = (Visibility)br.ReadByte(), - Mod = br.ReadByte() - }; - - if (goldSource.Mod == 1) - { - goldSource.Link = br.ReadStringEx(); - goldSource.DownloadLink = br.ReadStringEx(); - br.ReadByte(); - goldSource.Version = br.ReadInt32(); - goldSource.Size = br.ReadInt32(); - goldSource.Type = br.ReadByte(); - goldSource.DLL = br.ReadByte(); + return source; } + else + { + var goldSource = new GoldSourceInfo + { + Address = br.ReadStringEx(), + Name = br.ReadStringEx(), + Map = br.ReadStringEx(), + Folder = br.ReadStringEx(), + Game = br.ReadStringEx(), + Players = br.ReadByte(), + MaxPlayers = br.ReadByte(), + Protocol = br.ReadByte(), + ServerType = (ServerType)char.ToLower(Convert.ToChar(br.ReadByte())), + Environment = (Environment)char.ToLower(Convert.ToChar(br.ReadByte())), + Visibility = (Visibility)br.ReadByte(), + Mod = br.ReadByte() + }; + + if (goldSource.Mod == 1) + { + goldSource.Link = br.ReadStringEx(); + goldSource.DownloadLink = br.ReadStringEx(); + br.ReadByte(); + goldSource.Version = br.ReadInt32(); + goldSource.Size = br.ReadInt32(); + goldSource.Type = br.ReadByte(); + goldSource.DLL = br.ReadByte(); + } - goldSource.VAC = (VAC)br.ReadByte(); - goldSource.Bots = br.ReadByte(); + goldSource.VAC = (VAC)br.ReadByte(); + goldSource.Bots = br.ReadByte(); - return goldSource; + return goldSource; + } } } @@ -172,43 +174,44 @@ public async Task GetInfo() /// public async Task> GetPlayers() { - var responseData = await ConnectAndSendChallenge(A2S_PLAYER); - - using var br = new BinaryReader(new MemoryStream(responseData), Encoding.UTF8); - var header = br.ReadByte(); + byte[] response = await ConnectAndSendChallenge(A2S_PLAYER); - if (header != (byte)QueryResponse.S2A_PLAYER) + using (var br = new BinaryReader(new MemoryStream(response))) { - throw new Exception($"Packet header mismatch. Received: {header}. Expected: {QueryResponse.S2A_PLAYER}."); - } - - var playerCount = br.ReadByte(); + var header = br.ReadByte(); - var players = new List(); + if (header != (byte)QueryResponse.S2A_PLAYER) + { + throw new Exception($"Packet header mismatch. Received: {header}. Expected: {QueryResponse.S2A_PLAYER}."); + } - // Save the players - for (int i = 0; i < playerCount; i++) - { - br.ReadByte(); + var playerCount = br.ReadByte(); + var players = new List(); - players.Add(new Player + // Save the players + for (int i = 0; i < playerCount; i++) { - Name = br.ReadStringEx(), - Score = br.ReadInt32(), - Duration = br.ReadSingle(), - }); - } + br.ReadByte(); - if (br.BaseStream.Position < br.BaseStream.Length) - { - for (int i = 0; i < playerCount; i++) + players.Add(new Player + { + Name = br.ReadStringEx(), + Score = br.ReadInt32(), + Duration = br.ReadSingle(), + }); + } + + if (br.BaseStream.Position < br.BaseStream.Length) { - players[i].Deaths = br.ReadInt32(); - players[i].Money = br.ReadInt32(); + for (int i = 0; i < playerCount; i++) + { + players[i].Deaths = br.ReadInt32(); + players[i].Money = br.ReadInt32(); + } } - } - return players; + return players; + } } /// @@ -219,73 +222,76 @@ public async Task> GetPlayers() /// public async Task> GetRules() { - var responseData = await ConnectAndSendChallenge(A2S_RULES); - - using var br = new BinaryReader(new MemoryStream(responseData), Encoding.UTF8); - var header = br.ReadByte(); + byte[] response = await ConnectAndSendChallenge(A2S_RULES); - if (header != (byte)QueryResponse.S2A_RULES) + using (var br = new BinaryReader(new MemoryStream(response))) { - throw new Exception($"Packet header mismatch. Received: {header}. Expected: {QueryResponse.S2A_RULES}."); - } + var header = br.ReadByte(); - var ruleCount = br.ReadUInt16(); + if (header != (byte)QueryResponse.S2A_RULES) + { + throw new Exception($"Packet header mismatch. Received: {header}. Expected: {QueryResponse.S2A_RULES}."); + } - var rules = new Dictionary(); + var ruleCount = br.ReadUInt16(); + var rules = new Dictionary(); - // Save the rules into dictionary - for (int i = 0; i < ruleCount; i++) - { - rules.Add(br.ReadStringEx(), br.ReadStringEx()); - } + // Save the rules into dictionary + for (int i = 0; i < ruleCount; i++) + { + rules.Add(br.ReadStringEx(), br.ReadStringEx()); + } - return rules; + return rules; + } } private async Task ConnectAndSendChallenge(byte[] header) { - using var udpClient = new UdpClient(); + using (var udpClient = new System.Net.Sockets.UdpClient()) + { + udpClient.Client.SendTimeout = Timeout; + udpClient.Client.ReceiveTimeout = Timeout; - // Connect to remote host - udpClient.Connect(Host, Port); - udpClient.Client.SendTimeout = Timeout; - udpClient.Client.ReceiveTimeout = Timeout; + // Connect to remote host + udpClient.Connect(Host, Port); - // Set up request base - byte[] requestBase = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }.Concat(header).ToArray(); + // Set up request base + byte[] requestBase = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }.Concat(header).ToArray(); - if (header.SequenceEqual(A2S_INFO)) - { - requestBase = requestBase.Concat(Encoding.Default.GetBytes("Source Engine Query\0")).ToArray(); - } + if (header.SequenceEqual(A2S_INFO)) + { + requestBase = requestBase.Concat(Encoding.Default.GetBytes("Source Engine Query\0")).ToArray(); + } - // Set up request data - byte[] requestData = requestBase; + // Set up request data + byte[] requestData = requestBase; - if (!header.SequenceEqual(A2S_INFO)) - { - requestData = requestData.Concat(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }).ToArray(); - } + if (!header.SequenceEqual(A2S_INFO)) + { + requestData = requestData.Concat(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }).ToArray(); + } - // Send and receive - await udpClient.SendAsync(requestData, requestData.Length); - byte[] responseData = await Receive(udpClient); + // Send and receive + await udpClient.SendAsync(requestData, requestData.Length); + byte[] response = await Receive(udpClient); - // The server may reply with a challenge - if (responseData[0] == (byte)QueryResponse.S2C_CHALLENGE) - { - byte[] challenge = responseData.Skip(1).ToArray(); + // The server may reply with a challenge + if (response[0] == (byte)QueryResponse.S2C_CHALLENGE) + { + byte[] challenge = response.Skip(1).ToArray(); - // Send the challenge and receive - requestData = requestBase.Concat(challenge).ToArray(); - await udpClient.SendAsync(requestData, requestData.Length); - responseData = await Receive(udpClient); - } + // Send the challenge and receive + requestData = requestBase.Concat(challenge).ToArray(); + await udpClient.SendAsync(requestData, requestData.Length); + response = await Receive(udpClient); + } - return responseData; + return response; + } } - private async Task Receive(UdpClient udpClient) + private async Task Receive(System.Net.Sockets.UdpClient udpClient) { bool isCompressed; int totalPackets = -1, crc32Sum = 0; @@ -294,49 +300,51 @@ private async Task Receive(UdpClient udpClient) do { - var responseData = await udpClient.ReceiveAsyncWithTimeout(); - packets.Add(responseData); + byte[] response = await udpClient.ReceiveAsyncWithTimeout(); + packets.Add(response); - using var br = new BinaryReader(new MemoryStream(responseData), Encoding.UTF8); - var header = br.ReadInt32(); - - // Simple Response Format - if (header == -1) + using (var br = new BinaryReader(new MemoryStream(response))) { - // Return the payload - return responseData.Skip((int)br.BaseStream.Position).ToArray(); - } + var header = br.ReadInt32(); + + // Simple Response Format + if (header == -1) + { + // Return the payload + return response.Skip((int)br.BaseStream.Position).ToArray(); + } - // Packet id - int id = br.ReadInt32(); - isCompressed = id < 0; + // Packet id + int id = br.ReadInt32(); + isCompressed = id < 0; - // Check is GoldSource multi-packet response format - if (IsGoldSourceSplit(responseData, (int)br.BaseStream.Position)) - { - // Return the payload - return await ParseGoldSourcePackets(udpClient, packets); - } + // Check is GoldSource multi-packet response format + if (IsGoldSourceSplit(response, (int)br.BaseStream.Position)) + { + // Return the payload + return await ParseGoldSourcePackets(udpClient, packets); + } - // The total number of packets - totalPackets = br.ReadByte(); + // The total number of packets + totalPackets = br.ReadByte(); - // The number of the packet - int number = br.ReadByte(); + // The number of the packet + int number = br.ReadByte(); - // Packet size - br.ReadUInt16(); + // Packet size + br.ReadUInt16(); - if (number == 0 && isCompressed) - { - // Decompressed size - br.ReadInt32(); + if (number == 0 && isCompressed) + { + // Decompressed size + br.ReadInt32(); - // CRC32 sum - crc32Sum = br.ReadInt32(); - } + // CRC32 sum + crc32Sum = br.ReadInt32(); + } - payloads.Add(number, responseData.Skip((int)br.BaseStream.Position).ToArray()); + payloads.Add(number, response.Skip((int)br.BaseStream.Position).ToArray()); + } } while (totalPackets == -1 || payloads.Count < totalPackets); // Combine the payloads @@ -376,7 +384,7 @@ private bool IsGoldSourceSplit(byte[] responseData, int streamPosition) return number == 0 && data[1] == 0xFF && data[2] == 0xFF && data[3] == 0xFF && data[4] == 0xFF; } - private async Task ParseGoldSourcePackets(UdpClient udpClient, List packets) + private async Task ParseGoldSourcePackets(System.Net.Sockets.UdpClient udpClient, List packets) { int totalPackets = -1; var payloads = new SortedDictionary(); @@ -384,25 +392,27 @@ private async Task ParseGoldSourcePackets(UdpClient udpClient, List> 4; + // Upper 4 bits represent the number of the current packet (starting at 0) + int number = totalPackets >> 4; - // Bottom 4 bits represent the total number of packets (2 to 15) - totalPackets &= 0x0F; + // Bottom 4 bits represent the total number of packets (2 to 15) + totalPackets &= 0x0F; - payloads.Add(number, responseData.Skip((int)br.BaseStream.Position).ToArray()); + payloads.Add(number, response.Skip((int)br.BaseStream.Position).ToArray()); + } } // Combine the payloads @@ -413,12 +423,15 @@ private async Task ParseGoldSourcePackets(UdpClient udpClient, List Environment.Linux, - (byte)Environment.Windows => Environment.Windows, - _ => Environment.Mac, - }; + case (byte)Environment.Linux: + return Environment.Linux; + case (byte)Environment.Windows: + return Environment.Windows; + default: + return Environment.Mac; + } } private enum QueryResponse : byte @@ -435,7 +448,7 @@ private enum QueryResponse : byte /// public class RemoteConsole : ProtocolBase, IDisposable { - private TcpClient _tcpClient; + private System.Net.Sockets.TcpClient _tcpClient; /// public override string FullName => "Source RCON Protocol"; @@ -472,7 +485,7 @@ public async Task Authenticate(string password) } // Connect - _tcpClient = new TcpClient(); + _tcpClient = new System.Net.Sockets.TcpClient(); await _tcpClient.ConnectAsync(Host, Port); _tcpClient.Client.SendTimeout = Timeout; _tcpClient.Client.ReceiveTimeout = Timeout; @@ -554,29 +567,31 @@ public async Task SendCommand(string command) private (List, byte[]) GetPackets(byte[] bytes) { var packets = new List(); - using var br = new BinaryReader(new MemoryStream(bytes), Encoding.UTF8); - // + 4 to ensure br.ReadInt32() is readable - while (br.BaseStream.Position + 4 < br.BaseStream.Length) + using (var br = new BinaryReader(new MemoryStream(bytes))) { - int size = br.ReadInt32(); - - // Return if we know not enough bytes to read - if (br.BaseStream.Position + size > br.BaseStream.Length) + // + 4 to ensure br.ReadInt32() is readable + while (br.BaseStream.Position + 4 < br.BaseStream.Length) { - return (packets, bytes.Skip((int)br.BaseStream.Position - 4).ToArray()); - } + int size = br.ReadInt32(); - // Read packet and append to packets - var id = br.ReadInt32(); - var type = (PacketType)br.ReadInt32(); - var body = br.ReadStringEx(); - br.ReadByte(); + // Return if we know not enough bytes to read + if (br.BaseStream.Position + size > br.BaseStream.Length) + { + return (packets, bytes.Skip((int)br.BaseStream.Position - 4).ToArray()); + } - packets.Add(new Packet(id, type, body)); - } + // Read packet and append to packets + var id = br.ReadInt32(); + var type = (PacketType)br.ReadInt32(); + var body = br.ReadStringEx(); + br.ReadByte(); + + packets.Add(new Packet(id, type, body)); + } - return (packets, new byte[0]); + return (packets, new byte[0]); + } } private enum PacketType : int @@ -614,11 +629,13 @@ public byte[] GetBytes() /// public Packet(byte[] bytes) { - using var br = new BinaryReader(new MemoryStream(bytes), Encoding.UTF8); - br.ReadInt32(); - Id = br.ReadInt32(); - Type = (PacketType)br.ReadInt32(); - Body = br.ReadStringEx(); + using (var br = new BinaryReader(new MemoryStream(bytes))) + { + br.ReadInt32(); + Id = br.ReadInt32(); + Type = (PacketType)br.ReadInt32(); + Body = br.ReadStringEx(); + } } } } diff --git a/OpenGSQ/Protocols/TeamSpeak3.cs b/OpenGSQ/Protocols/TeamSpeak3.cs index 0efd044..1f10f05 100644 --- a/OpenGSQ/Protocols/TeamSpeak3.cs +++ b/OpenGSQ/Protocols/TeamSpeak3.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using System.Net.Sockets; using System.Text; using System.Threading.Tasks; @@ -60,35 +59,36 @@ public async Task>> GetChannels() private async Task SendAndReceive(byte[] data) { - using var tcpClient = new TcpClient(); + using (var tcpClient = new System.Net.Sockets.TcpClient()) + { + // Set the timeout + tcpClient.SendTimeout = Timeout; + tcpClient.ReceiveTimeout = Timeout; - // Set the timeout - tcpClient.SendTimeout = Timeout; - tcpClient.ReceiveTimeout = Timeout; + await tcpClient.ConnectAsync(Host, Port); - await tcpClient.ConnectAsync(Host, Port); + // Welcome message from the server + await tcpClient.ReceiveAsync(); - // Welcome message from the server - await tcpClient.ReceiveAsync(); + // Send voice port + var portData = Encoding.ASCII.GetBytes($"use port={_voicePort}\n"); + await tcpClient.SendAsync(portData); + await tcpClient.ReceiveAsync(); - // Send voice port - var portData = Encoding.ASCII.GetBytes($"use port={_voicePort}\n"); - await tcpClient.SendAsync(portData); - await tcpClient.ReceiveAsync(); + // Send data + await tcpClient.SendAsync(data.Concat(new byte[] { 0x0A }).ToArray()); - // Send data - await tcpClient.SendAsync(data.Concat(new byte[] { 0x0A }).ToArray()); + // Receive response + string response = string.Empty; - // Receive response - string response = string.Empty; + while (!response.EndsWith("error id=0 msg=ok\n\r")) + { + response += Encoding.UTF8.GetString(await tcpClient.ReceiveAsync()); + } - while (!response.EndsWith("error id=0 msg=ok\n\r")) - { - response += Encoding.UTF8.GetString(await tcpClient.ReceiveAsync()); + // Remove "\n\rerror id=0 msg=ok\n\r" + return response.Substring(0, response.Length - 21); } - - // Remove "\n\rerror id=0 msg=ok\n\r" - return response[..^21]; } private List> ParseRows(string response) @@ -102,9 +102,9 @@ private Dictionary ParseKvs(string response) foreach (var kv in response.Split(' ')) { - string[] items = kv.Split("=", 2); + string[] items = kv.Split(new char[] { '=' }, 2); string key = items[0]; - string val = items.Length == 2 ? items[1] : ""; + string val = items.Length == 2 ? items[1] : string.Empty; kvs[key] = val.Replace("\\p", "|").Replace("\\s", " ").Replace("\\/", "/"); } diff --git a/OpenGSQ/Protocols/Unreal2.cs b/OpenGSQ/Protocols/Unreal2.cs index 04ed521..3b4c902 100644 --- a/OpenGSQ/Protocols/Unreal2.cs +++ b/OpenGSQ/Protocols/Unreal2.cs @@ -1,7 +1,5 @@ using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Net.Sockets; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -46,35 +44,38 @@ public Unreal2(string host, int port, int timeout = 5000) : base(host, port, tim /// Thrown when the packet header does not match the expected value. public async Task GetDetails() { - using var udpClient = new UdpClient(); - byte[] response = await udpClient.CommunicateAsync(this, new byte[] { 0x79, 0x00, 0x00, 0x00, _DETAILS }); + byte[] response = await UdpClient.CommunicateAsync(this, new byte[] { 0x79, 0x00, 0x00, 0x00, _DETAILS }); - // Remove the first 4 bytes \x80\x00\x00\x00 - BinaryReader br = new BinaryReader(new MemoryStream(response.Skip(4).ToArray())); - byte header = br.ReadByte(); - - if (header != _DETAILS) + using (var br = new BinaryReader(new MemoryStream(response))) { - throw new InvalidPacketException($"Packet header mismatch. Received: {header}. Expected: {_DETAILS}."); - } + // Remove the first 4 bytes \x80\x00\x00\x00 + br.ReadBytes(4); - var details = new Status - { - ServerId = br.ReadInt32(), - ServerIP = br.ReadString(), - GamePort = br.ReadInt32(), - QueryPort = br.ReadInt32(), - ServerName = ReadString(br), - MapName = ReadString(br), - GameType = ReadString(br), - NumPlayers = br.ReadInt32(), - MaxPlayers = br.ReadInt32(), - Ping = br.ReadInt32(), - Flags = br.ReadInt32(), - Skill = ReadString(br) - }; - - return details; + byte header = br.ReadByte(); + + if (header != _DETAILS) + { + throw new InvalidPacketException($"Packet header mismatch. Received: {header}. Expected: {_DETAILS}."); + } + + var details = new Status + { + ServerId = br.ReadInt32(), + ServerIP = br.ReadString(), + GamePort = br.ReadInt32(), + QueryPort = br.ReadInt32(), + ServerName = ReadString(br), + MapName = ReadString(br), + GameType = ReadString(br), + NumPlayers = br.ReadInt32(), + MaxPlayers = br.ReadInt32(), + Ping = br.ReadInt32(), + Flags = br.ReadInt32(), + Skill = ReadString(br) + }; + + return details; + } } /// @@ -84,39 +85,42 @@ public async Task GetDetails() /// Thrown when the packet header does not match the expected value. public async Task> GetRules() { - using var udpClient = new UdpClient(); - byte[] response = await udpClient.CommunicateAsync(this, new byte[] { 0x79, 0x00, 0x00, 0x00, _RULES }); + byte[] response = await UdpClient.CommunicateAsync(this, new byte[] { 0x79, 0x00, 0x00, 0x00, _RULES }); - // Remove the first 4 bytes \x80\x00\x00\x00 - BinaryReader br = new BinaryReader(new MemoryStream(response.Skip(4).ToArray())); - byte header = br.ReadByte(); - - if (header != _RULES) + using (var br = new BinaryReader(new MemoryStream(response))) { - throw new InvalidPacketException($"Packet header mismatch. Received: {header}. Expected: {_RULES}."); - } + // Remove the first 4 bytes \x80\x00\x00\x00 + br.ReadBytes(4); - var rules = new Dictionary(); - var mutators = new List(); + byte header = br.ReadByte(); - while (br.BaseStream.Position != br.BaseStream.Length) - { - string key = ReadString(br); - string val = ReadString(br); - - if (key.ToLower() == "mutator") + if (header != _RULES) { - mutators.Add(val); + throw new InvalidPacketException($"Packet header mismatch. Received: {header}. Expected: {_RULES}."); } - else + + var rules = new Dictionary(); + var mutators = new List(); + + while (br.BaseStream.Position != br.BaseStream.Length) { - rules[key] = val; + string key = ReadString(br); + string val = ReadString(br); + + if (key.ToLower() == "mutator") + { + mutators.Add(val); + } + else + { + rules[key] = val; + } } - } - rules["Mutators"] = mutators; + rules["Mutators"] = mutators; - return rules; + return rules; + } } /// @@ -126,35 +130,38 @@ public async Task> GetRules() /// Thrown when the packet header does not match the expected value. public async Task> GetPlayers() { - using var udpClient = new UdpClient(); - byte[] response = await udpClient.CommunicateAsync(this, new byte[] { 0x79, 0x00, 0x00, 0x00, _PLAYERS }); - - // Remove the first 4 bytes \x80\x00\x00\x00 - BinaryReader br = new BinaryReader(new MemoryStream(response.Skip(4).ToArray())); - byte header = br.ReadByte(); + byte[] response = await UdpClient.CommunicateAsync(this, new byte[] { 0x79, 0x00, 0x00, 0x00, _PLAYERS }); - if (header != _PLAYERS) + using (var br = new BinaryReader(new MemoryStream(response))) { - throw new InvalidPacketException($"Packet header mismatch. Received: {header}. Expected: {_PLAYERS}."); - } + // Remove the first 4 bytes \x80\x00\x00\x00 + br.ReadBytes(4); - var players = new List(); + byte header = br.ReadByte(); - while (br.BaseStream.Position != br.BaseStream.Length) - { - var player = new Player + if (header != _PLAYERS) { - Id = br.ReadInt32(), - Name = ReadString(br), - Ping = br.ReadInt32(), - Score = br.ReadInt32(), - StatsId = br.ReadInt32() - }; + throw new InvalidPacketException($"Packet header mismatch. Received: {header}. Expected: {_PLAYERS}."); + } - players.Add(player); - } + var players = new List(); - return players; + while (br.BaseStream.Position != br.BaseStream.Length) + { + var player = new Player + { + Id = br.ReadInt32(), + Name = ReadString(br), + Ping = br.ReadInt32(), + Score = br.ReadInt32(), + StatsId = br.ReadInt32() + }; + + players.Add(player); + } + + return players; + } } /// @@ -176,6 +183,12 @@ protected static string StripColors(byte[] text) protected string ReadString(BinaryReader br) { int length = br.ReadByte(); + + if (length == 0) + { + return string.Empty; + } + string str = br.ReadStringEx(); byte[] b; diff --git a/OpenGSQ/Protocols/Vcmp.cs b/OpenGSQ/Protocols/Vcmp.cs index 3d228c1..b420c95 100644 --- a/OpenGSQ/Protocols/Vcmp.cs +++ b/OpenGSQ/Protocols/Vcmp.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net.Sockets; using System.Text; using System.Threading.Tasks; using OpenGSQ.Responses.Vcmp; @@ -17,8 +16,15 @@ public class Vcmp : ProtocolBase /// public override string FullName => "Vice City Multiplayer Protocol"; - private static readonly byte[] _requestHeader = Encoding.UTF8.GetBytes("VCMP"); - private static readonly byte[] _responseHeader = Encoding.UTF8.GetBytes("MP04"); + /// + /// The request header to be sent to the remote host. + /// + protected byte[] RequestHeader = Encoding.UTF8.GetBytes("VCMP"); + + /// + /// The expected response header from the remote host. + /// + protected byte[] ResponseHeader = Encoding.UTF8.GetBytes("MP04"); /// /// Initializes a new instance of the Vcmp class. @@ -37,18 +43,20 @@ public Vcmp(string host, int port, int timeout = 5000) : base(host, port, timeou public async Task GetStatus() { var response = await SendAndReceive(new byte[] { (byte)'i' }); - using var br = new BinaryReader(new MemoryStream(response), Encoding.UTF8); - return new Status + using (var br = new BinaryReader(new MemoryStream(response))) { - Version = Encoding.UTF8.GetString(br.ReadBytes(12)).Trim('\0'), - Password = br.ReadByte() == 1, - NumPlayers = br.ReadInt16(), - MaxPlayers = br.ReadInt16(), - ServerName = ReadString(br, 4), - GameType = ReadString(br, 4), - Language = ReadString(br, 4), - }; + return new Status + { + Version = Encoding.UTF8.GetString(br.ReadBytes(12)).Trim('\0'), + Password = br.ReadByte() == 1, + NumPlayers = br.ReadInt16(), + MaxPlayers = br.ReadInt16(), + ServerName = ReadString(br, 4), + GameType = ReadString(br, 4), + Language = ReadString(br, 4), + }; + } } /// @@ -58,20 +66,27 @@ public async Task GetStatus() public async Task> GetPlayers() { var response = await SendAndReceive(new byte[] { (byte)'c' }); - var players = new List(); - - using var br = new BinaryReader(new MemoryStream(response), Encoding.UTF8); - var numplayers = br.ReadInt16(); - for (var i = 0; i < numplayers; i++) + using (var br = new BinaryReader(new MemoryStream(response), Encoding.UTF8)) { - players.Add(new Player { Name = ReadString(br) }); - } + var numplayers = br.ReadInt16(); + var players = new List(); + + for (var i = 0; i < numplayers; i++) + { + players.Add(new Player { Name = ReadString(br) }); + } - return players; + return players; + } } - private async Task SendAndReceive(byte[] data) + /// + /// Sends data to a remote host and receives a response. + /// + /// The data to be sent. + /// A byte array containing the response from the remote host. + protected async Task SendAndReceive(byte[] data) { // Format the address var host = (await GetIPEndPoint()).Address.ToString(); @@ -88,22 +103,27 @@ private async Task SendAndReceive(byte[] data) headerData[5] = (byte)Port; byte[] packetHeader = headerData.Concat(data).ToArray(); - var request = _requestHeader.Concat(packetHeader).ToArray(); + var request = RequestHeader.Concat(packetHeader).ToArray(); // Validate the response - using var udpClient = new UdpClient(); - var response = await udpClient.CommunicateAsync(this, request); - var header = response[.._responseHeader.Length]; + var response = await UdpClient.CommunicateAsync(this, request); + var header = response.Take(ResponseHeader.Length).ToArray(); - if (!header.SequenceEqual(_responseHeader)) + if (!header.SequenceEqual(ResponseHeader)) { - throw new InvalidPacketException($"Packet header mismatch. Received: {BitConverter.ToString(header)}. Expected: {BitConverter.ToString(_responseHeader)}."); + throw new InvalidPacketException($"Packet header mismatch. Received: {BitConverter.ToString(header)}. Expected: {BitConverter.ToString(ResponseHeader)}."); } - return response[(_responseHeader.Length + packetHeader.Length)..]; + return response.Skip(ResponseHeader.Length + packetHeader.Length).ToArray(); } - private string ReadString(BinaryReader br, int readOffset = 1) + /// + /// Reads a string from a binary reader. + /// + /// The binary reader. + /// The read offset. Default is 1. + /// The string read from the binary reader. + protected string ReadString(BinaryReader br, int readOffset = 1) { var length = readOffset == 1 ? br.ReadByte() : br.ReadInt32(); return Encoding.UTF8.GetString(br.ReadBytes(length)); diff --git a/OpenGSQTests/OpenGSQTests.csproj b/OpenGSQTests/OpenGSQTests.csproj index c9d7d7d..4ead01c 100644 --- a/OpenGSQTests/OpenGSQTests.csproj +++ b/OpenGSQTests/OpenGSQTests.csproj @@ -1,7 +1,7 @@ - net7 + net7.0 false diff --git a/OpenGSQTests/Protocols/KillingFloorTests.cs b/OpenGSQTests/Protocols/KillingFloorTests.cs index 771c4d4..48e4f02 100644 --- a/OpenGSQTests/Protocols/KillingFloorTests.cs +++ b/OpenGSQTests/Protocols/KillingFloorTests.cs @@ -7,11 +7,11 @@ namespace OpenGSQ.Protocols.Tests [TestClass()] public class KillingFloorTests : TestBase { - public KillingFloor killingFloor = new("104.234.65.235", 7708); + public KillingFloor killingFloor = new("185.80.128.168", 7708); public KillingFloorTests() : base(nameof(KillingFloorTests)) { - // EnableSave = true; + EnableSave = true; } [TestMethod()] diff --git a/OpenGSQTests/Results/KillingFloorTests/GetDetailsTest.json b/OpenGSQTests/Results/KillingFloorTests/GetDetailsTest.json index c81a6c5..bbf81a8 100644 --- a/OpenGSQTests/Results/KillingFloorTests/GetDetailsTest.json +++ b/OpenGSQTests/Results/KillingFloorTests/GetDetailsTest.json @@ -1,16 +1,16 @@ { - "WaveCurrent": 4, - "WaveTotal": 7, + "WaveCurrent": 7, + "WaveTotal": 10, "ServerId": 0, "ServerIP": "", "GamePort": 7707, "QueryPort": 0, - "ServerName": "����||uD��e��e��p����||/T\\||��N��oF��i����s����e����|| #2 <> ��W��", - "MapName": "KF-XY-Doom-5", - "GameType": "KFGameType", - "NumPlayers": 17, - "MaxPlayers": 32, + "ServerName": "WS-GAMING.EU | NORMAL | 60LVL [#1]", + "MapName": "KF-PoliceStationOnEWIPE", + "GameType": "UZGameType", + "NumPlayers": 12, + "MaxPlayers": 25, "Ping": 0, - "Flags": 512, + "Flags": 64, "Skill": "0" } \ No newline at end of file diff --git a/OpenGSQTests/Results/KillingFloorTests/GetPlayersTest.json b/OpenGSQTests/Results/KillingFloorTests/GetPlayersTest.json index fb10aca..5586b0b 100644 --- a/OpenGSQTests/Results/KillingFloorTests/GetPlayersTest.json +++ b/OpenGSQTests/Results/KillingFloorTests/GetPlayersTest.json @@ -1,100 +1,72 @@ [ { - "Id": 2359, - "Name": "[CHL] KA_LE", - "Ping": 48, - "Score": 2332, - "StatsId": 536870912 - }, - { - "Id": 2326, - "Name": "[CHL] Kriss_Nemesis", - "Ping": 52, - "Score": 2489, - "StatsId": 536870912 - }, - { - "Id": 2265, - "Name": "[ECU] B[4]CK^", - "Ping": 128, - "Score": 2485, - "StatsId": 536870912 - }, - { - "Id": 2094, - "Name": "[PER] FabricioHC", + "Id": 1403, + "Name": "Wev", "Ping": 76, - "Score": 1805, - "StatsId": 536870912 - }, - { - "Id": 2080, - "Name": "[BOL] m21", - "Ping": 100, - "Score": 2295, + "Score": 525148, "StatsId": 536870912 }, { - "Id": 1534, - "Name": "[BRA] BMTVanDorst", - "Ping": 12, - "Score": 1569, + "Id": 1131, + "Name": "Ahoris_.i.", + "Ping": 92, + "Score": 3529808, "StatsId": 536870912 }, { - "Id": 1146, - "Name": "[BRA] Machado_Halloween", - "Ping": 4, - "Score": 1799, + "Id": 857, + "Name": "ForgotteN`", + "Ping": 60, + "Score": 4505621, "StatsId": 536870912 }, { - "Id": 1106, - "Name": "[PER] berby_de_bajoterra__", - "Ping": 140, - "Score": 1557, + "Id": 773, + "Name": "Toshka", + "Ping": 68, + "Score": 3568153, "StatsId": 536870912 }, { - "Id": 822, - "Name": "[ESP] HomerO", - "Ping": 188, - "Score": 6849, + "Id": 729, + "Name": "Jorji", + "Ping": 96, + "Score": 3940751, "StatsId": 536870912 }, { - "Id": 730, - "Name": "[COL] CACHONDOTAN", - "Ping": 160, - "Score": 2790, + "Id": 713, + "Name": "Kebabas", + "Ping": 48, + "Score": 3993232, "StatsId": 536870912 }, { - "Id": 457, - "Name": "[CHL] DAFTY", - "Ping": 48, - "Score": 2232, - "StatsId": 536870912 + "Id": 484, + "Name": "DS", + "Ping": 80, + "Score": 4811738, + "StatsId": 0 }, { - "Id": 355, - "Name": "[BRA] Scarphan", - "Ping": 16, - "Score": 799, + "Id": 483, + "Name": "johnxina", + "Ping": 36, + "Score": 47930368, "StatsId": 536870912 }, { - "Id": 354, - "Name": "[PER] El_Azothe", - "Ping": 160, - "Score": 6122, + "Id": 214, + "Name": "Dhaos", + "Ping": 168, + "Score": 1080845312, "StatsId": 536870912 }, { - "Id": 171, - "Name": "[ARG] Green", - "Ping": 40, - "Score": 7425, + "Id": 2, + "Name": "kaziukaass", + "Ping": 36, + "Score": 47753656, "StatsId": 536870912 } ] \ No newline at end of file diff --git a/OpenGSQTests/Results/KillingFloorTests/GetRulesTest.json b/OpenGSQTests/Results/KillingFloorTests/GetRulesTest.json index 5e1d606..38de585 100644 --- a/OpenGSQTests/Results/KillingFloorTests/GetRulesTest.json +++ b/OpenGSQTests/Results/KillingFloorTests/GetRulesTest.json @@ -1,27 +1,24 @@ { "ServerMode": "dedicated", - "AdminName": "AdminEmail", + "AdminName": "", + "AdminEmail": "kf@ws-gaming.eu", "ServerVersion": "1065", "IsVacSecured": "true", - "MaxSpectators": "6", + "MaxSpectators": "20", "MapVoting": "true", "KickVoting": "true", - "SP: Version": "750", - "SP: Min perk level": "6", - "SP: Max perk level": "99", - "SP: Num trader weapons": "140", - "SP: Perk 1": "Support Specialist", - "SP: Perk 2": "Berserker", + "Slot Machines": "Ver 200", + "Veterancy Handler": "Ver 700", + "Veterancy saving": "Enabled", + "Min perk level": "0", + "Max perk level": "60", + "Num trader weapons": "123", "Mutators": [ - "ZNBase", - "ZNBase", - "ZNBase", - "ZNBase", - "ZNBase", - "ZNBase", - "ZNBase", - "ZNBase", - "ZNBase", - "ZNBase" + "KillingFloorMut", + "SpecimenHPConfigMut", + "CleanAppIDMut", + "MutSlotMachine", + "KFShareCashMut", + "ServerPerksMut" ] } \ No newline at end of file