From 107c76bd97db863bf664fb1bd21d5638f967b4ea Mon Sep 17 00:00:00 2001 From: RestoreMonarchy <35507241+RestoreMonarchy@users.noreply.github.com> Date: Sat, 6 Jun 2020 22:57:00 +0200 Subject: [PATCH 01/10] Added Receive and Send timeouts --- .../SteamQueryNet/Services/UdpWrapper.cs | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/SteamQueryNet/SteamQueryNet/Services/UdpWrapper.cs b/SteamQueryNet/SteamQueryNet/Services/UdpWrapper.cs index 4f24a23..de9f0b3 100644 --- a/SteamQueryNet/SteamQueryNet/Services/UdpWrapper.cs +++ b/SteamQueryNet/SteamQueryNet/Services/UdpWrapper.cs @@ -1,5 +1,5 @@ using SteamQueryNet.Interfaces; - +using System; using System.Net; using System.Net.Sockets; using System.Threading.Tasks; @@ -8,46 +8,69 @@ namespace SteamQueryNet.Services { internal sealed class UdpWrapper : IUdpClient { - private readonly UdpClient _udpClient; + private readonly UdpClient udpClient; + private readonly int sendTimeout; + private readonly int receiveTimeout; public UdpWrapper(IPEndPoint localIpEndPoint, int sendTimeout, int receiveTimeout) { - _udpClient = new UdpClient(localIpEndPoint); - _udpClient.Client.SendTimeout = sendTimeout; - _udpClient.Client.ReceiveTimeout = receiveTimeout; + udpClient = new UdpClient(localIpEndPoint); + this.sendTimeout = sendTimeout; + this.receiveTimeout = receiveTimeout; } public bool IsConnected { get { - return this._udpClient.Client.Connected; + return udpClient.Client.Connected; } } public void Close() { - this._udpClient.Close(); + udpClient.Close(); } public void Connect(IPEndPoint remoteIpEndpoint) { - this._udpClient.Connect(remoteIpEndpoint); + udpClient.Connect(remoteIpEndpoint); } public void Dispose() { - this._udpClient.Dispose(); + udpClient.Dispose(); } public Task ReceiveAsync() { - return this._udpClient.ReceiveAsync(); + var asyncResult = udpClient.BeginReceive(null, null); + asyncResult.AsyncWaitHandle.WaitOne(receiveTimeout); + if (asyncResult.IsCompleted) + { + IPEndPoint remoteEP = null; + byte[] receivedData = udpClient.EndReceive(asyncResult, ref remoteEP); + return Task.FromResult(new UdpReceiveResult(receivedData, remoteEP)); + } + else + { + throw new TimeoutException(); + } } public Task SendAsync(byte[] datagram, int bytes) { - return this._udpClient.SendAsync(datagram, bytes); + var asyncResult = udpClient.BeginSend(datagram, bytes, null, null); + asyncResult.AsyncWaitHandle.WaitOne(sendTimeout); + if (asyncResult.IsCompleted) + { + int num = udpClient.EndSend(asyncResult); + return Task.FromResult(num); + } + else + { + throw new TimeoutException(); + } } } } From d5aa667628c2a9c1edfefcf17952f8748d509872 Mon Sep 17 00:00:00 2001 From: razaq Date: Thu, 13 Jan 2022 20:41:34 +0100 Subject: [PATCH 02/10] reformat, add info query challenge --- .../SteamQueryNet.Tests/ServerQueryTests.cs | 282 ++++----- .../SteamQueryNet.Tests/TestValidators.cs | 64 +-- .../SteamQueryNet/Attributes/EDFAttribute.cs | 10 +- .../Attributes/NotParsableAttribute.cs | 4 +- .../Attributes/ParseCustomAttribute.cs | 4 +- SteamQueryNet/SteamQueryNet/Enums/EDFFlags.cs | 18 +- .../SteamQueryNet/Enums/Environment.cs | 12 +- .../SteamQueryNet/Enums/ServerType.cs | 12 +- .../SteamQueryNet/Enums/ShipGameMode.cs | 18 +- SteamQueryNet/SteamQueryNet/Enums/VAC.cs | 10 +- .../SteamQueryNet/Enums/Visibility.cs | 10 +- .../SteamQueryNet/Interfaces/IServerQuery.cs | 150 ++--- .../SteamQueryNet/Interfaces/IUdpClient.cs | 16 +- .../SteamQueryNet/Models/PacketHeaders.cs | 12 +- SteamQueryNet/SteamQueryNet/Models/Player.cs | 108 ++-- .../SteamQueryNet/Models/ServerInfo.cs | 276 +++++---- .../Models/TheShip/ShipGameInfo.cs | 36 +- .../Models/TheShip/ShipPlayerDetails.cs | 28 +- SteamQueryNet/SteamQueryNet/ServerQuery.cs | 543 +++++++++--------- .../SteamQueryNet/Services/UdpWrapper.cs | 114 ++-- .../SteamQueryNet/SteamQueryNet.csproj | 3 + .../Utils/DataResolutionUtils.cs | 270 ++++----- SteamQueryNet/SteamQueryNet/Utils/Helpers.cs | 98 ++-- .../SteamQueryNet/Utils/RequestHelpers.cs | 53 +- 24 files changed, 1074 insertions(+), 1077 deletions(-) diff --git a/SteamQueryNet/SteamQueryNet.Tests/ServerQueryTests.cs b/SteamQueryNet/SteamQueryNet.Tests/ServerQueryTests.cs index 8205270..c624bbd 100644 --- a/SteamQueryNet/SteamQueryNet.Tests/ServerQueryTests.cs +++ b/SteamQueryNet/SteamQueryNet.Tests/ServerQueryTests.cs @@ -17,145 +17,145 @@ namespace SteamQueryNet.Tests { - public class ServerQueryTests - { - private const string IP_ADDRESS = "127.0.0.1"; - private const string HOST_NAME = "localhost"; - private const ushort PORT = 27015; - private byte _packetCount = 0; - private readonly IPEndPoint _localIpEndpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 0); - - [Theory] - [InlineData(IP_ADDRESS)] - [InlineData(HOST_NAME)] - public void ShouldInitializeWithProperHost(string host) - { - using (var sq = new ServerQuery(new Mock().Object, It.IsAny())) - { - sq.Connect(host, PORT); - } - } - - [Theory] - [InlineData("127.0.0.1:27015")] - [InlineData("127.0.0.1,27015")] - [InlineData("localhost:27015")] - [InlineData("localhost,27015")] - [InlineData("steam://connect/localhost:27015")] - [InlineData("steam://connect/127.0.0.1:27015")] - public void ShouldInitializeWithProperHostAndPort(string ipAndHost) - { - using (var sq = new ServerQuery(new Mock().Object, It.IsAny())) - { - sq.Connect(ipAndHost); - } - } - - [Theory] - [InlineData("invalidHost:-1")] - [InlineData("invalidHost,-1")] - [InlineData("invalidHost:65536")] - [InlineData("invalidHost,65536")] - [InlineData("256.256.256.256:-1")] - [InlineData("256.256.256.256,-1")] - [InlineData("256.256.256.256:65536")] - [InlineData("256.256.256.256,65536")] - public void ShouldNotInitializeWithAnInvalidHostAndPort(string invalidHost) - { - Assert.Throws(() => - { - using (var sq = new ServerQuery(new Mock().Object, It.IsAny())) - { - sq.Connect(invalidHost); - } - }); - } - - [Fact] - public void GetServerInfo_ShouldPopulateCorrectServerInfo() - { - (byte[] responsePacket, object responseObject) = ResponseHelper.GetValidResponse(ResponseHelper.ServerInfo); - var expectedObject = (ServerInfo)responseObject; - - byte[][] requestPackets = new byte[][] { RequestHelpers.PrepareAS2_INFO_Request() }; - byte[][] responsePackets = new byte[][] { responsePacket }; - - Mock udpClientMock = SetupReceiveResponse(responsePackets); - SetupRequestCompare(requestPackets, udpClientMock); - - using (var sq = new ServerQuery(udpClientMock.Object, _localIpEndpoint)) - { - Assert.Equal(JsonConvert.SerializeObject(expectedObject), JsonConvert.SerializeObject(sq.GetServerInfo())); - } - } - - [Fact] - public void GetPlayers_ShouldPopulateCorrectPlayers() - { - (byte[] playersPacket, object responseObject) = ResponseHelper.GetValidResponse(ResponseHelper.GetPlayers); - var expectedObject = (List)responseObject; - - byte[] challengePacket = RequestHelpers.PrepareAS2_RENEW_CHALLENGE_Request(); - - // Both requests will be executed on AS2_PLAYER since thats how you refresh challenges. - byte[][] requestPackets = new byte[][] { challengePacket, challengePacket }; - - // First response is the Challenge renewal response and the second - byte[][] responsePackets = new byte[][] { challengePacket, playersPacket }; - - Mock udpClientMock = SetupReceiveResponse(responsePackets); - SetupRequestCompare(requestPackets, udpClientMock); - - // Ayylmao it looks ugly as hell but we will improve it later on. - using (var sq = new ServerQuery(udpClientMock.Object, _localIpEndpoint)) - { - Assert.Equal(JsonConvert.SerializeObject(expectedObject), JsonConvert.SerializeObject(sq.GetPlayers())); - } - } - - /* - * We keep this test here to be able to have us a notifier when the Rules API becomes available. - * So, this is more like an integration test than an unit test. - * If this test starts to fail, we'll know that the Rules API started to respond. - */ - [Fact] - public void GetRules_ShouldThrowTimeoutException() - { - // Surf Heaven rulez. - const string trustedServer = "steam://connect/54.37.111.217:27015"; - - using (var sq = new ServerQuery()) - { - sq.Connect(trustedServer); - - // Make sure that the server is still alive. - Assert.True(sq.IsConnected); - bool responded = Task.WaitAll(new Task[] { sq.GetRulesAsync() }, 2000); - Assert.True(!responded); - } - } - - private void SetupRequestCompare(IEnumerable requestPackets, Mock udpClientMock) - { - udpClientMock - .Setup(x => x.SendAsync(It.IsAny(), It.IsAny())) - .Callback((request, length) => - { - Assert.True(TestValidators.CompareBytes(requestPackets.ElementAt(_packetCount), request)); - ++_packetCount; - }); - } - - private Mock SetupReceiveResponse(IEnumerable udpPackets) - { - var udpClientMock = new Mock(); - var setupSequence = udpClientMock.SetupSequence(x => x.ReceiveAsync()); - foreach (byte[] packet in udpPackets) - { - setupSequence = setupSequence.ReturnsAsync(new UdpReceiveResult(packet, _localIpEndpoint)); - } - - return udpClientMock; - } - } + public class ServerQueryTests + { + private const string IP_ADDRESS = "127.0.0.1"; + private const string HOST_NAME = "localhost"; + private const ushort PORT = 27015; + private byte _packetCount = 0; + private readonly IPEndPoint _localIpEndpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 0); + + [Theory] + [InlineData(IP_ADDRESS)] + [InlineData(HOST_NAME)] + public void ShouldInitializeWithProperHost(string host) + { + using (var sq = new ServerQuery(new Mock().Object, It.IsAny())) + { + sq.Connect(host, PORT); + } + } + + [Theory] + [InlineData("127.0.0.1:27015")] + [InlineData("127.0.0.1,27015")] + [InlineData("localhost:27015")] + [InlineData("localhost,27015")] + [InlineData("steam://connect/localhost:27015")] + [InlineData("steam://connect/127.0.0.1:27015")] + public void ShouldInitializeWithProperHostAndPort(string ipAndHost) + { + using (var sq = new ServerQuery(new Mock().Object, It.IsAny())) + { + sq.Connect(ipAndHost); + } + } + + [Theory] + [InlineData("invalidHost:-1")] + [InlineData("invalidHost,-1")] + [InlineData("invalidHost:65536")] + [InlineData("invalidHost,65536")] + [InlineData("256.256.256.256:-1")] + [InlineData("256.256.256.256,-1")] + [InlineData("256.256.256.256:65536")] + [InlineData("256.256.256.256,65536")] + public void ShouldNotInitializeWithAnInvalidHostAndPort(string invalidHost) + { + Assert.Throws(() => + { + using (var sq = new ServerQuery(new Mock().Object, It.IsAny())) + { + sq.Connect(invalidHost); + } + }); + } + + [Fact] + public void GetServerInfo_ShouldPopulateCorrectServerInfo() + { + (byte[] responsePacket, object responseObject) = ResponseHelper.GetValidResponse(ResponseHelper.ServerInfo); + var expectedObject = (ServerInfo)responseObject; + + byte[][] requestPackets = new byte[][] { RequestHelpers.PrepareAS2_INFO_Request(0) }; + byte[][] responsePackets = new byte[][] { responsePacket }; + + Mock udpClientMock = SetupReceiveResponse(responsePackets); + SetupRequestCompare(requestPackets, udpClientMock); + + using (var sq = new ServerQuery(udpClientMock.Object, _localIpEndpoint)) + { + Assert.Equal(JsonConvert.SerializeObject(expectedObject), JsonConvert.SerializeObject(sq.GetServerInfo())); + } + } + + [Fact] + public void GetPlayers_ShouldPopulateCorrectPlayers() + { + (byte[] playersPacket, object responseObject) = ResponseHelper.GetValidResponse(ResponseHelper.GetPlayers); + var expectedObject = (List)responseObject; + + byte[] challengePacket = RequestHelpers.PrepareAS2_RENEW_CHALLENGE_Request(); + + // Both requests will be executed on AS2_PLAYER since thats how you refresh challenges. + byte[][] requestPackets = new byte[][] { challengePacket, challengePacket }; + + // First response is the Challenge renewal response and the second + byte[][] responsePackets = new byte[][] { challengePacket, playersPacket }; + + Mock udpClientMock = SetupReceiveResponse(responsePackets); + SetupRequestCompare(requestPackets, udpClientMock); + + // Ayylmao it looks ugly as hell but we will improve it later on. + using (var sq = new ServerQuery(udpClientMock.Object, _localIpEndpoint)) + { + Assert.Equal(JsonConvert.SerializeObject(expectedObject), JsonConvert.SerializeObject(sq.GetPlayers())); + } + } + + /* + * We keep this test here to be able to have us a notifier when the Rules API becomes available. + * So, this is more like an integration test than an unit test. + * If this test starts to fail, we'll know that the Rules API started to respond. + */ + [Fact] + public void GetRules_ShouldThrowTimeoutException() + { + // Surf Heaven rulez. + const string trustedServer = "steam://connect/54.37.111.217:27015"; + + using (var sq = new ServerQuery()) + { + sq.Connect(trustedServer); + + // Make sure that the server is still alive. + Assert.True(sq.IsConnected); + bool responded = Task.WaitAll(new Task[] { sq.GetRulesAsync() }, 2000); + Assert.True(!responded); + } + } + + private void SetupRequestCompare(IEnumerable requestPackets, Mock udpClientMock) + { + udpClientMock + .Setup(x => x.SendAsync(It.IsAny(), It.IsAny())) + .Callback((request, length) => + { + Assert.True(TestValidators.CompareBytes(requestPackets.ElementAt(_packetCount), request)); + ++_packetCount; + }); + } + + private Mock SetupReceiveResponse(IEnumerable udpPackets) + { + var udpClientMock = new Mock(); + var setupSequence = udpClientMock.SetupSequence(x => x.ReceiveAsync()); + foreach (byte[] packet in udpPackets) + { + setupSequence = setupSequence.ReturnsAsync(new UdpReceiveResult(packet, _localIpEndpoint)); + } + + return udpClientMock; + } + } } diff --git a/SteamQueryNet/SteamQueryNet.Tests/TestValidators.cs b/SteamQueryNet/SteamQueryNet.Tests/TestValidators.cs index b9e1325..0e5f6be 100644 --- a/SteamQueryNet/SteamQueryNet.Tests/TestValidators.cs +++ b/SteamQueryNet/SteamQueryNet.Tests/TestValidators.cs @@ -1,39 +1,39 @@ namespace SteamQueryNet.Tests { - internal static class TestValidators - { - public static bool CompareBytes(byte[] source1, byte[] source2) - { - // Lets check their refs first. - if (source1 == source2) - { - // yay. - return true; - } + internal static class TestValidators + { + public static bool CompareBytes(byte[] source1, byte[] source2) + { + // Lets check their refs first. + if (source1 == source2) + { + // yay. + return true; + } - // Check their operability. - if (source1 == null || source2 == null) - { - // Consider: Maybe we should throw an exception here. - return false; - } + // Check their operability. + if (source1 == null || source2 == null) + { + // Consider: Maybe we should throw an exception here. + return false; + } - // They are not even same length lul. - if (source1.Length != source1.Length) - { - return false; - } + // They are not even same length lul. + if (source1.Length != source1.Length) + { + return false; + } - // Byte by byte comparison intensifies. - for (int i = 0; i < source1.Length; ++i) - { - if (source1[i] != source2[i]) - { - return false; - } - } + // Byte by byte comparison intensifies. + for (int i = 0; i < source1.Length; ++i) + { + if (source1[i] != source2[i]) + { + return false; + } + } - return true; - } - } + return true; + } + } } diff --git a/SteamQueryNet/SteamQueryNet/Attributes/EDFAttribute.cs b/SteamQueryNet/SteamQueryNet/Attributes/EDFAttribute.cs index a263b70..b59cba6 100644 --- a/SteamQueryNet/SteamQueryNet/Attributes/EDFAttribute.cs +++ b/SteamQueryNet/SteamQueryNet/Attributes/EDFAttribute.cs @@ -2,9 +2,9 @@ namespace SteamQueryNet.Attributes { - [AttributeUsage(AttributeTargets.Property)] - internal sealed class EDFAttribute : Attribute - { - internal EDFAttribute(byte condition) { } - } + [AttributeUsage(AttributeTargets.Property)] + internal sealed class EDFAttribute : Attribute + { + internal EDFAttribute(byte condition) { } + } } diff --git a/SteamQueryNet/SteamQueryNet/Attributes/NotParsableAttribute.cs b/SteamQueryNet/SteamQueryNet/Attributes/NotParsableAttribute.cs index 6079c4d..dc34f01 100644 --- a/SteamQueryNet/SteamQueryNet/Attributes/NotParsableAttribute.cs +++ b/SteamQueryNet/SteamQueryNet/Attributes/NotParsableAttribute.cs @@ -2,6 +2,6 @@ namespace SteamQueryNet.Attributes { - [AttributeUsage(AttributeTargets.Property)] - internal sealed class NotParsableAttribute : Attribute { } + [AttributeUsage(AttributeTargets.Property)] + internal sealed class NotParsableAttribute : Attribute { } } diff --git a/SteamQueryNet/SteamQueryNet/Attributes/ParseCustomAttribute.cs b/SteamQueryNet/SteamQueryNet/Attributes/ParseCustomAttribute.cs index c1b2851..9cf789b 100644 --- a/SteamQueryNet/SteamQueryNet/Attributes/ParseCustomAttribute.cs +++ b/SteamQueryNet/SteamQueryNet/Attributes/ParseCustomAttribute.cs @@ -2,6 +2,6 @@ namespace SteamQueryNet.Attributes { - [AttributeUsage(AttributeTargets.Property)] - internal sealed class ParseCustomAttribute : Attribute { } + [AttributeUsage(AttributeTargets.Property)] + internal sealed class ParseCustomAttribute : Attribute { } } diff --git a/SteamQueryNet/SteamQueryNet/Enums/EDFFlags.cs b/SteamQueryNet/SteamQueryNet/Enums/EDFFlags.cs index 672e314..2eeb59c 100644 --- a/SteamQueryNet/SteamQueryNet/Enums/EDFFlags.cs +++ b/SteamQueryNet/SteamQueryNet/Enums/EDFFlags.cs @@ -1,12 +1,12 @@ namespace SteamQueryNet.Enums { - public enum EDFFlags : byte - { - Port = 0x80, - SteamID = 0x10, - SourceTVPort = 0x40, - SourceTVServerName = 0x40, - Keywords = 0x20, - GameID = 0x01 - } + public enum EDFFlags : byte + { + Port = 0x80, + SteamID = 0x10, + SourceTVPort = 0x40, + SourceTVServerName = 0x40, + Keywords = 0x20, + GameID = 0x01 + } } diff --git a/SteamQueryNet/SteamQueryNet/Enums/Environment.cs b/SteamQueryNet/SteamQueryNet/Enums/Environment.cs index 5073505..e2c063c 100644 --- a/SteamQueryNet/SteamQueryNet/Enums/Environment.cs +++ b/SteamQueryNet/SteamQueryNet/Enums/Environment.cs @@ -1,9 +1,9 @@ namespace SteamQueryNet.Enums { - public enum ServerEnvironment : byte - { - Linux = (byte)'l', - Windows = (byte)'w', - Mac = (byte)'m' - } + public enum ServerEnvironment : byte + { + Linux = (byte)'l', + Windows = (byte)'w', + Mac = (byte)'m' + } } diff --git a/SteamQueryNet/SteamQueryNet/Enums/ServerType.cs b/SteamQueryNet/SteamQueryNet/Enums/ServerType.cs index b732c39..8c887c1 100644 --- a/SteamQueryNet/SteamQueryNet/Enums/ServerType.cs +++ b/SteamQueryNet/SteamQueryNet/Enums/ServerType.cs @@ -1,9 +1,9 @@ namespace SteamQueryNet.Enums { - public enum ServerType : byte - { - Dedicated = (byte)'d', - NonDedicated = (byte)'l', - SourceTVRelay = (byte)'p' - } + public enum ServerType : byte + { + Dedicated = (byte)'d', + NonDedicated = (byte)'l', + SourceTVRelay = (byte)'p' + } } diff --git a/SteamQueryNet/SteamQueryNet/Enums/ShipGameMode.cs b/SteamQueryNet/SteamQueryNet/Enums/ShipGameMode.cs index 06724cd..a82ebfb 100644 --- a/SteamQueryNet/SteamQueryNet/Enums/ShipGameMode.cs +++ b/SteamQueryNet/SteamQueryNet/Enums/ShipGameMode.cs @@ -1,12 +1,12 @@ namespace SteamQueryNet.Enums { - public enum ShipGameMode : byte - { - Hunt = 0, - Elimination = 1, - Duel = 2, - Deathmatch = 3, - VIPTeam = 4, - TeamElimination = 5 - } + public enum ShipGameMode : byte + { + Hunt = 0, + Elimination = 1, + Duel = 2, + Deathmatch = 3, + VIPTeam = 4, + TeamElimination = 5 + } } diff --git a/SteamQueryNet/SteamQueryNet/Enums/VAC.cs b/SteamQueryNet/SteamQueryNet/Enums/VAC.cs index 303596c..a3357f4 100644 --- a/SteamQueryNet/SteamQueryNet/Enums/VAC.cs +++ b/SteamQueryNet/SteamQueryNet/Enums/VAC.cs @@ -1,8 +1,8 @@ namespace SteamQueryNet.Enums { - public enum VAC : byte - { - Unsecured = 0, - Secured = 1 - } + public enum VAC : byte + { + Unsecured = 0, + Secured = 1 + } } diff --git a/SteamQueryNet/SteamQueryNet/Enums/Visibility.cs b/SteamQueryNet/SteamQueryNet/Enums/Visibility.cs index c310532..1c881f9 100644 --- a/SteamQueryNet/SteamQueryNet/Enums/Visibility.cs +++ b/SteamQueryNet/SteamQueryNet/Enums/Visibility.cs @@ -1,8 +1,8 @@ namespace SteamQueryNet.Enums { - public enum Visibility : byte - { - Public = 0, - Private = 1 - } + public enum Visibility : byte + { + Public = 0, + Private = 1 + } } diff --git a/SteamQueryNet/SteamQueryNet/Interfaces/IServerQuery.cs b/SteamQueryNet/SteamQueryNet/Interfaces/IServerQuery.cs index 2745090..17b476a 100644 --- a/SteamQueryNet/SteamQueryNet/Interfaces/IServerQuery.cs +++ b/SteamQueryNet/SteamQueryNet/Interfaces/IServerQuery.cs @@ -6,90 +6,90 @@ namespace SteamQueryNet.Interfaces { - public interface IServerQuery - { - /// - /// Renews the server challenge code of the ServerQuery instance in order to be able to execute further operations. - /// - /// The new created challenge. - int RenewChallenge(); + public interface IServerQuery + { + /// + /// Renews the server challenge code of the ServerQuery instance in order to be able to execute further operations. + /// + /// The new created challenge. + int RenewChallenge(); - /// - /// Renews the server challenge code of the ServerQuery instance in order to be able to execute further operations. - /// - /// The new created challenge. - Task RenewChallengeAsync(); + /// + /// Renews the server challenge code of the ServerQuery instance in order to be able to execute further operations. + /// + /// The new created challenge. + Task RenewChallengeAsync(); - /// - /// Configures and Connects the created instance of SteamQuery UDP socket for Steam Server Query Operations. - /// - /// IPAddress or HostName of the server that queries will be sent. - /// Port of the server that queries will be sent. - /// Connected instance of ServerQuery. - IServerQuery Connect(string serverAddress, ushort port); + /// + /// Configures and Connects the created instance of SteamQuery UDP socket for Steam Server Query Operations. + /// + /// IPAddress or HostName of the server that queries will be sent. + /// Port of the server that queries will be sent. + /// Connected instance of ServerQuery. + IServerQuery Connect(string serverAddress, ushort port); - /// - /// Configures and Connects the created instance of SteamQuery UDP socket for Steam Server Query Operations. - /// - /// IPAddress or HostName of the server and port separated by a colon(:) or a comma(,). - /// Connected instance of ServerQuery. - IServerQuery Connect(string serverAddressAndPort); + /// + /// Configures and Connects the created instance of SteamQuery UDP socket for Steam Server Query Operations. + /// + /// IPAddress or HostName of the server and port separated by a colon(:) or a comma(,). + /// Connected instance of ServerQuery. + IServerQuery Connect(string serverAddressAndPort); - /// - /// Configures and Connects the created instance of SteamQuery UDP socket for Steam Server Query Operations. - /// - /// Desired local IPEndpoint to bound. - /// IPAddress or HostName of the server and port separated by a colon(:) or a comma(,). - /// Connected instance of ServerQuery. - IServerQuery Connect(IPEndPoint customLocalIPEndpoint, string serverAddressAndPort); + /// + /// Configures and Connects the created instance of SteamQuery UDP socket for Steam Server Query Operations. + /// + /// Desired local IPEndpoint to bound. + /// IPAddress or HostName of the server and port separated by a colon(:) or a comma(,). + /// Connected instance of ServerQuery. + IServerQuery Connect(IPEndPoint customLocalIPEndpoint, string serverAddressAndPort); - /// - /// Configures and Connects the created instance of SteamQuery UDP socket for Steam Server Query Operations. - /// - /// Desired local IPEndpoint to bound. - /// IPAddress or HostName of the server that queries will be sent. - /// Port of the server that queries will be sent. - /// Connected instance of ServerQuery. - IServerQuery Connect(IPEndPoint customLocalIPEndpoint, string serverAddress, ushort port); + /// + /// Configures and Connects the created instance of SteamQuery UDP socket for Steam Server Query Operations. + /// + /// Desired local IPEndpoint to bound. + /// IPAddress or HostName of the server that queries will be sent. + /// Port of the server that queries will be sent. + /// Connected instance of ServerQuery. + IServerQuery Connect(IPEndPoint customLocalIPEndpoint, string serverAddress, ushort port); - /// - /// Requests and serializes the server information. - /// - /// Serialized ServerInfo instance. - ServerInfo GetServerInfo(); + /// + /// Requests and serializes the server information. + /// + /// Serialized ServerInfo instance. + ServerInfo GetServerInfo(); - /// - /// Requests and serializes the server information. - /// - /// Serialized ServerInfo instance. - Task GetServerInfoAsync(); + /// + /// Requests and serializes the server information. + /// + /// Serialized ServerInfo instance. + Task GetServerInfoAsync(); - /// - /// Requests and serializes the list of player information. - /// - /// Serialized list of Player instances. - List GetPlayers(); + /// + /// Requests and serializes the list of player information. + /// + /// Serialized list of Player instances. + List GetPlayers(); - /// - /// Requests and serializes the list of player information. - /// - /// Serialized list of Player instances. - Task> GetPlayersAsync(); + /// + /// Requests and serializes the list of player information. + /// + /// Serialized list of Player instances. + Task> GetPlayersAsync(); - /// - /// Requests and serializes the list of rules defined by the server. - /// Warning: CS:GO Rules reply is broken since update CSGO 1.32.3.0 (Feb 21, 2014). - /// Before the update rules got truncated when exceeding MTU, after the update rules reply is not sent at all. - /// - /// Serialized list of Rule instances. - List GetRules(); + /// + /// Requests and serializes the list of rules defined by the server. + /// Warning: CS:GO Rules reply is broken since update CSGO 1.32.3.0 (Feb 21, 2014). + /// Before the update rules got truncated when exceeding MTU, after the update rules reply is not sent at all. + /// + /// Serialized list of Rule instances. + List GetRules(); - /// - /// Requests and serializes the list of rules defined by the server. - /// Warning: CS:GO Rules reply is broken since update CSGO 1.32.3.0 (Feb 21, 2014). - /// Before the update rules got truncated when exceeding MTU, after the update rules reply is not sent at all. - /// - /// Serialized list of Rule instances. - Task> GetRulesAsync(); - } + /// + /// Requests and serializes the list of rules defined by the server. + /// Warning: CS:GO Rules reply is broken since update CSGO 1.32.3.0 (Feb 21, 2014). + /// Before the update rules got truncated when exceeding MTU, after the update rules reply is not sent at all. + /// + /// Serialized list of Rule instances. + Task> GetRulesAsync(); + } } diff --git a/SteamQueryNet/SteamQueryNet/Interfaces/IUdpClient.cs b/SteamQueryNet/SteamQueryNet/Interfaces/IUdpClient.cs index 10d006f..91e54e8 100644 --- a/SteamQueryNet/SteamQueryNet/Interfaces/IUdpClient.cs +++ b/SteamQueryNet/SteamQueryNet/Interfaces/IUdpClient.cs @@ -5,16 +5,16 @@ namespace SteamQueryNet.Interfaces { - public interface IUdpClient : IDisposable - { - bool IsConnected { get; } + public interface IUdpClient : IDisposable + { + bool IsConnected { get; } - void Close(); + void Close(); - void Connect(IPEndPoint remoteIpEndpoint); + void Connect(IPEndPoint remoteIpEndpoint); - Task SendAsync(byte[] datagram, int bytes); + Task SendAsync(byte[] datagram, int bytes); - Task ReceiveAsync(); - } + Task ReceiveAsync(); + } } diff --git a/SteamQueryNet/SteamQueryNet/Models/PacketHeaders.cs b/SteamQueryNet/SteamQueryNet/Models/PacketHeaders.cs index 34b895a..37a3e6c 100644 --- a/SteamQueryNet/SteamQueryNet/Models/PacketHeaders.cs +++ b/SteamQueryNet/SteamQueryNet/Models/PacketHeaders.cs @@ -1,11 +1,11 @@ namespace SteamQueryNet.Models { - internal sealed class RequestHeaders - { - public const byte A2S_INFO = 0x54; + internal sealed class RequestHeaders + { + public const byte A2S_INFO = 0x54; - public const byte A2S_PLAYER = 0x55; + public const byte A2S_PLAYER = 0x55; - public const byte A2S_RULES = 0x56; - } + public const byte A2S_RULES = 0x56; + } } diff --git a/SteamQueryNet/SteamQueryNet/Models/Player.cs b/SteamQueryNet/SteamQueryNet/Models/Player.cs index 0a94c81..c87542b 100644 --- a/SteamQueryNet/SteamQueryNet/Models/Player.cs +++ b/SteamQueryNet/SteamQueryNet/Models/Player.cs @@ -5,58 +5,58 @@ namespace SteamQueryNet.Models { - public class Player - { - /// - /// Index of player chunk starting from 0. - /// - public byte Index { get; set; } - - /// - /// Name of the player. - /// - public string Name { get; set; } - - /// - /// Player's score (usually "frags" or "kills".) - /// - public int Score { get; set; } - - /// - /// Time (in seconds) player has been connected to the server. - /// - public float Duration { get; set; } - - /// - /// Total time as Hours:Minutes:Seconds format. - /// - [NotParsable] - public string TotalDurationAsString - { - get - { - TimeSpan totalSpan = TimeSpan.FromSeconds(Duration); - string parsedHours = totalSpan.Hours >= 10 - ? totalSpan.Hours.ToString() - : $"0{totalSpan.Hours}"; - - string parsedMinutes = totalSpan.Minutes >= 10 - ? totalSpan.Minutes.ToString() - : $"0{totalSpan.Minutes}"; - - string parsedSeconds = totalSpan.Seconds >= 10 - ? totalSpan.Seconds.ToString() - : $"0{totalSpan.Seconds}"; - - return $"{parsedHours}:{parsedMinutes}:{parsedSeconds}"; - } - } - - /// - /// The Ship additional player info. - /// - /// Warning: this property information is not supported by SteamQueryNet yet. - [ParseCustom] - public ShipPlayerDetails ShipPlayerDetails { get; set; } - } + public class Player + { + /// + /// Index of player chunk starting from 0. + /// + public byte Index { get; set; } + + /// + /// Name of the player. + /// + public string Name { get; set; } + + /// + /// Player's score (usually "frags" or "kills".) + /// + public int Score { get; set; } + + /// + /// Time (in seconds) player has been connected to the server. + /// + public float Duration { get; set; } + + /// + /// Total time as Hours:Minutes:Seconds format. + /// + [NotParsable] + public string TotalDurationAsString + { + get + { + TimeSpan totalSpan = TimeSpan.FromSeconds(Duration); + string parsedHours = totalSpan.Hours >= 10 + ? totalSpan.Hours.ToString() + : $"0{totalSpan.Hours}"; + + string parsedMinutes = totalSpan.Minutes >= 10 + ? totalSpan.Minutes.ToString() + : $"0{totalSpan.Minutes}"; + + string parsedSeconds = totalSpan.Seconds >= 10 + ? totalSpan.Seconds.ToString() + : $"0{totalSpan.Seconds}"; + + return $"{parsedHours}:{parsedMinutes}:{parsedSeconds}"; + } + } + + /// + /// The Ship additional player info. + /// + /// Warning: this property information is not supported by SteamQueryNet yet. + [ParseCustom] + public ShipPlayerDetails ShipPlayerDetails { get; set; } + } } diff --git a/SteamQueryNet/SteamQueryNet/Models/ServerInfo.cs b/SteamQueryNet/SteamQueryNet/Models/ServerInfo.cs index 3b68730..0a25981 100644 --- a/SteamQueryNet/SteamQueryNet/Models/ServerInfo.cs +++ b/SteamQueryNet/SteamQueryNet/Models/ServerInfo.cs @@ -4,145 +4,139 @@ namespace SteamQueryNet.Models { - public class ServerInfo - { - /// - /// Protocol version used by the server. - /// - public byte Protocol { get; set; } - - /// - /// Name of the server. - /// - public string Name { get; set; } - - /// - /// Map the server has currently loaded. - /// - public string Map { get; set; } - - /// - /// Name of the folder containing the game files. - /// - public string Folder { get; set; } - - /// - /// Full name of the game. - /// - public string Game { get; set; } - - /// - /// Steam Application ID of game. - /// - public short ID { get; set; } - - /// - /// Number of players on the server. - /// - - private byte _players; - public byte Players - { - get - { - // Some servers send bots as players. We don't want that here. - return (byte)(this._players - this.Bots); - } - set - { - this._players = value; - } - } - - /// - /// Maximum number of players the server reports it can hold. - /// - public byte MaxPlayers { get; set; } - - /// - /// Number of bots on the server. - /// - public byte Bots { get; set; } - - /// - /// Indicates the type of server. - /// - public ServerType ServerType { get; set; } - - /// - /// Indicates the operating system of the server. - /// - public ServerEnvironment Environment { get; set; } - - /// - /// Indicates whether the server requires a password. - /// - public Visibility Visibility { get; set; } - - /// - /// Specifies whether the server uses VAC. - /// - public VAC VAC { get; set; } - - /// - /// This property only exist in a response if the server is running The Ship. - /// Warning: this property information is not supported by SteamQueryNet yet. - /// - [ParseCustom] - public ShipGameInfo ShipGameInfo { get; set; } - - /// - /// Version of the game installed on the server. - /// - public string Version { get; set; } - - /// - /// If present, this specifies which additional data fields will be included. - /// - public byte EDF { get; set; } - - /// - /// The server's game port number. - /// - [EDF((byte)EDFFlags.Port)] - public short Port { get; set; } - - /// - /// Server's SteamID. - /// - [EDF((byte)EDFFlags.SteamID)] - public long SteamID { get; set; } - - /// - /// Spectator port number for SourceTV. - /// - [EDF((byte)EDFFlags.SourceTVPort)] - public short SourceTVPort { get; set; } - - /// - /// Name of the spectator server for SourceTV. - /// - [EDF((byte)EDFFlags.SourceTVServerName)] - public string SourceTVServerName { get; set; } - - /// - /// Tags that describe the game according to the server (for future use.) - /// - [EDF((byte)EDFFlags.Keywords)] - public string Keywords { get; set; } - - /// - /// The server's 64-bit GameID. If this is present, a more accurate AppID is present in the low 24 bits. - /// The earlier AppID could have been truncated as it was forced into 16-bit storage. - /// - [EDF((byte)EDFFlags.GameID)] - public long GameID { get; set; } - - /// - /// Calculated roundtrip time of the server. - /// Warning: this value will be calculated by SteamQueryNet instead of steam itself. - /// - [NotParsable] - public long Ping { get; set; } - } + public class ServerInfo + { + /// + /// Protocol version used by the server. + /// + public byte Protocol { get; set; } + + /// + /// Name of the server. + /// + public string Name { get; set; } + + /// + /// Map the server has currently loaded. + /// + public string Map { get; set; } + + /// + /// Name of the folder containing the game files. + /// + public string Folder { get; set; } + + /// + /// Full name of the game. + /// + public string Game { get; set; } + + /// + /// Steam Application ID of game. + /// + public short ID { get; set; } + + /// + /// Number of players on the server. + /// + + private byte m_players; + public byte Players + { + // Some servers send bots as players. We don't want that here. + get => (byte)(m_players - Bots); + set => m_players = value; + } + + /// + /// Maximum number of players the server reports it can hold. + /// + public byte MaxPlayers { get; set; } + + /// + /// Number of bots on the server. + /// + public byte Bots { get; set; } + + /// + /// Indicates the type of server. + /// + public ServerType ServerType { get; set; } + + /// + /// Indicates the operating system of the server. + /// + public ServerEnvironment Environment { get; set; } + + /// + /// Indicates whether the server requires a password. + /// + public Visibility Visibility { get; set; } + + /// + /// Specifies whether the server uses VAC. + /// + public VAC VAC { get; set; } + + /// + /// This property only exist in a response if the server is running The Ship. + /// Warning: this property information is not supported by SteamQueryNet yet. + /// + [ParseCustom] + public ShipGameInfo ShipGameInfo { get; set; } + + /// + /// Version of the game installed on the server. + /// + public string Version { get; set; } + + /// + /// If present, this specifies which additional data fields will be included. + /// + public byte EDF { get; set; } + + /// + /// The server's game port number. + /// + [EDF((byte)EDFFlags.Port)] + public short Port { get; set; } + + /// + /// Server's SteamID. + /// + [EDF((byte)EDFFlags.SteamID)] + public long SteamID { get; set; } + + /// + /// Spectator port number for SourceTV. + /// + [EDF((byte)EDFFlags.SourceTVPort)] + public short SourceTVPort { get; set; } + + /// + /// Name of the spectator server for SourceTV. + /// + [EDF((byte)EDFFlags.SourceTVServerName)] + public string SourceTVServerName { get; set; } + + /// + /// Tags that describe the game according to the server (for future use.) + /// + [EDF((byte)EDFFlags.Keywords)] + public string Keywords { get; set; } + + /// + /// The server's 64-bit GameID. If this is present, a more accurate AppID is present in the low 24 bits. + /// The earlier AppID could have been truncated as it was forced into 16-bit storage. + /// + [EDF((byte)EDFFlags.GameID)] + public long GameID { get; set; } + + /// + /// Calculated roundtrip time of the server. + /// Warning: this value will be calculated by SteamQueryNet instead of steam itself. + /// + [NotParsable] + public long Ping { get; set; } + } } diff --git a/SteamQueryNet/SteamQueryNet/Models/TheShip/ShipGameInfo.cs b/SteamQueryNet/SteamQueryNet/Models/TheShip/ShipGameInfo.cs index 364253b..67fb320 100644 --- a/SteamQueryNet/SteamQueryNet/Models/TheShip/ShipGameInfo.cs +++ b/SteamQueryNet/SteamQueryNet/Models/TheShip/ShipGameInfo.cs @@ -2,24 +2,24 @@ namespace SteamQueryNet.Models.TheShip { - /// - /// These fields only exist in a response if the server is running The Ship. - /// - public class ShipGameInfo - { - /// - /// Indicates the game mode. - /// - public ShipGameMode Mode { get; set; } + /// + /// These fields only exist in a response if the server is running The Ship. + /// + public class ShipGameInfo + { + /// + /// Indicates the game mode. + /// + public ShipGameMode Mode { get; set; } - /// - /// The number of witnesses necessary to have a player arrested. - /// - public byte Witnesses { get; set; } + /// + /// The number of witnesses necessary to have a player arrested. + /// + public byte Witnesses { get; set; } - /// - /// Time (in seconds) before a player is arrested while being witnessed. - /// - public byte Duration { get; set; } - } + /// + /// Time (in seconds) before a player is arrested while being witnessed. + /// + public byte Duration { get; set; } + } } diff --git a/SteamQueryNet/SteamQueryNet/Models/TheShip/ShipPlayerDetails.cs b/SteamQueryNet/SteamQueryNet/Models/TheShip/ShipPlayerDetails.cs index 92db4e7..6625139 100644 --- a/SteamQueryNet/SteamQueryNet/Models/TheShip/ShipPlayerDetails.cs +++ b/SteamQueryNet/SteamQueryNet/Models/TheShip/ShipPlayerDetails.cs @@ -1,18 +1,18 @@ namespace SteamQueryNet.Models.TheShip { - /// - /// The Ship additional player info. - /// - public class ShipPlayerDetails - { - /// - /// Player's deaths. - /// - public long Deats { get; set; } + /// + /// The Ship additional player info. + /// + public class ShipPlayerDetails + { + /// + /// Player's deaths. + /// + public long Deats { get; set; } - /// - /// Player's money. - /// - public long Money { get; set; } - } + /// + /// Player's money. + /// + public long Money { get; set; } + } } diff --git a/SteamQueryNet/SteamQueryNet/ServerQuery.cs b/SteamQueryNet/SteamQueryNet/ServerQuery.cs index a59faa6..cecb03b 100644 --- a/SteamQueryNet/SteamQueryNet/ServerQuery.cs +++ b/SteamQueryNet/SteamQueryNet/ServerQuery.cs @@ -14,273 +14,278 @@ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("SteamQueryNet.Tests")] namespace SteamQueryNet { - public class ServerQuery : IServerQuery, IDisposable - { - private IPEndPoint _remoteIpEndpoint; - - private ushort _port; - private int _currentChallenge; - - internal virtual IUdpClient UdpClient { get; private set; } - - /// - /// Reflects the udp client connection state. - /// - public bool IsConnected - { - get - { - return UdpClient.IsConnected; - } - } - - /// - /// Amount of time in miliseconds to terminate send operation if the server won't respond. - /// - public int SendTimeout { get; set; } - - /// - /// Amount of time in miliseconds to terminate receive operation if the server won't respond. - /// - public int ReceiveTimeout { get; set; } - - /// - /// Creates a new instance of ServerQuery with given UDPClient and remote endpoint. - /// - /// UdpClient to communicate. - /// Remote server endpoint. - public ServerQuery(IUdpClient udpClient, IPEndPoint remoteEndpoint) - { - UdpClient = udpClient; - _remoteIpEndpoint = remoteEndpoint; - } - - /// - /// Creates a new instance of ServerQuery without UDP socket connection. - /// - public ServerQuery() { } - - /// - /// Creates a new ServerQuery instance for Steam Server Query Operations. - /// - /// IPAddress or HostName of the server that queries will be sent. - /// Port of the server that queries will be sent. - public ServerQuery(string serverAddress, ushort port) - { - PrepareAndConnect(serverAddress, port); - } - - /// - /// Creates a new ServerQuery instance for Steam Server Query Operations. - /// - /// IPAddress or HostName of the server and port separated by a colon(:) or a comma(,). - public ServerQuery(string serverAddressAndPort) - { - (string serverAddress, ushort port) = Helpers.ResolveIPAndPortFromString(serverAddressAndPort); - PrepareAndConnect(serverAddress, port); - } - - /// - /// Creates a new instance of ServerQuery with the given Local IPEndpoint. - /// - /// Desired local IPEndpoint to bound. - /// IPAddress or HostName of the server and port separated by a colon(:) or a comma(,). - public ServerQuery(IPEndPoint customLocalIPEndpoint, string serverAddressAndPort) - { - UdpClient = new UdpWrapper(customLocalIPEndpoint, SendTimeout, ReceiveTimeout); - (string serverAddress, ushort port) = Helpers.ResolveIPAndPortFromString(serverAddressAndPort); - PrepareAndConnect(serverAddress, port); - } - - /// - /// Creates a new instance of ServerQuery with the given Local IPEndpoint. - /// - /// Desired local IPEndpoint to bound. - /// IPAddress or HostName of the server that queries will be sent. - /// Port of the server that queries will be sent. - public ServerQuery(IPEndPoint customLocalIPEndpoint, string serverAddress, ushort port) - { - UdpClient = new UdpWrapper(customLocalIPEndpoint, SendTimeout, ReceiveTimeout); - PrepareAndConnect(serverAddress, port); - } - - /// - public IServerQuery Connect(string serverAddress, ushort port) - { - PrepareAndConnect(serverAddress, port); - return this; - } - - /// - public IServerQuery Connect(string serverAddressAndPort) - { - (string serverAddress, ushort port) = Helpers.ResolveIPAndPortFromString(serverAddressAndPort); - PrepareAndConnect(serverAddress, port); - return this; - } - - /// - public IServerQuery Connect(IPEndPoint customLocalIPEndpoint, string serverAddressAndPort) - { - UdpClient = new UdpWrapper(customLocalIPEndpoint, SendTimeout, ReceiveTimeout); - (string serverAddress, ushort port) = Helpers.ResolveIPAndPortFromString(serverAddressAndPort); - PrepareAndConnect(serverAddress, port); - return this; - } - - /// - public IServerQuery Connect(IPEndPoint customLocalIPEndpoint, string serverAddress, ushort port) - { - UdpClient = new UdpWrapper(customLocalIPEndpoint, SendTimeout, ReceiveTimeout); - PrepareAndConnect(serverAddress, port); - return this; - } - - /// - public async Task GetServerInfoAsync() - { - var sInfo = new ServerInfo - { - Ping = new Ping().Send(_remoteIpEndpoint.Address).RoundtripTime - }; - - byte[] response = await SendRequestAsync(RequestHelpers.PrepareAS2_INFO_Request()); - if (response.Length > 0) - { - DataResolutionUtils.ExtractData(sInfo, response, nameof(sInfo.EDF), true); - } - - return sInfo; - } - - /// - public ServerInfo GetServerInfo() - { - return Helpers.RunSync(GetServerInfoAsync); - } - - /// - public async Task RenewChallengeAsync() - { - byte[] response = await SendRequestAsync(RequestHelpers.PrepareAS2_RENEW_CHALLENGE_Request()); - if (response.Length > 0) - { - _currentChallenge = BitConverter.ToInt32(response.Skip(DataResolutionUtils.RESPONSE_CODE_INDEX).Take(sizeof(int)).ToArray(), 0); - } - - return _currentChallenge; - } - - /// - public int RenewChallenge() - { - return Helpers.RunSync(RenewChallengeAsync); - } - - /// - public async Task> GetPlayersAsync() - { - if (_currentChallenge == 0) - { - await RenewChallengeAsync(); - } - - byte[] response = await SendRequestAsync( - RequestHelpers.PrepareAS2_GENERIC_Request(RequestHeaders.A2S_PLAYER,_currentChallenge)); - - if (response.Length > 0) - { - return DataResolutionUtils.ExtractListData(response); - } - else - { - throw new InvalidOperationException("Server did not response the query"); - } - } - - /// - public List GetPlayers() - { - return Helpers.RunSync(GetPlayersAsync); - } - - /// - public async Task> GetRulesAsync() - { - if (_currentChallenge == 0) - { - await RenewChallengeAsync(); - } - - byte[] response = await SendRequestAsync( - RequestHelpers.PrepareAS2_GENERIC_Request(RequestHeaders.A2S_RULES, _currentChallenge)); - - if (response.Length > 0) - { - return DataResolutionUtils.ExtractListData(response); - } - else - { - throw new InvalidOperationException("Server did not response the query"); - } - } - - /// - public List GetRules() - { - return Helpers.RunSync(GetRulesAsync); - } - - /// - /// Disposes the object and its disposables. - /// - public void Dispose() - { - UdpClient.Close(); - UdpClient.Dispose(); - } - - private void PrepareAndConnect(string serverAddress, ushort port) - { - _port = port; - - // Try to parse the serverAddress as IP first - if (IPAddress.TryParse(serverAddress, out IPAddress parsedIpAddress)) - { - // Yep its an IP. - _remoteIpEndpoint = new IPEndPoint(parsedIpAddress, _port); - } - else - { - // Nope it might be a hostname. - try - { - IPAddress[] addresslist = Dns.GetHostAddresses(serverAddress); - if (addresslist.Length > 0) - { - // We get the first address. - _remoteIpEndpoint = new IPEndPoint(addresslist[0], _port); - } - else - { - throw new ArgumentException($"Invalid host address {serverAddress}"); - } - } - catch (SocketException ex) - { - throw new ArgumentException("Could not reach the hostname.", ex); - } - } - - UdpClient = UdpClient ?? new UdpWrapper(new IPEndPoint(IPAddress.Any, 0), SendTimeout, ReceiveTimeout); - UdpClient.Connect(_remoteIpEndpoint); - } - - private async Task SendRequestAsync(byte[] request) - { - await UdpClient.SendAsync(request, request.Length); - UdpReceiveResult result = await UdpClient.ReceiveAsync(); - return result.Buffer; - } - } + public class ServerQuery : IServerQuery, IDisposable + { + private IPEndPoint _remoteIpEndpoint; + + private ushort m_port; + private int m_currentChallenge; + + internal virtual IUdpClient UdpClient { get; private set; } + + /// + /// Reflects the udp client connection state. + /// + public bool IsConnected + { + get + { + return UdpClient.IsConnected; + } + } + + /// + /// Amount of time in miliseconds to terminate send operation if the server won't respond. + /// + public int SendTimeout { get; set; } + + /// + /// Amount of time in miliseconds to terminate receive operation if the server won't respond. + /// + public int ReceiveTimeout { get; set; } + + /// + /// Creates a new instance of ServerQuery with given UDPClient and remote endpoint. + /// + /// UdpClient to communicate. + /// Remote server endpoint. + public ServerQuery(IUdpClient udpClient, IPEndPoint remoteEndpoint) + { + UdpClient = udpClient; + _remoteIpEndpoint = remoteEndpoint; + } + + /// + /// Creates a new instance of ServerQuery without UDP socket connection. + /// + public ServerQuery() { } + + /// + /// Creates a new ServerQuery instance for Steam Server Query Operations. + /// + /// IPAddress or HostName of the server that queries will be sent. + /// Port of the server that queries will be sent. + public ServerQuery(string serverAddress, ushort port) + { + PrepareAndConnect(serverAddress, port); + } + + /// + /// Creates a new ServerQuery instance for Steam Server Query Operations. + /// + /// IPAddress or HostName of the server and port separated by a colon(:) or a comma(,). + public ServerQuery(string serverAddressAndPort) + { + (string serverAddress, ushort port) = Helpers.ResolveIPAndPortFromString(serverAddressAndPort); + PrepareAndConnect(serverAddress, port); + } + + /// + /// Creates a new instance of ServerQuery with the given Local IPEndpoint. + /// + /// Desired local IPEndpoint to bound. + /// IPAddress or HostName of the server and port separated by a colon(:) or a comma(,). + public ServerQuery(IPEndPoint customLocalIPEndpoint, string serverAddressAndPort) + { + UdpClient = new UdpWrapper(customLocalIPEndpoint, SendTimeout, ReceiveTimeout); + (string serverAddress, ushort port) = Helpers.ResolveIPAndPortFromString(serverAddressAndPort); + PrepareAndConnect(serverAddress, port); + } + + /// + /// Creates a new instance of ServerQuery with the given Local IPEndpoint. + /// + /// Desired local IPEndpoint to bound. + /// IPAddress or HostName of the server that queries will be sent. + /// Port of the server that queries will be sent. + public ServerQuery(IPEndPoint customLocalIPEndpoint, string serverAddress, ushort port) + { + UdpClient = new UdpWrapper(customLocalIPEndpoint, SendTimeout, ReceiveTimeout); + PrepareAndConnect(serverAddress, port); + } + + /// + public IServerQuery Connect(string serverAddress, ushort port) + { + PrepareAndConnect(serverAddress, port); + return this; + } + + /// + public IServerQuery Connect(string serverAddressAndPort) + { + (string serverAddress, ushort port) = Helpers.ResolveIPAndPortFromString(serverAddressAndPort); + PrepareAndConnect(serverAddress, port); + return this; + } + + /// + public IServerQuery Connect(IPEndPoint customLocalIPEndpoint, string serverAddressAndPort) + { + UdpClient = new UdpWrapper(customLocalIPEndpoint, SendTimeout, ReceiveTimeout); + (string serverAddress, ushort port) = Helpers.ResolveIPAndPortFromString(serverAddressAndPort); + PrepareAndConnect(serverAddress, port); + return this; + } + + /// + public IServerQuery Connect(IPEndPoint customLocalIPEndpoint, string serverAddress, ushort port) + { + UdpClient = new UdpWrapper(customLocalIPEndpoint, SendTimeout, ReceiveTimeout); + PrepareAndConnect(serverAddress, port); + return this; + } + + /// + public async Task GetServerInfoAsync() + { + var sInfo = new ServerInfo + { + Ping = new Ping().Send(_remoteIpEndpoint.Address)?.RoundtripTime ?? default + }; + + if (m_currentChallenge == 0) + { + await RenewChallengeAsync(); + } + + byte[] response = await SendRequestAsync(RequestHelpers.PrepareAS2_INFO_Request(m_currentChallenge)); + if (response.Length > 0) + { + DataResolutionUtils.ExtractData(sInfo, response, nameof(sInfo.EDF), true); + } + + return sInfo; + } + + /// + public ServerInfo GetServerInfo() + { + return Helpers.RunSync(GetServerInfoAsync); + } + + /// + public async Task RenewChallengeAsync() + { + byte[] response = await SendRequestAsync(RequestHelpers.PrepareAS2_RENEW_CHALLENGE_Request()); + if (response.Length > 0) + { + m_currentChallenge = BitConverter.ToInt32(response.Skip(DataResolutionUtils.RESPONSE_CODE_INDEX).Take(sizeof(int)).ToArray(), 0); + } + + return m_currentChallenge; + } + + /// + public int RenewChallenge() + { + return Helpers.RunSync(RenewChallengeAsync); + } + + /// + public async Task> GetPlayersAsync() + { + if (m_currentChallenge == 0) + { + await RenewChallengeAsync(); + } + + byte[] response = await SendRequestAsync( + RequestHelpers.PrepareAS2_GENERIC_Request(RequestHeaders.A2S_PLAYER, m_currentChallenge)); + + if (response.Length > 0) + { + return DataResolutionUtils.ExtractListData(response); + } + else + { + throw new InvalidOperationException("Server did not response the query"); + } + } + + /// + public List GetPlayers() + { + return Helpers.RunSync(GetPlayersAsync); + } + + /// + public async Task> GetRulesAsync() + { + if (m_currentChallenge == 0) + { + await RenewChallengeAsync(); + } + + byte[] response = await SendRequestAsync( + RequestHelpers.PrepareAS2_GENERIC_Request(RequestHeaders.A2S_RULES, m_currentChallenge)); + + if (response.Length > 0) + { + return DataResolutionUtils.ExtractListData(response); + } + else + { + throw new InvalidOperationException("Server did not response the query"); + } + } + + /// + public List GetRules() + { + return Helpers.RunSync(GetRulesAsync); + } + + /// + /// Disposes the object and its disposables. + /// + public void Dispose() + { + UdpClient.Close(); + UdpClient.Dispose(); + } + + private void PrepareAndConnect(string serverAddress, ushort port) + { + m_port = port; + + // Try to parse the serverAddress as IP first + if (IPAddress.TryParse(serverAddress, out IPAddress parsedIpAddress)) + { + // Yep its an IP. + _remoteIpEndpoint = new IPEndPoint(parsedIpAddress, m_port); + } + else + { + // Nope it might be a hostname. + try + { + IPAddress[] addresslist = Dns.GetHostAddresses(serverAddress); + if (addresslist.Length > 0) + { + // We get the first address. + _remoteIpEndpoint = new IPEndPoint(addresslist[0], m_port); + } + else + { + throw new ArgumentException($"Invalid host address {serverAddress}"); + } + } + catch (SocketException ex) + { + throw new ArgumentException("Could not reach the hostname.", ex); + } + } + + UdpClient = UdpClient ?? new UdpWrapper(new IPEndPoint(IPAddress.Any, 0), SendTimeout, ReceiveTimeout); + UdpClient.Connect(_remoteIpEndpoint); + } + + private async Task SendRequestAsync(byte[] request) + { + await UdpClient.SendAsync(request, request.Length); + UdpReceiveResult result = await UdpClient.ReceiveAsync(); + return result.Buffer; + } + } } diff --git a/SteamQueryNet/SteamQueryNet/Services/UdpWrapper.cs b/SteamQueryNet/SteamQueryNet/Services/UdpWrapper.cs index de9f0b3..0448318 100644 --- a/SteamQueryNet/SteamQueryNet/Services/UdpWrapper.cs +++ b/SteamQueryNet/SteamQueryNet/Services/UdpWrapper.cs @@ -6,71 +6,65 @@ namespace SteamQueryNet.Services { - internal sealed class UdpWrapper : IUdpClient - { - private readonly UdpClient udpClient; - private readonly int sendTimeout; - private readonly int receiveTimeout; + internal sealed class UdpWrapper : IUdpClient + { + private readonly UdpClient m_udpClient; + private readonly int m_sendTimeout; + private readonly int m_receiveTimeout; - public UdpWrapper(IPEndPoint localIpEndPoint, int sendTimeout, int receiveTimeout) - { - udpClient = new UdpClient(localIpEndPoint); - this.sendTimeout = sendTimeout; - this.receiveTimeout = receiveTimeout; - } + public UdpWrapper(IPEndPoint localIpEndPoint, int sendTimeout, int receiveTimeout) + { + m_udpClient = new UdpClient(localIpEndPoint); + this.m_sendTimeout = sendTimeout; + this.m_receiveTimeout = receiveTimeout; + } - public bool IsConnected - { - get - { - return udpClient.Client.Connected; - } - } + public bool IsConnected => m_udpClient.Client.Connected; - public void Close() - { - udpClient.Close(); - } + public void Close() + { + m_udpClient.Close(); + } - public void Connect(IPEndPoint remoteIpEndpoint) - { - udpClient.Connect(remoteIpEndpoint); - } + public void Connect(IPEndPoint remoteIpEndpoint) + { + m_udpClient.Connect(remoteIpEndpoint); + } - public void Dispose() - { - udpClient.Dispose(); - } + public void Dispose() + { + m_udpClient.Dispose(); + } - public Task ReceiveAsync() - { - var asyncResult = udpClient.BeginReceive(null, null); - asyncResult.AsyncWaitHandle.WaitOne(receiveTimeout); - if (asyncResult.IsCompleted) - { - IPEndPoint remoteEP = null; - byte[] receivedData = udpClient.EndReceive(asyncResult, ref remoteEP); - return Task.FromResult(new UdpReceiveResult(receivedData, remoteEP)); - } - else - { - throw new TimeoutException(); - } - } + public Task ReceiveAsync() + { + var asyncResult = m_udpClient.BeginReceive(null, null); + asyncResult.AsyncWaitHandle.WaitOne(m_receiveTimeout); + if (asyncResult.IsCompleted) + { + IPEndPoint remoteEP = null; + byte[] receivedData = m_udpClient.EndReceive(asyncResult, ref remoteEP); + return Task.FromResult(new UdpReceiveResult(receivedData, remoteEP)); + } + else + { + throw new TimeoutException(); + } + } - public Task SendAsync(byte[] datagram, int bytes) - { - var asyncResult = udpClient.BeginSend(datagram, bytes, null, null); - asyncResult.AsyncWaitHandle.WaitOne(sendTimeout); - if (asyncResult.IsCompleted) - { - int num = udpClient.EndSend(asyncResult); - return Task.FromResult(num); - } - else - { - throw new TimeoutException(); - } - } - } + public Task SendAsync(byte[] datagram, int bytes) + { + var asyncResult = m_udpClient.BeginSend(datagram, bytes, null, null); + asyncResult.AsyncWaitHandle.WaitOne(m_sendTimeout); + if (asyncResult.IsCompleted) + { + int num = m_udpClient.EndSend(asyncResult); + return Task.FromResult(num); + } + else + { + throw new TimeoutException(); + } + } + } } diff --git a/SteamQueryNet/SteamQueryNet/SteamQueryNet.csproj b/SteamQueryNet/SteamQueryNet/SteamQueryNet.csproj index 96551ea..126152d 100644 --- a/SteamQueryNet/SteamQueryNet/SteamQueryNet.csproj +++ b/SteamQueryNet/SteamQueryNet/SteamQueryNet.csproj @@ -22,6 +22,9 @@ Written in .net standard 2.0 so that it works with both .NET framework 4.6+ and https://github.com/cyilcode/SteamQueryNet git true + + Library + diff --git a/SteamQueryNet/SteamQueryNet/Utils/DataResolutionUtils.cs b/SteamQueryNet/SteamQueryNet/Utils/DataResolutionUtils.cs index 211da19..e9fbfab 100644 --- a/SteamQueryNet/SteamQueryNet/Utils/DataResolutionUtils.cs +++ b/SteamQueryNet/SteamQueryNet/Utils/DataResolutionUtils.cs @@ -9,139 +9,139 @@ namespace SteamQueryNet.Utils { - internal sealed class DataResolutionUtils - { - internal const byte RESPONSE_HEADER_COUNT = 5; - internal const byte RESPONSE_CODE_INDEX = 5; - - internal static IEnumerable ExtractData( - TObject objectRef, - byte[] dataSource, - string edfPropName = "", - bool stripHeaders = false) - where TObject : class - { - IEnumerable takenBytes = new List(); - - // We can be a good guy and ask for any extra jobs :) - IEnumerable enumerableSource = stripHeaders - ? dataSource.Skip(RESPONSE_HEADER_COUNT) - : dataSource; - - // We get every property that does not contain ParseCustom and NotParsable attributes on them to iterate through all and parse/assign their values. - IEnumerable propsOfObject = typeof(TObject).GetProperties() - .Where(x => !x.CustomAttributes.Any(y => y.AttributeType == typeof(ParseCustomAttribute) || y.AttributeType == typeof(NotParsableAttribute))); - - foreach (PropertyInfo property in propsOfObject) - { - /* Check for EDF property name, if it was provided then it mean that we have EDF properties in the model. - * You can check here: https://developer.valvesoftware.com/wiki/Server_queries#A2S_INFO to get more info about EDF's. */ - if (!string.IsNullOrEmpty(edfPropName)) - { - // Does the property have an EDFAttribute assigned ? - CustomAttributeData edfInfo = property.CustomAttributes.FirstOrDefault(x => x.AttributeType == typeof(EDFAttribute)); - if (edfInfo != null) - { - // Get the EDF value that was returned by the server. - byte edfValue = (byte)typeof(TObject).GetProperty(edfPropName).GetValue(objectRef); - - // Get the EDF condition value that was provided in the model. - byte edfPropertyConditionValue = (byte)edfInfo.ConstructorArguments[0].Value; - - // Continue if the condition does not pass because it means that the server did not include any information about this property. - if ((edfValue & edfPropertyConditionValue) <= 0) { continue; } - } - } - - /* Basic explanation of what is going of from here; - * Get the type of the property and get amount of bytes of its size from the response array, - * Convert the parsed value to its type and assign it. - */ - - /* We have to handle strings separately since their size is unknown and they are also null terminated. - * Check here: https://developer.valvesoftware.com/wiki/String for further information about Strings in the protocol. - */ - if (property.PropertyType == typeof(string)) - { - // Clear the buffer first then take till the termination. - takenBytes = enumerableSource - .SkipWhile(x => x == 0) - .TakeWhile(x => x != 0); - - // Parse it into a string. - property.SetValue(objectRef, Encoding.UTF8.GetString(takenBytes.ToArray())); - - // Update the source by skipping the amount of bytes taken from the source and + 1 for termination byte. - enumerableSource = enumerableSource.Skip(takenBytes.Count() + 1); - } - else - { - // Is the property an Enum ? if yes we should be getting the underlying type since it might differ. - Type typeOfProperty = property.PropertyType.IsEnum - ? property.PropertyType.GetEnumUnderlyingType() - : property.PropertyType; - - // Extract the value and the size from the source. - (object result, int size) = ExtractMarshalType(enumerableSource, typeOfProperty); - - /* If the property is an enum we should parse it first then assign its value, - * if not we can just give it to SetValue since it was converted by ExtractMarshalType already.*/ - property.SetValue(objectRef, property.PropertyType.IsEnum - ? Enum.Parse(property.PropertyType, result.ToString()) - : result); - - // Update the source by skipping the amount of bytes taken from the source. - enumerableSource = enumerableSource.Skip(size); - } - } - - // We return the last state of the processed source. - return enumerableSource; - } - - internal static List ExtractListData(byte[] rawSource) - where TObject : class - { - // Create a list to contain the serialized data. - var objectList = new List(); - - // Skip the response headers. - byte itemCount = rawSource[RESPONSE_CODE_INDEX]; - - // Skip +1 for item_count. - IEnumerable dataSource = rawSource.Skip(RESPONSE_HEADER_COUNT + 1); - - for (byte i = 0; i < itemCount; i++) - { - // Activate a new instance of the object. - var objectInstance = Activator.CreateInstance(); - - // Extract the data. - dataSource = ExtractData(objectInstance, dataSource.ToArray()); - - // Add it into the list. - objectList.Add(objectInstance); - } - - return objectList; - } - - internal static (object, int) ExtractMarshalType(IEnumerable source, Type type) - { - // Get the size of the given type. - int sizeOfType = Marshal.SizeOf(type); - - // Take amount of bytes from the source array. - IEnumerable takenBytes = source.Take(sizeOfType); - - // We actually need to go into an unsafe block here since as far as i know, this is the only way to convert a byte[] source into its given type on runtime. - unsafe - { - fixed (byte* sourcePtr = takenBytes.ToArray()) - { - return (Marshal.PtrToStructure(new IntPtr(sourcePtr), type), sizeOfType); - } - } - } - } + internal sealed class DataResolutionUtils + { + internal const byte RESPONSE_HEADER_COUNT = 5; + internal const byte RESPONSE_CODE_INDEX = 5; + + internal static IEnumerable ExtractData( + TObject objectRef, + byte[] dataSource, + string edfPropName = "", + bool stripHeaders = false) + where TObject : class + { + IEnumerable takenBytes = new List(); + + // We can be a good guy and ask for any extra jobs :) + IEnumerable enumerableSource = stripHeaders + ? dataSource.Skip(RESPONSE_HEADER_COUNT) + : dataSource; + + // We get every property that does not contain ParseCustom and NotParsable attributes on them to iterate through all and parse/assign their values. + IEnumerable propsOfObject = typeof(TObject).GetProperties() + .Where(x => !x.CustomAttributes.Any(y => y.AttributeType == typeof(ParseCustomAttribute) || y.AttributeType == typeof(NotParsableAttribute))); + + foreach (PropertyInfo property in propsOfObject) + { + /* Check for EDF property name, if it was provided then it mean that we have EDF properties in the model. + * You can check here: https://developer.valvesoftware.com/wiki/Server_queries#A2S_INFO to get more info about EDF's. */ + if (!string.IsNullOrEmpty(edfPropName)) + { + // Does the property have an EDFAttribute assigned ? + CustomAttributeData edfInfo = property.CustomAttributes.FirstOrDefault(x => x.AttributeType == typeof(EDFAttribute)); + if (edfInfo != null) + { + // Get the EDF value that was returned by the server. + byte edfValue = (byte)typeof(TObject).GetProperty(edfPropName).GetValue(objectRef); + + // Get the EDF condition value that was provided in the model. + byte edfPropertyConditionValue = (byte)edfInfo.ConstructorArguments[0].Value; + + // Continue if the condition does not pass because it means that the server did not include any information about this property. + if ((edfValue & edfPropertyConditionValue) <= 0) { continue; } + } + } + + /* Basic explanation of what is going of from here; + * Get the type of the property and get amount of bytes of its size from the response array, + * Convert the parsed value to its type and assign it. + */ + + /* We have to handle strings separately since their size is unknown and they are also null terminated. + * Check here: https://developer.valvesoftware.com/wiki/String for further information about Strings in the protocol. + */ + if (property.PropertyType == typeof(string)) + { + // Clear the buffer first then take till the termination. + takenBytes = enumerableSource + .SkipWhile(x => x == 0) + .TakeWhile(x => x != 0); + + // Parse it into a string. + property.SetValue(objectRef, Encoding.UTF8.GetString(takenBytes.ToArray())); + + // Update the source by skipping the amount of bytes taken from the source and + 1 for termination byte. + enumerableSource = enumerableSource.Skip(takenBytes.Count() + 1); + } + else + { + // Is the property an Enum ? if yes we should be getting the underlying type since it might differ. + Type typeOfProperty = property.PropertyType.IsEnum + ? property.PropertyType.GetEnumUnderlyingType() + : property.PropertyType; + + // Extract the value and the size from the source. + (object result, int size) = ExtractMarshalType(enumerableSource, typeOfProperty); + + /* If the property is an enum we should parse it first then assign its value, + * if not we can just give it to SetValue since it was converted by ExtractMarshalType already.*/ + property.SetValue(objectRef, property.PropertyType.IsEnum + ? Enum.Parse(property.PropertyType, result.ToString()) + : result); + + // Update the source by skipping the amount of bytes taken from the source. + enumerableSource = enumerableSource.Skip(size); + } + } + + // We return the last state of the processed source. + return enumerableSource; + } + + internal static List ExtractListData(byte[] rawSource) + where TObject : class + { + // Create a list to contain the serialized data. + var objectList = new List(); + + // Skip the response headers. + byte itemCount = rawSource[RESPONSE_CODE_INDEX]; + + // Skip +1 for item_count. + IEnumerable dataSource = rawSource.Skip(RESPONSE_HEADER_COUNT + 1); + + for (byte i = 0; i < itemCount; i++) + { + // Activate a new instance of the object. + var objectInstance = Activator.CreateInstance(); + + // Extract the data. + dataSource = ExtractData(objectInstance, dataSource.ToArray()); + + // Add it into the list. + objectList.Add(objectInstance); + } + + return objectList; + } + + internal static (object, int) ExtractMarshalType(IEnumerable source, Type type) + { + // Get the size of the given type. + int sizeOfType = Marshal.SizeOf(type); + + // Take amount of bytes from the source array. + IEnumerable takenBytes = source.Take(sizeOfType); + + // We actually need to go into an unsafe block here since as far as i know, this is the only way to convert a byte[] source into its given type on runtime. + unsafe + { + fixed (byte* sourcePtr = takenBytes.ToArray()) + { + return (Marshal.PtrToStructure(new IntPtr(sourcePtr), type), sizeOfType); + } + } + } + } } diff --git a/SteamQueryNet/SteamQueryNet/Utils/Helpers.cs b/SteamQueryNet/SteamQueryNet/Utils/Helpers.cs index 9471be2..a146b0d 100644 --- a/SteamQueryNet/SteamQueryNet/Utils/Helpers.cs +++ b/SteamQueryNet/SteamQueryNet/Utils/Helpers.cs @@ -5,60 +5,60 @@ namespace SteamQueryNet.Utils { - internal class Helpers - { - internal static TResult RunSync(Func> func) - { - var cultureUi = CultureInfo.CurrentUICulture; - var culture = CultureInfo.CurrentCulture; - return new TaskFactory().StartNew(() => - { - Thread.CurrentThread.CurrentCulture = culture; - Thread.CurrentThread.CurrentUICulture = cultureUi; - return func(); - }).Unwrap().GetAwaiter().GetResult(); - } + internal class Helpers + { + internal static TResult RunSync(Func> func) + { + var cultureUi = CultureInfo.CurrentUICulture; + var culture = CultureInfo.CurrentCulture; + return new TaskFactory().StartNew(() => + { + Thread.CurrentThread.CurrentCulture = culture; + Thread.CurrentThread.CurrentUICulture = cultureUi; + return func(); + }).Unwrap().GetAwaiter().GetResult(); + } - internal static (string serverAddress, ushort port) ResolveIPAndPortFromString(string serverAddressAndPort) - { - const string steamUrl = "steam://connect/"; - // Check for usual suspects. - if (string.IsNullOrEmpty(serverAddressAndPort)) - { - throw new ArgumentException($"Couldn't parse hostname or port with value: {serverAddressAndPort}", nameof(serverAddressAndPort)); - } + internal static (string serverAddress, ushort port) ResolveIPAndPortFromString(string serverAddressAndPort) + { + const string steamUrl = "steam://connect/"; + // Check for usual suspects. + if (string.IsNullOrEmpty(serverAddressAndPort)) + { + throw new ArgumentException($"Couldn't parse hostname or port with value: {serverAddressAndPort}", nameof(serverAddressAndPort)); + } - // Check if its a steam url. - if (serverAddressAndPort.Contains(steamUrl)) - { - // Yep lets get rid of it since we dont need it. - serverAddressAndPort = serverAddressAndPort.Replace(steamUrl, string.Empty); - } + // Check if its a steam url. + if (serverAddressAndPort.Contains(steamUrl)) + { + // Yep lets get rid of it since we dont need it. + serverAddressAndPort = serverAddressAndPort.Replace(steamUrl, string.Empty); + } - // Lets be a nice guy and clear out all possible copy paste error whitespaces. - serverAddressAndPort = serverAddressAndPort.Replace(" ", string.Empty); + // Lets be a nice guy and clear out all possible copy paste error whitespaces. + serverAddressAndPort = serverAddressAndPort.Replace(" ", string.Empty); - // Try with a colon - string[] parts = serverAddressAndPort.Split(':'); - if (parts.Length != 2) - { + // Try with a colon + string[] parts = serverAddressAndPort.Split(':'); + if (parts.Length != 2) + { - // Not a colon. Try a comma then. - parts = serverAddressAndPort.Split(','); - if (parts.Length != 2) - { - // Y u do dis ? - throw new ArgumentException($"Couldn't parse hostname or port with value: {serverAddressAndPort}", nameof(serverAddressAndPort)); - } - } + // Not a colon. Try a comma then. + parts = serverAddressAndPort.Split(','); + if (parts.Length != 2) + { + // Y u do dis ? + throw new ArgumentException($"Couldn't parse hostname or port with value: {serverAddressAndPort}", nameof(serverAddressAndPort)); + } + } - // Parse the port see if its in range. - if (!ushort.TryParse(parts[1], out ushort parsedPort)) - { - throw new ArgumentException($"Couldn't parse the port number from the parameter with value: {serverAddressAndPort}", nameof(serverAddressAndPort)); - } + // Parse the port see if its in range. + if (!ushort.TryParse(parts[1], out ushort parsedPort)) + { + throw new ArgumentException($"Couldn't parse the port number from the parameter with value: {serverAddressAndPort}", nameof(serverAddressAndPort)); + } - return (parts[0], parsedPort); - } - } + return (parts[0], parsedPort); + } + } } diff --git a/SteamQueryNet/SteamQueryNet/Utils/RequestHelpers.cs b/SteamQueryNet/SteamQueryNet/Utils/RequestHelpers.cs index 658458e..12c3ec9 100644 --- a/SteamQueryNet/SteamQueryNet/Utils/RequestHelpers.cs +++ b/SteamQueryNet/SteamQueryNet/Utils/RequestHelpers.cs @@ -1,39 +1,40 @@ using SteamQueryNet.Models; using System; +using System.Collections.Generic; using System.Linq; using System.Text; namespace SteamQueryNet.Utils { - internal sealed class RequestHelpers - { - internal static byte[] PrepareAS2_INFO_Request() - { - const string requestPayload = "Source Engine Query\0"; - return BuildRequest(RequestHeaders.A2S_INFO, Encoding.UTF8.GetBytes(requestPayload)); - } + internal sealed class RequestHelpers + { + internal static byte[] PrepareAS2_INFO_Request(int challenge) + { + const string requestPayload = "Source Engine Query\0"; + return BuildRequest(RequestHeaders.A2S_INFO, Encoding.UTF8.GetBytes(requestPayload).Concat(BitConverter.GetBytes(challenge))); + } - internal static byte[] PrepareAS2_RENEW_CHALLENGE_Request() - { - return BuildRequest(RequestHeaders.A2S_PLAYER, BitConverter.GetBytes(-1)); - } + internal static byte[] PrepareAS2_RENEW_CHALLENGE_Request() + { + return BuildRequest(RequestHeaders.A2S_PLAYER, BitConverter.GetBytes(-1)); + } - internal static byte[] PrepareAS2_GENERIC_Request(byte challengeRequestCode, int challenge) - { - return BuildRequest(challengeRequestCode, BitConverter.GetBytes(challenge)); - } + internal static byte[] PrepareAS2_GENERIC_Request(byte challengeRequestCode, int challenge) + { + return BuildRequest(challengeRequestCode, BitConverter.GetBytes(challenge)); + } - private static byte[] BuildRequest(byte headerCode, byte[] extraParams = null) - { - /* All requests consist of 4 FF's followed by a header code to execute the request. - * Check here: https://developer.valvesoftware.com/wiki/Server_queries#Protocol for further information about the protocol. */ - var request = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, headerCode }; + private static byte[] BuildRequest(byte headerCode, IEnumerable extraParams = null) + { + /* All requests consist of 4 FF's followed by a header code to execute the request. + * Check here: https://developer.valvesoftware.com/wiki/Server_queries#Protocol for further information about the protocol. */ + var request = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, headerCode }; - // If we have any extra payload, concatenate those into our requestHeaders and return; - return extraParams != null - ? request.Concat(extraParams).ToArray() - : request; - } - } + // If we have any extra payload, concatenate those into our requestHeaders and return; + return extraParams != null + ? request.Concat(extraParams).ToArray() + : request; + } + } } From f526663db6c08d4de884f05b00ba406b67c1c3c9 Mon Sep 17 00:00:00 2001 From: razaq Date: Thu, 13 Jan 2022 20:47:00 +0100 Subject: [PATCH 03/10] retarget to .NET 5 --- SteamQueryNet/SteamQueryNet.Tests/SteamQueryNet.Tests.csproj | 2 +- SteamQueryNet/SteamQueryNet/SteamQueryNet.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SteamQueryNet/SteamQueryNet.Tests/SteamQueryNet.Tests.csproj b/SteamQueryNet/SteamQueryNet.Tests/SteamQueryNet.Tests.csproj index 23f9821..de840c8 100644 --- a/SteamQueryNet/SteamQueryNet.Tests/SteamQueryNet.Tests.csproj +++ b/SteamQueryNet/SteamQueryNet.Tests/SteamQueryNet.Tests.csproj @@ -1,7 +1,7 @@ - netcoreapp2.0 + net5.0-windows false diff --git a/SteamQueryNet/SteamQueryNet/SteamQueryNet.csproj b/SteamQueryNet/SteamQueryNet/SteamQueryNet.csproj index 126152d..f9fe07c 100644 --- a/SteamQueryNet/SteamQueryNet/SteamQueryNet.csproj +++ b/SteamQueryNet/SteamQueryNet/SteamQueryNet.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + net5.0-windows7.0 Cem YILMAZ Cem YILMAZ 1.0.6 From 08cc2b137a2312f016edbc027186dc8a8e1e477b Mon Sep 17 00:00:00 2001 From: razaq Date: Thu, 13 Jan 2022 21:39:01 +0100 Subject: [PATCH 04/10] remove unusable constructors because of timeouts --- SteamQueryNet/SteamQueryNet/ServerQuery.cs | 70 ++++++---------------- 1 file changed, 18 insertions(+), 52 deletions(-) diff --git a/SteamQueryNet/SteamQueryNet/ServerQuery.cs b/SteamQueryNet/SteamQueryNet/ServerQuery.cs index cecb03b..0c946d3 100644 --- a/SteamQueryNet/SteamQueryNet/ServerQuery.cs +++ b/SteamQueryNet/SteamQueryNet/ServerQuery.cs @@ -16,7 +16,7 @@ namespace SteamQueryNet { public class ServerQuery : IServerQuery, IDisposable { - private IPEndPoint _remoteIpEndpoint; + private IPEndPoint m_remoteIpEndpoint; private ushort m_port; private int m_currentChallenge; @@ -26,21 +26,15 @@ public class ServerQuery : IServerQuery, IDisposable /// /// Reflects the udp client connection state. /// - public bool IsConnected - { - get - { - return UdpClient.IsConnected; - } - } + public bool IsConnected => UdpClient.IsConnected; /// - /// Amount of time in miliseconds to terminate send operation if the server won't respond. + /// Amount of time in milliseconds to terminate send operation if the server won't respond. /// public int SendTimeout { get; set; } /// - /// Amount of time in miliseconds to terminate receive operation if the server won't respond. + /// Amount of time in milliseconds to terminate receive operation if the server won't respond. /// public int ReceiveTimeout { get; set; } @@ -52,7 +46,7 @@ public bool IsConnected public ServerQuery(IUdpClient udpClient, IPEndPoint remoteEndpoint) { UdpClient = udpClient; - _remoteIpEndpoint = remoteEndpoint; + m_remoteIpEndpoint = remoteEndpoint; } /// @@ -65,19 +59,21 @@ public ServerQuery() { } /// /// IPAddress or HostName of the server that queries will be sent. /// Port of the server that queries will be sent. - public ServerQuery(string serverAddress, ushort port) + public IServerQuery Connect(string serverAddress, ushort port) { PrepareAndConnect(serverAddress, port); + return this; } /// /// Creates a new ServerQuery instance for Steam Server Query Operations. /// /// IPAddress or HostName of the server and port separated by a colon(:) or a comma(,). - public ServerQuery(string serverAddressAndPort) + public IServerQuery Connect(string serverAddressAndPort) { (string serverAddress, ushort port) = Helpers.ResolveIPAndPortFromString(serverAddressAndPort); PrepareAndConnect(serverAddress, port); + return this; } /// @@ -85,11 +81,12 @@ public ServerQuery(string serverAddressAndPort) /// /// Desired local IPEndpoint to bound. /// IPAddress or HostName of the server and port separated by a colon(:) or a comma(,). - public ServerQuery(IPEndPoint customLocalIPEndpoint, string serverAddressAndPort) + public IServerQuery Connect(IPEndPoint customLocalIPEndpoint, string serverAddressAndPort) { UdpClient = new UdpWrapper(customLocalIPEndpoint, SendTimeout, ReceiveTimeout); (string serverAddress, ushort port) = Helpers.ResolveIPAndPortFromString(serverAddressAndPort); PrepareAndConnect(serverAddress, port); + return this; } /// @@ -98,37 +95,6 @@ public ServerQuery(IPEndPoint customLocalIPEndpoint, string serverAddressAndPort /// Desired local IPEndpoint to bound. /// IPAddress or HostName of the server that queries will be sent. /// Port of the server that queries will be sent. - public ServerQuery(IPEndPoint customLocalIPEndpoint, string serverAddress, ushort port) - { - UdpClient = new UdpWrapper(customLocalIPEndpoint, SendTimeout, ReceiveTimeout); - PrepareAndConnect(serverAddress, port); - } - - /// - public IServerQuery Connect(string serverAddress, ushort port) - { - PrepareAndConnect(serverAddress, port); - return this; - } - - /// - public IServerQuery Connect(string serverAddressAndPort) - { - (string serverAddress, ushort port) = Helpers.ResolveIPAndPortFromString(serverAddressAndPort); - PrepareAndConnect(serverAddress, port); - return this; - } - - /// - public IServerQuery Connect(IPEndPoint customLocalIPEndpoint, string serverAddressAndPort) - { - UdpClient = new UdpWrapper(customLocalIPEndpoint, SendTimeout, ReceiveTimeout); - (string serverAddress, ushort port) = Helpers.ResolveIPAndPortFromString(serverAddressAndPort); - PrepareAndConnect(serverAddress, port); - return this; - } - - /// public IServerQuery Connect(IPEndPoint customLocalIPEndpoint, string serverAddress, ushort port) { UdpClient = new UdpWrapper(customLocalIPEndpoint, SendTimeout, ReceiveTimeout); @@ -141,7 +107,7 @@ public async Task GetServerInfoAsync() { var sInfo = new ServerInfo { - Ping = new Ping().Send(_remoteIpEndpoint.Address)?.RoundtripTime ?? default + Ping = new Ping().Send(m_remoteIpEndpoint.Address)?.RoundtripTime ?? default }; if (m_currentChallenge == 0) @@ -253,18 +219,18 @@ private void PrepareAndConnect(string serverAddress, ushort port) if (IPAddress.TryParse(serverAddress, out IPAddress parsedIpAddress)) { // Yep its an IP. - _remoteIpEndpoint = new IPEndPoint(parsedIpAddress, m_port); + m_remoteIpEndpoint = new IPEndPoint(parsedIpAddress, m_port); } else { // Nope it might be a hostname. try { - IPAddress[] addresslist = Dns.GetHostAddresses(serverAddress); - if (addresslist.Length > 0) + IPAddress[] addressList = Dns.GetHostAddresses(serverAddress); + if (addressList.Length > 0) { // We get the first address. - _remoteIpEndpoint = new IPEndPoint(addresslist[0], m_port); + m_remoteIpEndpoint = new IPEndPoint(addressList[0], m_port); } else { @@ -277,8 +243,8 @@ private void PrepareAndConnect(string serverAddress, ushort port) } } - UdpClient = UdpClient ?? new UdpWrapper(new IPEndPoint(IPAddress.Any, 0), SendTimeout, ReceiveTimeout); - UdpClient.Connect(_remoteIpEndpoint); + UdpClient ??= new UdpWrapper(new IPEndPoint(IPAddress.Any, 0), SendTimeout, ReceiveTimeout); + UdpClient.Connect(m_remoteIpEndpoint); } private async Task SendRequestAsync(byte[] request) From 20da3c4dc93c0f9a34fbe5ad9694b4a88ac18cda Mon Sep 17 00:00:00 2001 From: razaq Date: Thu, 13 Jan 2022 22:47:22 +0100 Subject: [PATCH 05/10] retarget to .NET 6 --- SteamQueryNet/SteamQueryNet/SteamQueryNet.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SteamQueryNet/SteamQueryNet/SteamQueryNet.csproj b/SteamQueryNet/SteamQueryNet/SteamQueryNet.csproj index f9fe07c..3e5485f 100644 --- a/SteamQueryNet/SteamQueryNet/SteamQueryNet.csproj +++ b/SteamQueryNet/SteamQueryNet/SteamQueryNet.csproj @@ -1,7 +1,7 @@  - net5.0-windows7.0 + net6.0 Cem YILMAZ Cem YILMAZ 1.0.6 From 69e02ddfa5b2ae6bdf7725de9227089ecfa47285 Mon Sep 17 00:00:00 2001 From: razaq Date: Fri, 14 Jan 2022 00:13:32 +0100 Subject: [PATCH 06/10] rework timeout and async handling --- .../SteamQueryNet/Interfaces/IServerQuery.cs | 9 ++-- .../SteamQueryNet/Interfaces/IUdpClient.cs | 5 +- SteamQueryNet/SteamQueryNet/ServerQuery.cs | 51 +++++++++++------- .../SteamQueryNet/Services/UdpWrapper.cs | 52 ++++++++++++------- 4 files changed, 74 insertions(+), 43 deletions(-) diff --git a/SteamQueryNet/SteamQueryNet/Interfaces/IServerQuery.cs b/SteamQueryNet/SteamQueryNet/Interfaces/IServerQuery.cs index 17b476a..375023d 100644 --- a/SteamQueryNet/SteamQueryNet/Interfaces/IServerQuery.cs +++ b/SteamQueryNet/SteamQueryNet/Interfaces/IServerQuery.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Net; +using System.Threading; using System.Threading.Tasks; namespace SteamQueryNet.Interfaces @@ -18,7 +19,7 @@ public interface IServerQuery /// Renews the server challenge code of the ServerQuery instance in order to be able to execute further operations. /// /// The new created challenge. - Task RenewChallengeAsync(); + Task RenewChallengeAsync(CancellationToken cancellationToken); /// /// Configures and Connects the created instance of SteamQuery UDP socket for Steam Server Query Operations. @@ -62,7 +63,7 @@ public interface IServerQuery /// Requests and serializes the server information. /// /// Serialized ServerInfo instance. - Task GetServerInfoAsync(); + Task GetServerInfoAsync(CancellationToken cancellationToken); /// /// Requests and serializes the list of player information. @@ -74,7 +75,7 @@ public interface IServerQuery /// Requests and serializes the list of player information. /// /// Serialized list of Player instances. - Task> GetPlayersAsync(); + Task> GetPlayersAsync(CancellationToken cancellationToken); /// /// Requests and serializes the list of rules defined by the server. @@ -90,6 +91,6 @@ public interface IServerQuery /// Before the update rules got truncated when exceeding MTU, after the update rules reply is not sent at all. /// /// Serialized list of Rule instances. - Task> GetRulesAsync(); + Task> GetRulesAsync(CancellationToken cancellationToken); } } diff --git a/SteamQueryNet/SteamQueryNet/Interfaces/IUdpClient.cs b/SteamQueryNet/SteamQueryNet/Interfaces/IUdpClient.cs index 91e54e8..e922f79 100644 --- a/SteamQueryNet/SteamQueryNet/Interfaces/IUdpClient.cs +++ b/SteamQueryNet/SteamQueryNet/Interfaces/IUdpClient.cs @@ -1,6 +1,7 @@ using System; using System.Net; using System.Net.Sockets; +using System.Threading; using System.Threading.Tasks; namespace SteamQueryNet.Interfaces @@ -13,8 +14,8 @@ public interface IUdpClient : IDisposable void Connect(IPEndPoint remoteIpEndpoint); - Task SendAsync(byte[] datagram, int bytes); + Task SendAsync(byte[] datagram, CancellationToken cancellationToken); - Task ReceiveAsync(); + Task ReceiveAsync(CancellationToken cancellationToken); } } diff --git a/SteamQueryNet/SteamQueryNet/ServerQuery.cs b/SteamQueryNet/SteamQueryNet/ServerQuery.cs index 0c946d3..b57d96a 100644 --- a/SteamQueryNet/SteamQueryNet/ServerQuery.cs +++ b/SteamQueryNet/SteamQueryNet/ServerQuery.cs @@ -9,6 +9,7 @@ using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; +using System.Threading; using System.Threading.Tasks; [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("SteamQueryNet.Tests")] @@ -103,7 +104,7 @@ public IServerQuery Connect(IPEndPoint customLocalIPEndpoint, string serverAddre } /// - public async Task GetServerInfoAsync() + public async Task GetServerInfoAsync(CancellationToken cancellationToken) { var sInfo = new ServerInfo { @@ -112,10 +113,10 @@ public async Task GetServerInfoAsync() if (m_currentChallenge == 0) { - await RenewChallengeAsync(); + await RenewChallengeAsync(cancellationToken); } - byte[] response = await SendRequestAsync(RequestHelpers.PrepareAS2_INFO_Request(m_currentChallenge)); + byte[] response = await SendRequestAsync(RequestHelpers.PrepareAS2_INFO_Request(m_currentChallenge), cancellationToken); if (response.Length > 0) { DataResolutionUtils.ExtractData(sInfo, response, nameof(sInfo.EDF), true); @@ -127,13 +128,16 @@ public async Task GetServerInfoAsync() /// public ServerInfo GetServerInfo() { - return Helpers.RunSync(GetServerInfoAsync); + Task task = GetServerInfoAsync(new CancellationTokenSource().Token); + task.RunSynchronously(); + return task.Result; + // return Helpers.RunSync(GetServerInfoAsync); } /// - public async Task RenewChallengeAsync() + public async Task RenewChallengeAsync(CancellationToken cancellationToken) { - byte[] response = await SendRequestAsync(RequestHelpers.PrepareAS2_RENEW_CHALLENGE_Request()); + byte[] response = await SendRequestAsync(RequestHelpers.PrepareAS2_RENEW_CHALLENGE_Request(), cancellationToken); if (response.Length > 0) { m_currentChallenge = BitConverter.ToInt32(response.Skip(DataResolutionUtils.RESPONSE_CODE_INDEX).Take(sizeof(int)).ToArray(), 0); @@ -145,19 +149,23 @@ public async Task RenewChallengeAsync() /// public int RenewChallenge() { - return Helpers.RunSync(RenewChallengeAsync); + Task task = RenewChallengeAsync(new CancellationTokenSource().Token); + task.RunSynchronously(); + return task.Result; + // return Helpers.RunSync(RenewChallengeAsync); } /// - public async Task> GetPlayersAsync() + public async Task> GetPlayersAsync(CancellationToken cancellationToken) { if (m_currentChallenge == 0) { - await RenewChallengeAsync(); + await RenewChallengeAsync(cancellationToken); } byte[] response = await SendRequestAsync( - RequestHelpers.PrepareAS2_GENERIC_Request(RequestHeaders.A2S_PLAYER, m_currentChallenge)); + RequestHelpers.PrepareAS2_GENERIC_Request(RequestHeaders.A2S_PLAYER, m_currentChallenge), + cancellationToken); if (response.Length > 0) { @@ -172,19 +180,23 @@ public async Task> GetPlayersAsync() /// public List GetPlayers() { - return Helpers.RunSync(GetPlayersAsync); + Task> task = GetPlayersAsync(new CancellationTokenSource().Token); + task.RunSynchronously(); + return task.Result; + // return Helpers.RunSync(GetPlayersAsync); } /// - public async Task> GetRulesAsync() + public async Task> GetRulesAsync(CancellationToken cancellationToken) { if (m_currentChallenge == 0) { - await RenewChallengeAsync(); + await RenewChallengeAsync(cancellationToken); } byte[] response = await SendRequestAsync( - RequestHelpers.PrepareAS2_GENERIC_Request(RequestHeaders.A2S_RULES, m_currentChallenge)); + RequestHelpers.PrepareAS2_GENERIC_Request(RequestHeaders.A2S_RULES, m_currentChallenge), + cancellationToken); if (response.Length > 0) { @@ -199,7 +211,10 @@ public async Task> GetRulesAsync() /// public List GetRules() { - return Helpers.RunSync(GetRulesAsync); + Task> task = GetRulesAsync(new CancellationTokenSource().Token); + task.RunSynchronously(); + return task.Result; + // return Helpers.RunSync(GetRulesAsync); } /// @@ -247,10 +262,10 @@ private void PrepareAndConnect(string serverAddress, ushort port) UdpClient.Connect(m_remoteIpEndpoint); } - private async Task SendRequestAsync(byte[] request) + private async Task SendRequestAsync(byte[] request, CancellationToken cancellationToken) { - await UdpClient.SendAsync(request, request.Length); - UdpReceiveResult result = await UdpClient.ReceiveAsync(); + await UdpClient.SendAsync(request, cancellationToken); + UdpReceiveResult result = await UdpClient.ReceiveAsync(cancellationToken); return result.Buffer; } } diff --git a/SteamQueryNet/SteamQueryNet/Services/UdpWrapper.cs b/SteamQueryNet/SteamQueryNet/Services/UdpWrapper.cs index 0448318..93bd174 100644 --- a/SteamQueryNet/SteamQueryNet/Services/UdpWrapper.cs +++ b/SteamQueryNet/SteamQueryNet/Services/UdpWrapper.cs @@ -2,6 +2,7 @@ using System; using System.Net; using System.Net.Sockets; +using System.Threading; using System.Threading.Tasks; namespace SteamQueryNet.Services @@ -15,8 +16,8 @@ internal sealed class UdpWrapper : IUdpClient public UdpWrapper(IPEndPoint localIpEndPoint, int sendTimeout, int receiveTimeout) { m_udpClient = new UdpClient(localIpEndPoint); - this.m_sendTimeout = sendTimeout; - this.m_receiveTimeout = receiveTimeout; + m_sendTimeout = sendTimeout; + m_receiveTimeout = receiveTimeout; } public bool IsConnected => m_udpClient.Client.Connected; @@ -36,34 +37,47 @@ public void Dispose() m_udpClient.Dispose(); } - public Task ReceiveAsync() + public async Task ReceiveAsync(CancellationToken cancellationToken) { - var asyncResult = m_udpClient.BeginReceive(null, null); - asyncResult.AsyncWaitHandle.WaitOne(m_receiveTimeout); - if (asyncResult.IsCompleted) + var source = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + source.CancelAfter(m_receiveTimeout); + + try { - IPEndPoint remoteEP = null; - byte[] receivedData = m_udpClient.EndReceive(asyncResult, ref remoteEP); - return Task.FromResult(new UdpReceiveResult(receivedData, remoteEP)); + return await m_udpClient.ReceiveAsync(source.Token); } - else + catch (OperationCanceledException) { - throw new TimeoutException(); + if (cancellationToken.IsCancellationRequested) + { + throw; + } + else + { + throw new TimeoutException(); + } } } - public Task SendAsync(byte[] datagram, int bytes) + public async Task SendAsync(byte[] datagram, CancellationToken cancellationToken) { - var asyncResult = m_udpClient.BeginSend(datagram, bytes, null, null); - asyncResult.AsyncWaitHandle.WaitOne(m_sendTimeout); - if (asyncResult.IsCompleted) + var source = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + source.CancelAfter(m_receiveTimeout); + + try { - int num = m_udpClient.EndSend(asyncResult); - return Task.FromResult(num); + return await m_udpClient.SendAsync(datagram, source.Token); } - else + catch (OperationCanceledException) { - throw new TimeoutException(); + if (cancellationToken.IsCancellationRequested) + { + throw; + } + else + { + throw new TimeoutException(); + } } } } From ed7bf78793c007b649d3b0420ae4c5c9a7f8fe29 Mon Sep 17 00:00:00 2001 From: razaq Date: Fri, 14 Jan 2022 12:53:46 +0100 Subject: [PATCH 07/10] fix parsing of empty strings --- SteamQueryNet/SteamQueryNet/Utils/DataResolutionUtils.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/SteamQueryNet/SteamQueryNet/Utils/DataResolutionUtils.cs b/SteamQueryNet/SteamQueryNet/Utils/DataResolutionUtils.cs index e9fbfab..a908f1a 100644 --- a/SteamQueryNet/SteamQueryNet/Utils/DataResolutionUtils.cs +++ b/SteamQueryNet/SteamQueryNet/Utils/DataResolutionUtils.cs @@ -64,9 +64,7 @@ internal static IEnumerable ExtractData( if (property.PropertyType == typeof(string)) { // Clear the buffer first then take till the termination. - takenBytes = enumerableSource - .SkipWhile(x => x == 0) - .TakeWhile(x => x != 0); + takenBytes = enumerableSource.TakeWhile(x => x != 0); // Parse it into a string. property.SetValue(objectRef, Encoding.UTF8.GetString(takenBytes.ToArray())); From f0ee836c4e6a574161bef03f387ee11ba31401d8 Mon Sep 17 00:00:00 2001 From: razaq Date: Fri, 14 Jan 2022 12:57:32 +0100 Subject: [PATCH 08/10] update README --- README.md | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index c28d62c..63611f9 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ SteamQueryNet is a C# wrapper for [Steam Server Queries](https://developer.valve * Light * Dependency free -* Written in .net standard 2.0 so that it works with both .NET framework 4.6+ and core. +* Written in .NET 6 # How to install ? @@ -22,9 +22,8 @@ SteamQueryNet comes with a single object that gives you access to all API's of t To make use of the API's listed above, an instance of `ServerQuery` should be created. ```csharp -string serverIp = "127.0.0.1"; -int serverPort = 27015; -IServerQuery serverQuery = new ServerQuery(serverIp, serverPort); +IServerQuery serverQuery = new ServerQuery(); +serverQuery.Connect(host, port); ``` or you can use string resolvers like below: @@ -45,15 +44,6 @@ or you can use string resolvers like below: IServerQuery serverQuery = new ServerQuery(myHostAndPort); ``` -Also, it is possible to create `ServerQuery` object without connecting like below: - -```csharp -IServerQuery serverQuery = new ServerQuery(); -serverQuery.Connect(host, port); -``` - -*Note*: `Connect` function overloads are similar to `ServerQuery` non-empty constructors. - ## Providing Custom UDPClient You can provide custom UDP clients by implementing `IUdpClient` in `SteamQueryNet.Interfaces` namespace. @@ -114,13 +104,5 @@ List players = serverQuery.GetPlayers(); List rules = serverQuery.GetRules(); ``` -While **it is not encouraged**, you can chain `Connect` function or Non-empty Constructors to get information in a single line. - -```csharp -ServerInfo serverInfo = new ServerQuery() -.Connect(host, port) -.GetServerInfo(); -``` - # Todos * Enable CI From fc7d186e721ba4f7d0827b1ef4f6d959a6f9398b Mon Sep 17 00:00:00 2001 From: razaq Date: Fri, 14 Jan 2022 14:21:40 +0100 Subject: [PATCH 09/10] fix rules size being 2 bytes instead of 1 --- SteamQueryNet/SteamQueryNet/ServerQuery.cs | 5 +-- .../Utils/DataResolutionUtils.cs | 38 +++++++++++++++++-- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/SteamQueryNet/SteamQueryNet/ServerQuery.cs b/SteamQueryNet/SteamQueryNet/ServerQuery.cs index b57d96a..c0d0b38 100644 --- a/SteamQueryNet/SteamQueryNet/ServerQuery.cs +++ b/SteamQueryNet/SteamQueryNet/ServerQuery.cs @@ -169,7 +169,7 @@ public async Task> GetPlayersAsync(CancellationToken cancellationTo if (response.Length > 0) { - return DataResolutionUtils.ExtractListData(response); + return DataResolutionUtils.ExtractPlayersData(response); } else { @@ -197,10 +197,9 @@ public async Task> GetRulesAsync(CancellationToken cancellationToken) byte[] response = await SendRequestAsync( RequestHelpers.PrepareAS2_GENERIC_Request(RequestHeaders.A2S_RULES, m_currentChallenge), cancellationToken); - if (response.Length > 0) { - return DataResolutionUtils.ExtractListData(response); + return DataResolutionUtils.ExtractRulesData(response); } else { diff --git a/SteamQueryNet/SteamQueryNet/Utils/DataResolutionUtils.cs b/SteamQueryNet/SteamQueryNet/Utils/DataResolutionUtils.cs index a908f1a..73e9c8d 100644 --- a/SteamQueryNet/SteamQueryNet/Utils/DataResolutionUtils.cs +++ b/SteamQueryNet/SteamQueryNet/Utils/DataResolutionUtils.cs @@ -97,7 +97,7 @@ internal static IEnumerable ExtractData( return enumerableSource; } - internal static List ExtractListData(byte[] rawSource) + internal static List ExtractPlayersData(byte[] rawSource) where TObject : class { // Create a list to contain the serialized data. @@ -106,8 +106,40 @@ internal static List ExtractListData(byte[] rawSource) // Skip the response headers. byte itemCount = rawSource[RESPONSE_CODE_INDEX]; - // Skip +1 for item_count. - IEnumerable dataSource = rawSource.Skip(RESPONSE_HEADER_COUNT + 1); + // Skip +1 for item_count + IEnumerable dataSource = rawSource.Skip(RESPONSE_HEADER_COUNT + sizeof(byte)); + + for (byte i = 0; i < itemCount; i++) + { + // Activate a new instance of the object. + var objectInstance = Activator.CreateInstance(); + + // Extract the data. + dataSource = ExtractData(objectInstance, dataSource.ToArray()); + + // Add it into the list. + objectList.Add(objectInstance); + } + + return objectList; + } + + internal static List ExtractRulesData(byte[] rawSource) + where TObject : class + { + // Create a list to contain the serialized data. + var objectList = new List(); + + // Skip the response headers. + Int16 itemCount = BitConverter.ToInt16( + rawSource + .Skip(RESPONSE_CODE_INDEX) + .Take(sizeof(Int16)) + .ToArray() + ); + + // Skip +2 for item_count, because its short + IEnumerable dataSource = rawSource.Skip(RESPONSE_HEADER_COUNT + sizeof(Int16)); for (byte i = 0; i < itemCount; i++) { From 89aaa5976fc2508567eeea6ee162977a0bae5671 Mon Sep 17 00:00:00 2001 From: razaq Date: Mon, 26 Aug 2024 19:26:58 +0200 Subject: [PATCH 10/10] bump to .NET 8 --- SteamQueryNet/SteamQueryNet/SteamQueryNet.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SteamQueryNet/SteamQueryNet/SteamQueryNet.csproj b/SteamQueryNet/SteamQueryNet/SteamQueryNet.csproj index 3e5485f..693e9c3 100644 --- a/SteamQueryNet/SteamQueryNet/SteamQueryNet.csproj +++ b/SteamQueryNet/SteamQueryNet/SteamQueryNet.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 Cem YILMAZ Cem YILMAZ 1.0.6