Gs3Status.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. using System.Net;
  2. using System.Net.Sockets;
  3. using System.Text;
  4. //https://github.com/opengsq/opengsq-dotnet/blob/main/OpenGSQ/Protocols/GameSpy3.cs
  5. //https://github.com/opengsq/opengsq-dotnet/blob/main/OpenGSQ/ProtocolBase.cs
  6. //https://github.com/opengsq/opengsq-dotnet/blob/main/OpenGSQ/BinaryReaderExtensions.cs
  7. /*
  8. MIT License
  9. Copyright (c) 2021 OpenGSQ
  10. Permission is hereby granted, free of charge, to any person obtaining a copy
  11. of this software and associated documentation files (the "Software"), to deal
  12. in the Software without restriction, including without limitation the rights
  13. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  14. copies of the Software, and to permit persons to whom the Software is
  15. furnished to do so, subject to the following conditions:
  16. The above copyright notice and this permission notice shall be included in all
  17. copies or substantial portions of the Software.
  18. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  19. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  20. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  21. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  22. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  23. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  24. SOFTWARE.
  25. */
  26. namespace VeloeMonitorDataCollector.Dependencies
  27. {
  28. public class Gs3Status
  29. {
  30. /// <summary>
  31. /// Represents a network endpoint as an IP address and a port number.
  32. /// </summary>
  33. protected IPEndPoint _EndPoint;
  34. /// <summary>
  35. /// Timeout in millisecond
  36. /// </summary>
  37. protected int _Timeout;
  38. /// <summary>
  39. /// Cached challenge bytes
  40. /// </summary>
  41. //protected byte[] _Challenge = new byte[0];
  42. /// <summary>
  43. /// Gamespy Query Protocol version 3
  44. /// </summary>
  45. /// <param name="address"></param>
  46. /// <param name="port"></param>
  47. /// <param name="timeout"></param>
  48. public Gs3Status(string address, int port, int timeout = 5000)
  49. {
  50. if (IPAddress.TryParse(address, out var ipAddress))
  51. {
  52. _EndPoint = new IPEndPoint(ipAddress, port);
  53. }
  54. else
  55. {
  56. _EndPoint = new IPEndPoint(Dns.GetHostAddresses(address)[0], port);
  57. }
  58. _Timeout = timeout;
  59. }
  60. #pragma warning disable 1591
  61. protected bool _Challenge;
  62. #pragma warning restore 1591
  63. /// <summary>
  64. /// Retrieves information about the server including, Info, Players, and Teams.
  65. /// </summary>
  66. /// <returns></returns>
  67. /// <exception cref="SocketException"></exception>
  68. public Status GetStatus()
  69. {
  70. using (var udpClient = new UdpClient())
  71. {
  72. var responseData = ConnectAndSendPackets(udpClient);
  73. using (var br = new BinaryReader(new MemoryStream(responseData), Encoding.UTF8))
  74. {
  75. return new Status
  76. {
  77. // Save Status Info
  78. Info = GetInfo(br),
  79. // Save Status Players
  80. Players = GetPlayers(br),
  81. // Save Status Teams
  82. Teams = GetTeams(br)
  83. };
  84. }
  85. }
  86. }
  87. private byte[] ConnectAndSendPackets(UdpClient udpClient)
  88. {
  89. // Connect to remote host
  90. udpClient.Connect(_EndPoint);
  91. udpClient.Client.SendTimeout = _Timeout;
  92. udpClient.Client.ReceiveTimeout = _Timeout;
  93. // Packet 1: Initial request
  94. byte[] responseData, challenge = new byte[] { }, requestData = new byte[] { 0xFE, 0xFD, 0x09, 0x04, 0x05, 0x06, 0x07 };
  95. if (_Challenge)
  96. {
  97. udpClient.Send(requestData, requestData.Length);
  98. // Packet 2: First response
  99. responseData = udpClient.Receive(ref _EndPoint);
  100. // Get challenge
  101. if (int.TryParse(Encoding.ASCII.GetString(responseData.Skip(5).ToArray()).Trim(), out int result) && result != 0)
  102. {
  103. challenge = BitConverter.GetBytes(result);
  104. if (BitConverter.IsLittleEndian)
  105. {
  106. Array.Reverse(challenge);
  107. }
  108. }
  109. }
  110. // Packet 3: Second request
  111. requestData[2] = 0x00;
  112. requestData = requestData.Concat(challenge).Concat(new byte[] { 0xFF, 0xFF, 0xFF, 0x01 }).ToArray();
  113. udpClient.Send(requestData, requestData.Length);
  114. // Packet 4: Server response
  115. responseData = Receive(udpClient);
  116. return responseData;
  117. }
  118. private byte[] Receive(UdpClient udpClient)
  119. {
  120. int totalPackets = -1;
  121. var payloads = new SortedDictionary<int, byte[]>();
  122. do
  123. {
  124. var responseData = udpClient.Receive(ref _EndPoint);
  125. using (var br = new BinaryReader(new MemoryStream(responseData), Encoding.UTF8))
  126. {
  127. var header = br.ReadByte();
  128. if (header != 0)
  129. {
  130. throw new Exception($"Packet header mismatch. Received: {header}. Expected: 0.");
  131. }
  132. // Skip the timestamp and splitnum
  133. br.ReadBytes(13);
  134. // The 'numPackets' byte
  135. var numPackets = br.ReadByte();
  136. // The low 7 bits are the packet index (starting at zero)
  137. var number = numPackets & 0x7F;
  138. // The high bit is whether or not this is the last packet
  139. var isLastPacket = numPackets >> 7 == 1;
  140. // Save totalPackets as packet number + 1
  141. if (isLastPacket)
  142. {
  143. totalPackets = number + 1;
  144. }
  145. // The object id. Example: \x01
  146. var objectId = br.ReadByte();
  147. // The object header
  148. byte[] objectHeader = new byte[] { };
  149. if (objectId >= 1)
  150. {
  151. // The object name. Example: "player_"
  152. string objectName = br.ReadStringEx();
  153. // The object items appear count
  154. int count = br.ReadByte();
  155. // If the object item doesn't appear before, set the header back
  156. if (count == 0)
  157. {
  158. // Set the header. Example: \x00\x01player_\x00\x00
  159. objectHeader = new byte[] { 0x00, objectId }.Concat(Encoding.UTF8.GetBytes(objectName)).Concat(new byte[] { 0x00, 0x00 }).ToArray();
  160. }
  161. }
  162. // Save the payload
  163. byte[] payload = objectHeader.Concat(responseData.Skip((int)br.BaseStream.Position)).ToArray();
  164. payloads.Add(number, TrimPayload(payload));
  165. }
  166. } while (totalPackets == -1 || payloads.Count < totalPackets);
  167. // Combine the payloads
  168. var combinedPayload = payloads.Values.Aggregate((a, b) => a.Concat(b).ToArray());
  169. return combinedPayload;
  170. }
  171. /// <summary>
  172. /// Remove the last trash string on the payload
  173. /// </summary>
  174. /// <param name="payload"></param>
  175. /// <returns></returns>
  176. private byte[] TrimPayload(byte[] payload)
  177. {
  178. int i = payload.Length;
  179. while (payload[--i] != 0) ;
  180. return payload.Take(i).ToArray();
  181. }
  182. private Dictionary<string, string> GetInfo(BinaryReader br)
  183. {
  184. var info = new Dictionary<string, string>();
  185. // Read all key values
  186. while (br.TryReadStringEx(out var key))
  187. {
  188. info[key] = br.ReadStringEx().Trim();
  189. }
  190. return info;
  191. }
  192. private List<Dictionary<string, string>> GetPlayers(BinaryReader br)
  193. {
  194. var players = new List<Dictionary<string, string>>();
  195. // Return if BaseStream is end
  196. if (br.BaseStream.Position == br.BaseStream.Length)
  197. {
  198. return players;
  199. }
  200. // Skip \x01player_\x00\x00
  201. br.ReadByte();
  202. string key = br.ReadStringEx().TrimEnd('_');
  203. br.ReadByte();
  204. // Team index
  205. int i = 0;
  206. // Loop all values and save
  207. while (br.BaseStream.Position < br.BaseStream.Length)
  208. {
  209. if (br.TryReadStringEx(out var value))
  210. {
  211. // Add a Dictionary object if not exists
  212. if (players.Count < i + 1)
  213. {
  214. players.Add(new Dictionary<string, string>());
  215. }
  216. // Save the value
  217. players[i++][key] = value.Trim();
  218. }
  219. else
  220. {
  221. // Return if no player
  222. if (br.BaseStream.Position == br.BaseStream.Length)
  223. {
  224. break;
  225. }
  226. // Set new key
  227. if (br.TryReadStringEx(out key))
  228. {
  229. // Remove the trailing "_"
  230. key = key.TrimEnd('_');
  231. }
  232. else
  233. {
  234. break;
  235. }
  236. // Reset the team index
  237. i = br.ReadByte();
  238. }
  239. }
  240. return players;
  241. }
  242. private List<Dictionary<string, string>> GetTeams(BinaryReader br)
  243. {
  244. var teams = new List<Dictionary<string, string>>();
  245. // Return if BaseStream is end
  246. if (br.BaseStream.Position == br.BaseStream.Length)
  247. {
  248. return teams;
  249. }
  250. // Skip \x00\x02team_t\x00\x00
  251. br.ReadBytes(2);
  252. string key = br.ReadStringEx().TrimEnd('t').TrimEnd('_');
  253. br.ReadByte();
  254. // Player index
  255. int i = 0;
  256. // Loop all values and save
  257. while (br.BaseStream.Position < br.BaseStream.Length)
  258. {
  259. if (br.TryReadStringEx(out var value))
  260. {
  261. // Add a Dictionary object if not exists
  262. if (teams.Count < i + 1)
  263. {
  264. teams.Add(new Dictionary<string, string>());
  265. }
  266. // Save the value
  267. teams[i++][key] = value.Trim();
  268. }
  269. else
  270. {
  271. // Return if no team
  272. if (br.BaseStream.Position == br.BaseStream.Length)
  273. {
  274. break;
  275. }
  276. // Set new key
  277. if (br.TryReadStringEx(out key))
  278. {
  279. // Remove the trailing "_t"
  280. key = key.TrimEnd('t').TrimEnd('_');
  281. }
  282. else
  283. {
  284. break;
  285. }
  286. // Reset the team index
  287. i = br.ReadByte();
  288. }
  289. }
  290. return teams;
  291. }
  292. /// <summary>
  293. /// Status object
  294. /// </summary>
  295. public class Status
  296. {
  297. /// <summary>
  298. /// Status Info
  299. /// </summary>
  300. public Dictionary<string, string> Info { get; set; }
  301. /// <summary>
  302. /// Status Players
  303. /// </summary>
  304. public List<Dictionary<string, string>> Players { get; set; }
  305. /// <summary>
  306. /// Status Teams
  307. /// </summary>
  308. public List<Dictionary<string, string>> Teams { get; set; }
  309. }
  310. }
  311. /// <summary>
  312. /// BinaryReader Extensions
  313. /// </summary>
  314. public static class BinaryReaderExtensions
  315. {
  316. /// <summary>
  317. /// Reads a string from the current stream until charByte.
  318. /// </summary>
  319. /// <param name="br"></param>
  320. /// <param name="charBytes"></param>
  321. /// <returns>The string being read.</returns>
  322. /// <exception cref="EndOfStreamException">The end of the stream is reached.</exception>
  323. /// <exception cref="ObjectDisposedException">The stream is closed.</exception>
  324. /// <exception cref="IOException">An I/O error occurs.</exception>
  325. public static string ReadStringEx(this BinaryReader br, byte[] charBytes)
  326. {
  327. charBytes = charBytes ?? new byte[] { 0 };
  328. var bytes = new List<byte>();
  329. byte streamByte;
  330. while (Array.IndexOf(charBytes, streamByte = br.ReadByte()) == -1)
  331. {
  332. bytes.Add(streamByte);
  333. }
  334. return Encoding.UTF8.GetString(bytes.ToArray());
  335. }
  336. /// <summary>
  337. /// Reads a string from the current stream until charByte.
  338. /// </summary>
  339. /// <param name="br"></param>
  340. /// <param name="charByte"></param>
  341. /// <returns>The string being read.</returns>
  342. /// <exception cref="EndOfStreamException">The end of the stream is reached.</exception>
  343. /// <exception cref="ObjectDisposedException">The stream is closed.</exception>
  344. /// <exception cref="IOException">An I/O error occurs.</exception>
  345. public static string ReadStringEx(this BinaryReader br, byte charByte = 0)
  346. {
  347. return br.ReadStringEx(new byte[] { charByte });
  348. }
  349. /// <summary>
  350. /// Reads a string from the current stream until charByte. Return true if is not null and empty.
  351. /// </summary>
  352. /// <param name="br"></param>
  353. /// <param name="outString"></param>
  354. /// <param name="charBytes"></param>
  355. /// <returns></returns>
  356. public static bool TryReadStringEx(this BinaryReader br, out string outString, byte[] charBytes)
  357. {
  358. outString = br.ReadStringEx(charBytes);
  359. return !string.IsNullOrEmpty(outString);
  360. }
  361. /// <summary>
  362. /// Reads a string from the current stream until charByte. Return true if is not null and empty.
  363. /// </summary>
  364. /// <param name="br"></param>
  365. /// <param name="outString"></param>
  366. /// <param name="charByte"></param>
  367. /// <returns></returns>
  368. public static bool TryReadStringEx(this BinaryReader br, out string outString, byte charByte = 0)
  369. {
  370. return br.TryReadStringEx(out outString, new byte[] { charByte });
  371. }
  372. }
  373. }