query_gsquery.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. #! /usr/bin/env python3
  2. # LinuxGSM query_gsquery.py module
  3. # Author: Daniel Gibbs
  4. # Contributors: https://linuxgsm.com/contrib
  5. # Website: https://linuxgsm.com
  6. # Description: Allows querying of various game servers.
  7. """Query game servers using a set of lightweight UDP protocols."""
  8. import argparse
  9. import socket
  10. import sys
  11. engine_types = (
  12. "protocol-valve",
  13. "protocol-valve-won",
  14. "protocol-quake2",
  15. "protocol-quake3",
  16. "protocol-gamespy1",
  17. "protocol-unreal2",
  18. "ut3",
  19. "minecraft",
  20. "minecraftbe",
  21. "jc2m",
  22. "mumbleping",
  23. "soldat",
  24. "teeworlds",
  25. )
  26. class GSQuery:
  27. """Game server query dispatcher."""
  28. server_response_timeout = 2
  29. default_buffer_length = 1024
  30. sourcequery = (
  31. "protocol-valve",
  32. "protocol-valve-won",
  33. "avalanche3.0",
  34. "barotrauma",
  35. "madness",
  36. "quakelive",
  37. "realvirtuality",
  38. "refractor",
  39. "source",
  40. "goldsrc",
  41. "spark",
  42. "starbound",
  43. "unity3d",
  44. "unreal4",
  45. "wurm",
  46. )
  47. idtech2query = ("protocol-quake2", "idtech2", "quake", "iw2.0")
  48. idtech3query = ("protocol-quake3", "iw3.0", "ioquake3", "qfusion")
  49. minecraftquery = ("minecraft", "lwjgl2")
  50. minecraftbequery = ("minecraftbe",)
  51. jc2mquery = ("jc2m",)
  52. mumblequery = ("mumbleping",)
  53. soldatquery = ("soldat",)
  54. twquery = ("teeworlds",)
  55. unrealquery = ("protocol-gamespy1", "unreal")
  56. unreal2query = ("protocol-unreal2", "unreal2")
  57. unreal3query = ("ut3", "unreal3")
  58. def __init__(self, arguments):
  59. """Create a query instance from parsed CLI args."""
  60. self.argument = arguments
  61. if self.argument.engine in self.sourcequery:
  62. self.query_prompt_string = (
  63. b"\xff\xff\xff\xffdetails\0"
  64. if self.argument.engine == "protocol-valve-won"
  65. else b"\xff\xff\xff\xffTSource Engine Query\0"
  66. )
  67. elif self.argument.engine in self.idtech2query:
  68. self.query_prompt_string = b"\xff\xff\xff\xffstatus\x00"
  69. elif self.argument.engine in self.idtech3query:
  70. self.query_prompt_string = b"\xff\xff\xff\xffgetstatus"
  71. elif self.argument.engine in self.jc2mquery:
  72. self.query_prompt_string = b"\xfe\xfd\x09\x10\x20\x30\x40"
  73. elif self.argument.engine in self.minecraftquery:
  74. self.query_prompt_string = b"\xfe\xfd\x09\x3d\x54\x1f\x93"
  75. elif self.argument.engine in self.minecraftbequery:
  76. self.query_prompt_string = (
  77. b"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00"
  78. b"\xfe\xfe\xfe\xfe\xfd\xfd\xfd\xfd\x12\x34\x56\x78"
  79. b"\x00\x00\x00\x00\x00\x00\x00\x00"
  80. )
  81. elif self.argument.engine in self.mumblequery:
  82. self.query_prompt_string = (
  83. b"\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08"
  84. )
  85. elif self.argument.engine in self.soldatquery:
  86. self.query_prompt_string = b"\x69\x00"
  87. elif self.argument.engine in self.twquery:
  88. self.query_prompt_string = b"\x04\x00\x00\xff\xff\xff\xff\x05" + bytearray(
  89. 511
  90. )
  91. elif self.argument.engine in self.unrealquery:
  92. self.query_prompt_string = b"\x5c\x69\x6e\x66\x6f\x5c"
  93. elif self.argument.engine in self.unreal2query:
  94. self.query_prompt_string = b"\x79\x00\x00\x00\x00"
  95. elif self.argument.engine in self.unreal3query:
  96. self.query_prompt_string = b"\xfe\xfd\x09\x00\x00\x00\x00"
  97. self.connected = False
  98. self.response = None
  99. @staticmethod
  100. def fatal_error(error_message, error_code=1):
  101. """Write an error message to stderr and exit."""
  102. sys.stderr.write("ERROR: " + str(error_message) + "\n")
  103. sys.exit(error_code)
  104. @staticmethod
  105. def exit_success(success_message=""):
  106. """Write a success message to stdout and exit."""
  107. sys.stdout.write("OK: " + str(success_message) + "\n")
  108. sys.exit(0)
  109. def responding(self):
  110. """Send a single UDP query and print the response."""
  111. # Connect.
  112. connection = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  113. connection.settimeout(self.server_response_timeout)
  114. try:
  115. self.connected = connection.connect(
  116. (self.argument.address, int(self.argument.port))
  117. )
  118. except socket.timeout:
  119. self.fatal_error("Request timed out", 1)
  120. except OSError:
  121. self.fatal_error("Unable to connect", 1)
  122. # Send.
  123. connection.send(self.query_prompt_string)
  124. # Receive.
  125. try:
  126. self.response = connection.recv(self.default_buffer_length)
  127. except OSError:
  128. self.fatal_error("Unable to receive", 2)
  129. connection.close()
  130. # Response.
  131. if self.response is None:
  132. self.fatal_error("No response", 3)
  133. if self.argument.engine == "protocol-valve-won" and (
  134. not self.response.startswith(b"\xff\xff\xff\xffm")
  135. or len(self.response.split(b"\0")) < 7
  136. ):
  137. self.fatal_error("Invalid WON server response", 3)
  138. if len(self.response) < 5:
  139. sys.exit("Short response.", 3)
  140. else:
  141. self.exit_success(str(self.response))
  142. def parse_args():
  143. """Parse command-line arguments."""
  144. parser = argparse.ArgumentParser(
  145. description="Allows querying of various game servers.",
  146. usage="usage: python3 %(prog)s [options]",
  147. add_help=False,
  148. )
  149. parser.add_argument(
  150. "-a",
  151. "--address",
  152. type=str,
  153. required=True,
  154. help="The IPv4 address of the server.",
  155. )
  156. parser.add_argument(
  157. "-p",
  158. "--port",
  159. type=int,
  160. required=True,
  161. help="The IPv4 port of the server.",
  162. )
  163. parser.add_argument(
  164. "-e",
  165. "--engine",
  166. metavar="ENGINE",
  167. choices=engine_types,
  168. help="Engine type: " + " ".join(engine_types),
  169. )
  170. parser.add_argument(
  171. "-v",
  172. "--verbose",
  173. action="store_true",
  174. help="Display verbose output.",
  175. )
  176. parser.add_argument(
  177. "-d",
  178. "--debug",
  179. action="store_true",
  180. help="Display debugging output.",
  181. )
  182. parser.add_argument(
  183. "-V",
  184. "--version",
  185. action="version",
  186. version="%(prog)s 0.0.1",
  187. help="Display version and exit.",
  188. )
  189. parser.add_argument("-h", "--help", action="help", help="Display help and exit.")
  190. return parser.parse_args()
  191. def main():
  192. """CLI entrypoint."""
  193. arguments = parse_args()
  194. server = GSQuery(arguments)
  195. server.responding()
  196. if __name__ == "__main__":
  197. main()