gamespy_gamestats_server.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. """DWC Network Server Emulator
  2. Copyright (C) 2014 polaris-
  3. Copyright (C) 2014 ToadKing
  4. Copyright (C) 2014 AdmiralCurtiss
  5. Copyright (C) 2014 msoucy
  6. Copyright (C) 2015 Sepalani
  7. This program is free software: you can redistribute it and/or modify
  8. it under the terms of the GNU Affero General Public License as
  9. published by the Free Software Foundation, either version 3 of the
  10. License, or (at your option) any later version.
  11. This program is distributed in the hope that it will be useful,
  12. but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. GNU Affero General Public License for more details.
  15. You should have received a copy of the GNU Affero General Public License
  16. along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. """
  18. import logging
  19. import time
  20. import traceback
  21. from twisted.internet.protocol import Factory
  22. from twisted.internet.endpoints import serverFromString
  23. from twisted.protocols.basic import LineReceiver
  24. from twisted.internet import reactor
  25. from twisted.internet.error import ReactorAlreadyRunning
  26. import gamespy.gs_database as gs_database
  27. import gamespy.gs_query as gs_query
  28. import gamespy.gs_utility as gs_utils
  29. import other.utils as utils
  30. import dwc_config
  31. logger = dwc_config.get_logger('GameSpyGamestatsServer')
  32. address = dwc_config.get_ip_port('GameSpyGamestatsServer')
  33. class GameSpyGamestatsServer(object):
  34. def __init__(self):
  35. pass
  36. def start(self):
  37. endpoint_search = serverFromString(
  38. reactor,
  39. "tcp:%d:interface=%s" % (address[1], address[0])
  40. )
  41. conn_search = endpoint_search.listen(GamestatsFactory())
  42. try:
  43. if not reactor.running:
  44. reactor.run(installSignalHandlers=0)
  45. except ReactorAlreadyRunning:
  46. pass
  47. class GamestatsFactory(Factory):
  48. def __init__(self):
  49. logger.log(logging.INFO,
  50. "Now listening for connections on %s:%d...",
  51. address[0], address[1])
  52. self.sessions = {}
  53. def buildProtocol(self, address):
  54. return Gamestats(self.sessions, address)
  55. class Gamestats(LineReceiver):
  56. def __init__(self, sessions, address):
  57. self.setRawMode() # We're dealing with binary data so set to raw mode
  58. self.db = gs_database.GamespyDatabase()
  59. self.sessions = sessions
  60. self.address = address
  61. # Stores any unparsable/incomplete commands until the next
  62. # rawDataReceived
  63. self.remaining_message = ""
  64. self.session = ""
  65. self.gameid = ""
  66. self.lid = "0"
  67. self.data = ""
  68. def log(self, level, msg, *args, **kwargs):
  69. if not self.session:
  70. if not self.gameid:
  71. logger.log(level, "[%s:%d] " + msg,
  72. self.address.host, self.address.port,
  73. *args, **kwargs)
  74. else:
  75. logger.log(level, "[%s:%d | %s] " + msg,
  76. self.address.host, self.address.port, self.gameid,
  77. *args, **kwargs)
  78. else:
  79. if not self.gameid:
  80. logger.log(level, "[%s:%d | %s] " + msg,
  81. self.address.host, self.address.port, self.session,
  82. *args, **kwargs)
  83. else:
  84. logger.log(level, "[%s:%d | %s | %s] " + msg,
  85. self.address.host, self.address.port, self.session,
  86. self.gameid, *args, **kwargs)
  87. def connectionMade(self):
  88. try:
  89. self.log(logging.INFO,
  90. "Received connection from %s:%d",
  91. self.address.host, self.address.port)
  92. # Generate a random challenge string
  93. self.challenge = utils.generate_random_str(
  94. 10, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  95. )
  96. # The first command sent to the client is always a login challenge
  97. # containing the server challenge key.
  98. msg = gs_query.create_gamespy_message([
  99. ('__cmd__', "lc"),
  100. ('__cmd_val__', "1"),
  101. ('challenge', self.challenge),
  102. ('id', "1"),
  103. ])
  104. self.log(logging.DEBUG, "SENDING: '%s'...", msg)
  105. msg = self.crypt(msg)
  106. self.transport.write(bytes(msg))
  107. except:
  108. self.log(logging.ERROR,
  109. "Unknown exception: %s",
  110. traceback.format_exc())
  111. def connectionLost(self, reason):
  112. return
  113. def rawDataReceived(self, data):
  114. try:
  115. # Decrypt packet
  116. self.remaining_message += data
  117. if "\\final\\" not in data:
  118. return
  119. msg = str(self.crypt(self.remaining_message))
  120. self.data = msg
  121. self.remaining_message = ""
  122. commands, self.remaining_message = \
  123. gs_query.parse_gamespy_message(msg)
  124. logger.log(logging.DEBUG, "STATS RESPONSE: %s", msg)
  125. cmds = {
  126. "auth": self.perform_auth,
  127. "authp": self.perform_authp,
  128. "ka": self.perform_ka,
  129. "setpd": self.perform_setpd,
  130. "getpd": self.perform_getpd,
  131. "newgame": self.perform_newgame,
  132. "updgame": self.perform_updgame,
  133. }
  134. def cmd_err(data_parsed):
  135. logger.log(logging.DEBUG,
  136. "Found unknown command, don't know how to"
  137. " handle '%s'.", data_parsed['__cmd__'])
  138. for data_parsed in commands:
  139. print(data_parsed)
  140. cmds.get(data_parsed['__cmd__'], cmd_err)(data_parsed)
  141. except:
  142. self.log(logging.ERROR,
  143. "Unknown exception: %s",
  144. traceback.format_exc())
  145. def perform_auth(self, data_parsed):
  146. self.log(logging.DEBUG, "%s", "Parsing 'auth'...")
  147. if "gamename" in data_parsed:
  148. self.gameid = data_parsed['gamename']
  149. self.session = utils.generate_random_number_str(10)
  150. msg = gs_query.create_gamespy_message([
  151. ('__cmd__', "lc"),
  152. ('__cmd_val__', "2"),
  153. ('sesskey', self.session),
  154. ('proof', 0),
  155. ('id', "1"),
  156. ])
  157. self.log(logging.DEBUG, "SENDING: '%s'...", msg)
  158. msg = self.crypt(msg)
  159. self.transport.write(bytes(msg))
  160. def perform_authp(self, data_parsed):
  161. authtoken_parsed = gs_utils.parse_authtoken(data_parsed['authtoken'],
  162. self.db)
  163. # print authtoken_parsed
  164. if "lid" in data_parsed:
  165. self.lid = data_parsed['lid']
  166. userid, profileid, gsbrcd, uniquenick = \
  167. gs_utils.login_profile_via_parsed_authtoken(authtoken_parsed,
  168. self.db)
  169. if profileid is not None:
  170. # Successfully logged in or created account, continue
  171. # creating session.
  172. sesskey = self.db.create_session(profileid, '')
  173. self.sessions[profileid] = self
  174. self.profileid = int(profileid)
  175. msg = gs_query.create_gamespy_message([
  176. ('__cmd__', "pauthr"),
  177. ('__cmd_val__', profileid),
  178. ('lid', self.lid),
  179. ])
  180. else:
  181. # login failed
  182. self.log(logging.WARNING, "Invalid password")
  183. msg = gs_query.create_gamespy_message([
  184. ('__cmd__', "pauthr"),
  185. ('__cmd_val__', -3),
  186. ('lid', self.lid),
  187. ('errmsg', 'Invalid Validation'),
  188. ])
  189. self.log(logging.DEBUG, "SENDING: '%s'...", msg)
  190. msg = self.crypt(msg)
  191. self.transport.write(bytes(msg))
  192. def perform_ka(self, data_parsed):
  193. msg = gs_query.create_gamespy_message([
  194. ('__cmd__', "ka"),
  195. ('__cmd_val__', ""),
  196. ])
  197. self.log(logging.DEBUG, "SENDING: '%s'...", msg)
  198. msg = self.crypt(msg)
  199. self.transport.write(bytes(msg))
  200. return
  201. def perform_setpd(self, data_parsed):
  202. data = self.data
  203. msg = gs_query.create_gamespy_message([
  204. ('__cmd__', "setpdr"),
  205. ('__cmd_val__', 1),
  206. ('lid', self.lid),
  207. ('pid', self.profileid),
  208. ('mod', int(time.time())),
  209. ])
  210. self.log(logging.DEBUG, "SENDING: '%s'...", msg)
  211. msg = self.crypt(msg)
  212. self.transport.write(bytes(msg))
  213. # TODO: Return error message.
  214. if int(data_parsed['pid']) != self.profileid:
  215. logger.log(logging.WARNING,
  216. "ERROR: %d tried to update %d's profile",
  217. int(data_parsed['pid']), self.profileid)
  218. return
  219. data_str = "\\data\\"
  220. length = int(data_parsed['length'])
  221. if len(data) < length:
  222. # The packet isn't complete yet, keep loop until we get the
  223. # entire packet. The length entire packet SHOULD always be
  224. # greater than the data field, so this check should be fine.
  225. return
  226. if data_str in data:
  227. idx = data.index(data_str) + len(data_str)
  228. data = data[idx:idx + length].rstrip("\\")
  229. else:
  230. logger.log(logging.ERROR,
  231. "ERROR: Could not find \data\ in setpd command: %s",
  232. data)
  233. data = ""
  234. current_data = self.db.pd_get(self.profileid,
  235. data_parsed['dindex'],
  236. data_parsed['ptype'])
  237. if current_data and data and 'data' in current_data:
  238. current_data = current_data['data'].lstrip('\\').split('\\')
  239. new_data = data.lstrip('\\').split('\\')
  240. current_data = dict(zip(current_data[0::2],
  241. current_data[1::2]))
  242. new_data = dict(zip(new_data[0::2], new_data[1::2]))
  243. for k in new_data.keys():
  244. current_data[k] = new_data[k]
  245. # TODO: use str.join()
  246. data = "\\"
  247. for k in current_data.keys():
  248. data += k + "\\" + current_data[k] + "\\"
  249. data = data.rstrip("\\") # Don't put trailing \ into db
  250. self.db.pd_insert(self.profileid,
  251. data_parsed['dindex'],
  252. data_parsed['ptype'],
  253. data)
  254. def perform_getpd(self, data_parsed):
  255. pid = int(data_parsed['pid'])
  256. profile = self.db.pd_get(pid,
  257. data_parsed['dindex'],
  258. data_parsed['ptype'])
  259. if profile is None:
  260. self.log(logging.WARNING,
  261. "Could not find profile for %d %s %s",
  262. pid, data_parsed['dindex'], data_parsed['ptype'])
  263. keys = data_parsed['keys'].split('\x01')
  264. profile_data = None
  265. data = ""
  266. # Someone figure out if this is actually a good way to handle this
  267. # when no profile is found
  268. if profile is not None and 'data' in profile:
  269. profile_data = profile['data']
  270. if profile_data.endswith("\\"):
  271. profile_data = profile_data[:-1]
  272. profile_data = \
  273. gs_query.parse_gamespy_message("\\prof\\" + profile_data +
  274. "\\final\\")
  275. if profile_data is not None:
  276. profile_data = profile_data[0][0]
  277. else:
  278. self.log(logging.WARNING,
  279. "Could not get data section from profile for %d",
  280. pid)
  281. if len(keys):
  282. # TODO: more clean/pythonic way to do (join?)
  283. for key in keys:
  284. if key in ("__cmd__", "__cmd_val__", ""):
  285. continue
  286. data += "\\" + key + "\\"
  287. if profile_data is not None and key in profile_data:
  288. data += profile_data[key]
  289. else:
  290. self.log(logging.WARNING,
  291. "No keys requested, defaulting to all keys: %s",
  292. profile['data'])
  293. data = profile['data']
  294. modified = int(time.time())
  295. # Check if there is a nul byte and data after it
  296. data_blocks = data.split('\x00')
  297. if len(data_blocks) > 1 and any(block for block in data_blocks[1:]):
  298. self.log(logging.WARNING, "Data after nul byte: %s", data)
  299. data = data_blocks[0]
  300. msg = gs_query.create_gamespy_message([
  301. ('__cmd__', "getpdr"),
  302. ('__cmd_val__', 1),
  303. ('lid', self.lid),
  304. ('pid', pid),
  305. ('mod', modified),
  306. ('length', len(data)),
  307. ('data', ''),
  308. (data,)
  309. ])
  310. if msg.count('\\') % 2:
  311. # An empty field must be terminated by \ before \final\
  312. msg = msg.replace("\\final\\", "\\\\final\\")
  313. self.log(logging.DEBUG, "SENDING: '%s'...", msg)
  314. msg = self.crypt(msg)
  315. self.transport.write(bytes(msg))
  316. def perform_newgame(self, data_parsed):
  317. # No op
  318. return
  319. def perform_updgame(self, data_parsed):
  320. # No op
  321. return
  322. def crypt(self, data):
  323. key = bytearray(b"GameSpy3D")
  324. key_len = len(key)
  325. output = bytearray(data.encode("ascii"))
  326. if "\\final\\" in output:
  327. end = output.index("\\final\\")
  328. else:
  329. end = len(output)
  330. for i in range(end):
  331. output[i] ^= key[i % key_len]
  332. return output
  333. if __name__ == "__main__":
  334. gsss = GameSpyGamestatsServer()
  335. gsss.start()