gamespy_qr_server.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. """DWC Network Server Emulator
  2. Copyright (C) 2014 polaris-
  3. Copyright (C) 2014 ToadKing
  4. Copyright (C) 2014 AdmiralCurtiss
  5. Copyright (C) 2015 Sepalani
  6. This program is free software: you can redistribute it and/or modify
  7. it under the terms of the GNU Affero General Public License as
  8. published by the Free Software Foundation, either version 3 of the
  9. License, or (at your option) any later version.
  10. This program is distributed in the hope that it will be useful,
  11. but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. GNU Affero General Public License for more details.
  14. You should have received a copy of the GNU Affero General Public License
  15. along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. Server emulator for *.available.gs.nintendowifi.net and
  17. *.master.gs.nintendowifi.net
  18. Query and Reporting:
  19. http://docs.poweredbygamespy.com/wiki/Query_and_Reporting_Overview
  20. """
  21. import logging
  22. import select
  23. import socket
  24. import struct
  25. import threading
  26. import time
  27. import queue
  28. import traceback
  29. from multiprocessing.managers import BaseManager
  30. import gamespy.gs_utility as gs_utils
  31. import gamespy.gs_database as gs_database
  32. import other.utils as utils
  33. import dwc_config
  34. from gamespy_server_browser_server import GameSpyServerBrowserServer
  35. logger = dwc_config.get_logger('GameSpyQRServer')
  36. class GameSpyServerDatabase(BaseManager):
  37. pass
  38. class GameSpyQRServer(object):
  39. class Session(object):
  40. def __init__(self, address):
  41. self.session = ""
  42. self.challenge = ""
  43. self.secretkey = "" # Parse gslist.cfg later
  44. self.sent_challenge = False
  45. self.heartbeat_data = None
  46. self.address = address
  47. self.console = 0
  48. self.playerid = 0
  49. self.ingamesn = None
  50. self.gamename = ""
  51. self.keepalive = -1
  52. def __init__(self):
  53. self.sessions = {}
  54. # Generate a dictionary "secret_key_list" containing the secret game
  55. # keys associated with their game IDs. The dictionary key will be the
  56. # game's ID, and the value will be the secret key.
  57. self.secret_key_list = gs_utils.generate_secret_keys("gslist.cfg")
  58. # self.log(logging.DEBUG, address, session_id,
  59. # "Generated list of secret game keys...")
  60. GameSpyServerDatabase.register("update_server_list")
  61. GameSpyServerDatabase.register("delete_server")
  62. def log(self, level, address, session_id, msg, *args, **kwargs):
  63. """TODO: Use logger format"""
  64. if address is None:
  65. logger.log(level, msg, *args, **kwargs)
  66. else:
  67. if session_id is not None:
  68. logger.log(level, "[%s:%d %08x] " + msg,
  69. address[0], address[1], session_id,
  70. *args, **kwargs)
  71. else:
  72. logger.log(level, "[%s:%d] " + msg,
  73. address[0], address[1],
  74. *args, **kwargs)
  75. def start(self):
  76. try:
  77. manager_address = dwc_config.get_ip_port('GameSpyManager')
  78. manager_password = ""
  79. self.server_manager = GameSpyServerDatabase(
  80. address=manager_address,
  81. authkey=manager_password.encode()
  82. )
  83. self.server_manager.connect()
  84. # Start QR server
  85. # Accessible to outside connections (use this if you don't know
  86. # what you're doing)
  87. address = dwc_config.get_ip_port('GameSpyQRServer')
  88. self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  89. self.socket.bind(address)
  90. self.socket.setblocking(0)
  91. logger.log(logging.INFO,
  92. "Server is now listening on %s:%s...",
  93. address[0], address[1])
  94. # Dependencies! I don't really like this solution but it's easier
  95. # than trying to manage it another way.
  96. server_browser_server = GameSpyServerBrowserServer(self)
  97. server_browser_server_thread = threading.Thread(
  98. target=server_browser_server.start
  99. )
  100. server_browser_server_thread.start()
  101. self.write_queue = queue.Queue()
  102. self.db = gs_database.GamespyDatabase()
  103. threading.Thread(target=self.write_queue_worker).start()
  104. while True:
  105. ready = select.select([self.socket], [], [], 15)
  106. if ready[0]:
  107. try:
  108. recv_data, address = self.socket.recvfrom(2048)
  109. self.handle_packet(self.socket, recv_data, address)
  110. except:
  111. logger.log(logging.ERROR,
  112. "Failed to handle client: %s",
  113. traceback.format_exc())
  114. self.keepalive_check()
  115. except:
  116. logger.log(logging.ERROR,
  117. "Unknown exception: %s",
  118. traceback.format_exc())
  119. def write_queue_send(self, data, address):
  120. time.sleep(0.05)
  121. self.socket.sendto(data, address)
  122. def write_queue_worker(self):
  123. while True:
  124. data, address = self.write_queue.get()
  125. threading.Thread(target=self.write_queue_send,
  126. args=(data, address)).start()
  127. self.write_queue.task_done()
  128. def update_server_list(self, session_id, k):
  129. if "statechanged" in k and k['statechanged'] == "2": # Close server
  130. self.server_manager.delete_server(k['gamename'], session_id)
  131. if session_id in self.sessions:
  132. del self.sessions[session_id]
  133. else:
  134. # dwc_mtype controls what kind of server query we're looking for.
  135. # dwc_mtype = 0 is used when looking for a matchmaking game.
  136. # dwc_mtype = 1 is unknown.
  137. # dwc_mtype = 2 is used when hosting a friends only game
  138. # (possibly other uses too).
  139. # dwc_mtype = 3 is used when looking for a friends only game
  140. # (possibly other uses too).
  141. # Some memory could be saved by clearing out any unwanted fields
  142. # from k before sending.
  143. self.server_manager.update_server_list(
  144. k['gamename'], session_id, k,
  145. self.sessions[session_id].console
  146. )._getvalue()
  147. if session_id in self.sessions:
  148. self.sessions[session_id].gamename = k['gamename']
  149. def handle_packet(self, socket, recv_data, address):
  150. """Tetris DS overlay 10 @ 02144184 - Handle responses back to server.
  151. After some more packet inspection, it seems the format goes something
  152. like this:
  153. - All server messages seem to always start with \xfe\xfd.
  154. - The first byte from the client (or third byte from the server) is
  155. a command.
  156. - Bytes 2 - 5 from the client is some kind of ID. This will have to
  157. be inspected later. I believe it's a session-like ID because the
  158. number changes between connections. Copying the client's ID might
  159. be enough.
  160. The above was as guessed.
  161. The code in Tetris DS (overlay 10) @ 0216E974 handles the network
  162. command creation.
  163. R1 contains the command to be sent to the server.
  164. R2 contains a pointer to some unknown integer that gets written
  165. after the command.
  166. - Commands
  167. Commands range from 0x00 to 0x09 (for client only at least?)
  168. (Tetris DS overlay 10 @ 0216DDCC)
  169. CLIENT:
  170. 0x01 - Response (Tetris DS overlay 10 @ 216DCA4)
  171. Sends back base64 of RC4 encrypted string that was
  172. gotten from the server's 0x01.
  173. 0x03 - Send client state? (Tetris DS overlay 10 @ 216DA30)
  174. Data sent:
  175. 1) Loop for each localip available on the system, write
  176. as localip%d\x00(local ip)
  177. 2) localport\x00(local port)
  178. 3) natneg (either 0 or 1)
  179. 4) ONLY IF STATE CHANGED: statechanged\x00(state)
  180. (Possible values: 0, 1, 2, 3)
  181. 5) gamename\x00(game name)
  182. 6) ONLY IF PUBLIC IP AND PORT ARE AVAILABLE:
  183. publicip\x00(public ip)
  184. 7) ONLY IF PUBLIC IP AND PORT ARE AVAILABLE:
  185. publicport\x00(public port)
  186. if statechanged != 2:
  187. Write various other data described here:
  188. http://docs.poweredbygamespy.com/wiki/Query_and_Reporting_Implementation
  189. 0x07 - Unknown, related to server's 0x06
  190. (returns value sent from server)
  191. 0x08 - Keep alive? Sent after 0x03
  192. 0x09 - Availability check
  193. SERVER:
  194. 0x01 - Unknown
  195. Data sent:
  196. 8 random ASCII characters (?) followed by the public IP and
  197. port of the client as a hex string
  198. 0x06 - Unknown
  199. First 4 bytes is some kind of id? I believe it's a unique
  200. identifier for the data being sent, seeing how the server
  201. can send the same IP information many times in a row. If
  202. the IP information has already been parsed then it doesn't
  203. waste time handling it.
  204. After that is a "SBCM" section which is 0x14 bytes in
  205. total. SBCM information gets parsed at 2141A0C in Tetris DS
  206. overlay 10. Seems to contain IP address information.
  207. The SBCM seems to contain a little information that must be
  208. parsed before.
  209. After the SBCM:
  210. \x03\x00\x00\x00 - Always the same?
  211. \x01 - Found player?
  212. \x04 - Unknown
  213. (2 bytes) - Unknown. Port?
  214. (4 bytes) - Player's IP
  215. (4 bytes) - Unknown. Some other IP? Remote server IP?
  216. \x00\x00\x00\x00 - Unknown but seems to get checked
  217. Another SBCM, after a player has been found and attempting
  218. to start a game:
  219. \x03\x00\x00\x00 - Always the same?
  220. \x05 - Connecting to player?
  221. \x00 - Unknown
  222. (2 bytes) - Unknown. Port? Same as before.
  223. (4 bytes) - Player's IP
  224. (4 bytes) - Unknown. Some other IP? Remote server IP?
  225. 0x0a - Response to 0x01
  226. Gets sent after receiving 0x01 from the client.
  227. So, server 0x01 -> client 0x01 -> server 0x0a.
  228. Has no other data besides the client ID.
  229. - \xfd\xfc commands get passed directly between the other player(s)?
  230. Open source version of GameSpy found here:
  231. https://github.com/sfcspanky/Openspy-Core/tree/master/qr
  232. Use as reference.
  233. """
  234. session_id = None
  235. if recv_data[0] != '\x09':
  236. # Don't add a session if the client is trying to check if the game
  237. # is available or not
  238. session_id = struct.unpack("<I", recv_data[1:5])[0]
  239. session_id_raw = recv_data[1:5]
  240. if session_id not in self.sessions:
  241. # Found a new session, add to session list
  242. self.sessions[session_id] = self.Session(address)
  243. self.sessions[session_id].session = session_id
  244. self.sessions[session_id].keepalive = int(time.time())
  245. self.sessions[session_id].disconnected = False
  246. if session_id in self.sessions and \
  247. self.sessions[session_id].disconnected:
  248. return
  249. if session_id in self.sessions:
  250. # Make sure the server doesn't get removed
  251. self.sessions[session_id].keepalive = int(time.time())
  252. # Handle commands
  253. if recv_data[0] == '\x00': # Query
  254. self.log(logging.DEBUG, address, session_id,
  255. "NOT IMPLEMENTED! Received query from %s:%s... %s",
  256. address[0], address[1], recv_data[5:])
  257. elif recv_data[0] == '\x01': # Challenge
  258. self.log(logging.DEBUG, address, session_id,
  259. "Received challenge from %s:%s... %s",
  260. address[0], address[1], recv_data[5:])
  261. # Prepare the challenge sent from the server to be compared
  262. challenge = gs_utils.prepare_rc4_base64(
  263. self.sessions[session_id].secretkey,
  264. self.sessions[session_id].challenge
  265. )
  266. # Compare challenge
  267. client_challenge = recv_data[5:-1]
  268. if client_challenge == challenge:
  269. # Challenge succeeded
  270. # Send message back to client saying it was accepted
  271. # Send client registered command
  272. packet = bytearray([0xfe, 0xfd, 0x0a])
  273. packet.extend(session_id_raw) # Get the session ID
  274. self.write_queue.put((packet, address))
  275. self.log(logging.DEBUG, address, session_id,
  276. "Sent client registered to %s:%s...",
  277. address[0], address[1])
  278. if self.sessions[session_id].heartbeat_data is not None:
  279. self.update_server_list(
  280. session_id,
  281. self.sessions[session_id].heartbeat_data
  282. )
  283. else:
  284. # Failed the challenge, request another during the next
  285. # heartbeat
  286. self.sessions[session_id].sent_challenge = False
  287. self.server_manager.delete_server(
  288. self.sessions[session_id].gamename,
  289. session_id
  290. )
  291. elif recv_data[0] == '\x02': # Echo
  292. self.log(logging.DEBUG, address, session_id,
  293. "NOT IMPLEMENTED! Received echo from %s:%s... %s",
  294. address[0], address[1], recv_data[5:])
  295. elif recv_data[0] == '\x03': # Heartbeat
  296. data = recv_data[5:]
  297. self.log(logging.DEBUG, address, session_id,
  298. "Received heartbeat from %s:%s... %s",
  299. address[0], address[1], data)
  300. # Parse information from heartbeat here
  301. d = data.rstrip('\0').split('\0')
  302. # It may be safe to ignore "unknown" keys because the proper key
  303. # names get filled in later...
  304. k = {}
  305. for i in range(0, len(d), 2):
  306. # self.log(logging.DEBUG, address, session_id,
  307. # "%s = %s",
  308. # d[i], d[i + 1])
  309. k[d[i]] = d[i+1]
  310. if self.sessions[session_id].ingamesn is not None:
  311. if "gamename" in k and "dwc_pid" in k:
  312. try:
  313. profile = self.db.get_profile_from_profileid(
  314. k['dwc_pid']
  315. )
  316. naslogin = self.db.get_nas_login_from_userid(
  317. profile['userid']
  318. )
  319. # Convert to string from unicode (which is just a
  320. # base64 string anyway)
  321. self.sessions[session_id].ingamesn = \
  322. str(naslogin['ingamesn'])
  323. except Exception as e:
  324. # If the game doesn't have, don't worry about it.
  325. pass
  326. if self.sessions[session_id].ingamesn is not None and \
  327. "ingamesn" not in k:
  328. k['ingamesn'] = self.sessions[session_id].ingamesn
  329. if "gamename" in k:
  330. if k['gamename'] in self.secret_key_list:
  331. self.sessions[session_id].secretkey = \
  332. self.secret_key_list[k['gamename']]
  333. else:
  334. self.log(logging.INFO, address, session_id,
  335. "Connection from unknown game '%s'!",
  336. k['gamename'])
  337. if self.sessions[session_id].playerid == 0 and "dwc_pid" in k:
  338. # Get the player's id and then query the profile to figure
  339. # out what console they are on. The endianness of some server
  340. # data depends on the endianness of the console, so we must be
  341. # able to account for that.
  342. self.sessions[session_id].playerid = int(k['dwc_pid'])
  343. # Try to detect console without hitting the database first
  344. found_console = False
  345. if 'gamename' in k:
  346. self.sessions[session_id].console = 0
  347. if k['gamename'].endswith('ds') or \
  348. k['gamename'].endswith('dsam') or \
  349. k['gamename'].endswith('dsi') or \
  350. k['gamename'].endswith('dsiam'):
  351. self.sessions[session_id].console = 0
  352. found_console = True
  353. elif k['gamename'].endswith('wii') or \
  354. k['gamename'].endswith('wiiam') or \
  355. k['gamename'].endswith('wiiware') or \
  356. k['gamename'].endswith('wiiwaream'):
  357. self.sessions[session_id].console = 1
  358. found_console = True
  359. if found_console is False:
  360. # Couldn't detect game, try to get it from the database
  361. # Try a 3 times before giving up
  362. for i in range(0, 3):
  363. try:
  364. profile = self.db.get_profile_from_profileid(
  365. self.sessions[session_id].playerid
  366. )
  367. if "console" in profile:
  368. self.sessions[session_id].console = \
  369. profile['console']
  370. break
  371. except:
  372. time.sleep(0.5)
  373. if 'publicip' in k and k['publicip'] == "0":
  374. # and k['dwc_hoststate'] == "2":
  375. # When dwc_hoststate == 2 then it doesn't send an IP,
  376. # so calculate it ourselves
  377. be = self.sessions[session_id].console != 0
  378. k['publicip'] = str(utils.get_ip(
  379. bytearray([int(x) for x in address[0].split('.')]),
  380. 0,
  381. be
  382. ))
  383. if 'publicport' in k and \
  384. 'localport' in k and \
  385. k['publicport'] != k['localport']:
  386. self.log(logging.DEBUG, address, session_id,
  387. "publicport %s doesn't match localport %s,"
  388. " so changing publicport to %s...",
  389. k['publicport'], k['localport'],
  390. str(address[1]))
  391. k['publicport'] = str(address[1])
  392. if self.sessions[session_id].sent_challenge:
  393. self.update_server_list(session_id, k)
  394. else:
  395. addr_hex = ''.join(["%02X" % int(x)
  396. for x in address[0].split('.')])
  397. port_hex = "%04X" % int(address[1])
  398. server_challenge = utils.generate_random_str(6) + '00' + \
  399. addr_hex + port_hex
  400. self.sessions[session_id].challenge = server_challenge
  401. # Send challenge command
  402. packet = bytearray([0xfe, 0xfd, 0x01])
  403. # Get the session ID
  404. packet.extend(session_id_raw)
  405. packet.extend(server_challenge)
  406. packet.extend('\x00')
  407. self.write_queue.put((packet, address))
  408. self.log(logging.DEBUG, address, session_id,
  409. "Sent challenge to %s:%s...",
  410. address[0], address[1])
  411. self.sessions[session_id].sent_challenge = True
  412. self.sessions[session_id].heartbeat_data = k
  413. elif recv_data[0] == '\x04': # Add Error
  414. self.log(logging.WARNING, address, session_id,
  415. "NOT IMPLEMENTED! Received add error from %s:%s... %s",
  416. address[0], address[1], recv_data[5:])
  417. elif recv_data[0] == '\x05': # Echo Response
  418. self.log(logging.WARNING, address, session_id,
  419. "NOT IMPLEMENTED! Received echo response"
  420. " from %s:%s... %s",
  421. address[0], address[1], recv_data[5:])
  422. elif recv_data[0] == '\x06': # Client Message
  423. self.log(logging.WARNING, address, session_id,
  424. "NOT IMPLEMENTED! Received echo from %s:%s... %s",
  425. address[0], address[1], recv_data[5:])
  426. elif recv_data[0] == '\x07': # Client Message Ack
  427. # self.log(logging.WARNING, address, session_id,
  428. # "NOT IMPLEMENTED! Received client message ack"
  429. # " from %s:%s... %s",
  430. # address[0], address[1], recv_data[5:])
  431. self.log(logging.DEBUG, address, session_id,
  432. "Received client message ack from %s:%s...",
  433. address[0], address[1])
  434. elif recv_data[0] == '\x08': # Keep Alive
  435. self.log(logging.DEBUG, address, session_id,
  436. "Received keep alive from %s:%s...",
  437. address[0], address[1])
  438. self.sessions[session_id].keepalive = int(time.time())
  439. elif recv_data[0] == '\x09': # Available
  440. # Availability check only sent to *.available.gs.nintendowifi.net
  441. self.log(logging.DEBUG, address, session_id,
  442. "Received availability request for '%s' from %s:%s...",
  443. recv_data[5: -1], address[0], address[1])
  444. self.write_queue.put((
  445. bytearray([0xfe, 0xfd, 0x09, 0x00, 0x00, 0x00, 0x00]),
  446. address
  447. ))
  448. elif recv_data[0] == '\x0a': # Client Registered
  449. # Only sent to client, never received?
  450. self.log(logging.WARNING, address, session_id,
  451. "NOT IMPLEMENTED! Received client registered"
  452. " from %s:%s... %s",
  453. address[0], address[1], recv_data[5:])
  454. else:
  455. self.log(logging.ERROR, address, session_id,
  456. "Unknown request from %s:%s:",
  457. address[0], address[1])
  458. self.log(logging.DEBUG, address, session_id,
  459. "%s",
  460. utils.pretty_print_hex(recv_data))
  461. def keepalive_check(self):
  462. # self.log(logging.DEBUG, None, session_id,
  463. # "Keep alive check on %d sessions",
  464. # len(self.sessions))
  465. pruned = []
  466. now = int(time.time())
  467. for session_id in self.sessions:
  468. delta = now - self.sessions[session_id].keepalive
  469. timeout = 61 # Remove clients that haven't responded in x seconds
  470. if delta < 0 or delta >= timeout:
  471. pruned.append(session_id)
  472. self.server_manager.delete_server(
  473. self.sessions[session_id].gamename,
  474. self.sessions[session_id].session
  475. )
  476. self.log(logging.DEBUG, None, session_id,
  477. "Keep alive check removed %s:%s for game %s."
  478. " Client hasn't responded in %d seconds.",
  479. self.sessions[session_id].address[0],
  480. self.sessions[session_id].address[1],
  481. self.sessions[session_id].gamename,
  482. delta)
  483. for session_id in pruned:
  484. del self.sessions[session_id]
  485. if __name__ == "__main__":
  486. qr_server = GameSpyQRServer()
  487. qr_server.start()