gamespy_backend_server.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. """DWC Network Server Emulator
  2. Copyright (C) 2014 polaris-
  3. Copyright (C) 2015 Sepalani
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU Affero General Public License as
  6. published by the Free Software Foundation, either version 3 of the
  7. License, or (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU Affero General Public License for more details.
  12. You should have received a copy of the GNU Affero General Public License
  13. along with this program. If not, see <http://www.gnu.org/licenses/>.
  14. Master server list server
  15. Basic idea:
  16. The server listing does not need to be persistent, and it must be easily
  17. searchable for any unknown parameters. So instead of using a SQL database,
  18. I've opted to create a server list database server which communicates
  19. between the server browser and the qr server. The server list database
  20. will be stored in dictionaries as to allow dynamic columns that can be
  21. easily searched. The main reason for this configuration is because it
  22. cannot be guaranteed what data a game's server will required. For example,
  23. in addition to the common fields such as publicip, numplayers, dwc_pid,
  24. etc, Lost Magic also uses fields such as LMname, LMsecN, LMrating,
  25. LMbtmode, and LMversion.
  26. It would be possible to create game-specific databases but this would be
  27. more of a hassle and less universal. It would also be possible pickle a
  28. dictionary containing all of the fields and store it in a SQL database
  29. instead, but that would require unpickling every server each time you want
  30. to match search queries which would cause overhead if there are a lot of
  31. running servers. One trade off here is that we'll be using more memory by
  32. storing each server as a dictionary in the memory instead of storing it in
  33. a SQL database.
  34. - qr_server and server_browser both will act as clients to
  35. gs_server_database.
  36. - qr_server will send an add and/or delete to add or remove servers from the
  37. server list.
  38. - server_browser will send a request with the game name followed by optional
  39. search parameters to get a list of servers.
  40. """
  41. import logging
  42. import time
  43. import ast
  44. from multiprocessing.managers import BaseManager
  45. from multiprocessing import freeze_support
  46. from other.sql import sql_commands, LIKE
  47. import other.utils as utils
  48. import dwc_config
  49. logger = dwc_config.get_logger('GameSpyManager')
  50. class TokenType:
  51. UNKNOWN = 0
  52. FIELD = 1
  53. STRING = 2
  54. NUMBER = 3
  55. TOKEN = 4
  56. class GameSpyServerDatabase(BaseManager):
  57. pass
  58. class GameSpyBackendServer(object):
  59. def __init__(self):
  60. self.server_list = {}
  61. self.natneg_list = {}
  62. GameSpyServerDatabase.register(
  63. "get_server_list",
  64. callable=lambda: self.server_list
  65. )
  66. GameSpyServerDatabase.register(
  67. "find_servers",
  68. callable=self.find_servers
  69. )
  70. GameSpyServerDatabase.register(
  71. "find_server_by_address",
  72. callable=self.find_server_by_address
  73. )
  74. GameSpyServerDatabase.register(
  75. "find_server_by_local_address",
  76. callable=self.find_server_by_local_address
  77. )
  78. GameSpyServerDatabase.register(
  79. "update_server_list",
  80. callable=self.update_server_list
  81. )
  82. GameSpyServerDatabase.register(
  83. "delete_server",
  84. callable=self.delete_server
  85. )
  86. GameSpyServerDatabase.register(
  87. "add_natneg_server",
  88. callable=self.add_natneg_server
  89. )
  90. GameSpyServerDatabase.register(
  91. "get_natneg_server",
  92. callable=self.get_natneg_server
  93. )
  94. GameSpyServerDatabase.register(
  95. "delete_natneg_server",
  96. callable=self.delete_natneg_server
  97. )
  98. def start(self):
  99. address = dwc_config.get_ip_port('GameSpyManager')
  100. password = ""
  101. logger.log(logging.INFO,
  102. "Started server on %s:%d...",
  103. address[0], address[1])
  104. manager = GameSpyServerDatabase(address=address,
  105. authkey=password.encode())
  106. server = manager.get_server()
  107. server.serve_forever()
  108. def get_token(self, filters):
  109. """Complex example from Dungeon Explorer: Warriors of Ancient Arts
  110. dwc_mver = 3 and dwc_pid != 474890913 and maxplayers = 2 and
  111. numplayers < 2 and dwc_mtype = 0 and dwc_mresv != dwc_pid and
  112. (MatchType='english')
  113. Even more complex example from Phantasy Star Zero:
  114. dwc_mver = 3 and dwc_pid != 4 and maxplayers = 3 and
  115. numplayers < 3 and dwc_mtype = 0 and dwc_mresv != dwc_pid and
  116. (((20=auth)AND((1&mskdif)=mskdif)AND((14&mskstg)=mskstg)))
  117. Example with OR from Mario Kart Wii:
  118. dwc_mver = 90 and dwc_pid != 1 and maxplayers = 11 and
  119. numplayers < 11 and dwc_mtype = 0 and dwc_hoststate = 2 and
  120. dwc_suspend = 0 and (rk = 'vs_123' and (ev > 4263 or ev <= 5763)
  121. and p = 0)
  122. Example with LIKE from Fortune Street:
  123. dwc_mver = 90 and dwc_pid != 15 and maxplayers = 3 and
  124. numplayers < 3 and dwc_mtype = 0 and dwc_suspend = 0 and
  125. dwc_hoststate = 2 and ((zvar LIKE '102') AND (zmtp LIKE 'EU') AND
  126. (zrule LIKE '1') AND (zpnum LIKE '2') AND (zaddc LIKE '0') AND rel='1')
  127. """
  128. i = 0
  129. start = i
  130. special_chars = "_"
  131. token_type = TokenType.UNKNOWN
  132. # Skip whitespace
  133. while i < len(filters) and filters[i].isspace():
  134. i += 1
  135. start += 1
  136. if i < len(filters):
  137. if filters[i] == "(":
  138. i += 1
  139. token_type = TokenType.TOKEN
  140. elif filters[i] == ")":
  141. i += 1
  142. token_type = TokenType.TOKEN
  143. elif filters[i] == "&":
  144. i += 1
  145. token_type = TokenType.TOKEN
  146. elif filters[i] == "=":
  147. i += 1
  148. token_type = TokenType.TOKEN
  149. elif filters[i] == ">" or filters[i] == "<":
  150. i += 1
  151. token_type = TokenType.TOKEN
  152. if i < len(filters) and filters[i] == "=":
  153. # >= or <=
  154. i += 1
  155. elif i + 1 < len(filters) and filters[i] == "!" and \
  156. filters[i + 1] == "=":
  157. i += 2
  158. token_type = TokenType.TOKEN
  159. elif filters[i] == "'":
  160. # String literal
  161. token_type = TokenType.STRING
  162. i += 1 # Skip quotation mark
  163. while i < len(filters) and filters[i] != "'":
  164. i += 1
  165. if i < len(filters) and filters[i] == "'":
  166. i += 1 # Skip quotation mark
  167. elif filters[i] == "\"":
  168. # I don't know if it's in the spec or not, but I added ""
  169. # string literals as well just in case.
  170. token_type = TokenType.STRING
  171. i += 1 # Skip quotation mark
  172. while i < len(filters) and filters[i] != "\"":
  173. i += 1
  174. if i < len(filters) and filters[i] == "\"":
  175. i += 1 # Skip quotation mark
  176. elif i + 1 < len(filters) and filters[i] == '-' and \
  177. filters[i + 1].isdigit():
  178. # Negative number
  179. token_type = TokenType.NUMBER
  180. i += 1
  181. while i < len(filters) and filters[i].isdigit():
  182. i += 1
  183. elif filters[i].isalnum() or filters[i] in special_chars:
  184. # Whole numbers or words
  185. if filters[i].isdigit():
  186. token_type = TokenType.NUMBER
  187. elif filters[i].isalpha():
  188. token_type = TokenType.FIELD
  189. while i < len(filters) and (filters[i].isalnum() or
  190. filters[i] in special_chars) and \
  191. filters[i] not in "!=>< ":
  192. i += 1
  193. token = filters[start:i]
  194. if token_type == TokenType.FIELD and \
  195. (token.lower() == "and" or token.lower() == "or"):
  196. token = token.lower()
  197. return token, i, token_type
  198. def translate_expression(self, filters):
  199. output = []
  200. variables = []
  201. while len(filters) > 0:
  202. token, i, token_type = self.get_token(filters)
  203. filters = filters[i:]
  204. if token_type == TokenType.TOKEN:
  205. # Python uses == instead of = for comparisons, so replace
  206. # it with the proper token for compilation.
  207. if token == "=":
  208. token = "=="
  209. elif token_type == TokenType.FIELD:
  210. if token.upper() in sql_commands:
  211. # Convert "A SQL_COMMAND B" into "A |SQL_COMMAND| B"
  212. output.extend(["|", token.upper(), "|"])
  213. continue
  214. else:
  215. # Each server has its own variables so handle it later.
  216. variables.append(len(output))
  217. output.append(token)
  218. return output, variables
  219. def validate_ast(self, node, num_literal_only, is_sql=False):
  220. # This function tries to verify that the expression is a valid
  221. # expression before it gets evaluated.
  222. # Anything besides the whitelisted things below are strictly
  223. # forbidden:
  224. # - String literals
  225. # - Number literals
  226. # - Binary operators (CAN ONLY BE PERFORMED ON TWO NUMBER LITERALS)
  227. # - Comparisons (cannot use 'in', 'not in', 'is', 'is not' operators)
  228. #
  229. # Anything such as variables or arrays or function calls are NOT
  230. # VALID.
  231. # Never run the expression received from the client before running
  232. # this function on the expression first.
  233. # print type(node)
  234. # Only allow literals, comparisons, and math operations
  235. valid_node = False
  236. if isinstance(node, ast.Num):
  237. valid_node = True
  238. elif isinstance(node, ast.Str):
  239. if is_sql or not num_literal_only:
  240. valid_node = True
  241. elif isinstance(node, ast.BoolOp):
  242. for value in node.values:
  243. valid_node = self.validate_ast(value, num_literal_only)
  244. if not valid_node:
  245. break
  246. elif isinstance(node, ast.BinOp):
  247. # Allow SQL_COMMAND infix operator with more types
  248. is_sql |= \
  249. hasattr(node, "left") and \
  250. hasattr(node.left, "right") and \
  251. isinstance(node.left.right, ast.Name) and \
  252. node.left.right.id in sql_commands
  253. valid_node = self.validate_ast(node.left, True, is_sql)
  254. if valid_node:
  255. valid_node = self.validate_ast(node.right, True, is_sql)
  256. elif isinstance(node, ast.UnaryOp):
  257. valid_node = self.validate_ast(node.operand, num_literal_only)
  258. elif isinstance(node, ast.Expr):
  259. valid_node = self.validate_ast(node.value, num_literal_only)
  260. elif isinstance(node, ast.Compare):
  261. valid_node = self.validate_ast(node.left, num_literal_only)
  262. for op in node.ops:
  263. # print type(op)
  264. # Restrict "is", "is not", "in", and "not in" python
  265. # comparison operators. These are python-specific and the
  266. # games have no way of knowing what they are, so there's no
  267. # reason to keep them around.
  268. if isinstance(op, ast.Is) or isinstance(op, ast.IsNot) or \
  269. isinstance(op, ast.In) or isinstance(op, ast.NotIn):
  270. valid_node = False
  271. break
  272. if valid_node:
  273. for expr in node.comparators:
  274. valid_node = self.validate_ast(expr, num_literal_only)
  275. elif isinstance(node, ast.Call):
  276. valid_node = False
  277. elif isinstance(node, ast.Name):
  278. valid_node = node.id in sql_commands
  279. return valid_node
  280. def find_servers(self, gameid, filters, fields, max_count):
  281. matched_servers = []
  282. if gameid not in self.server_list:
  283. return []
  284. start = time.time()
  285. for server in self.server_list[gameid]:
  286. stop_search = False
  287. if filters:
  288. translated, variables = self.translate_expression(filters)
  289. for idx in variables:
  290. token = translated[idx]
  291. if token in server:
  292. token = server[token]
  293. _, _, token_type = self.get_token(token)
  294. if token_type == TokenType.FIELD:
  295. # At this point, any field should be a string.
  296. # This does not support stuff like:
  297. # dwc_test = 'test', dwc_test2 = dwc_test,
  298. # dwc_test3 = dwc_test2
  299. token = '"' + token + '"'
  300. elif token_type == TokenType.NUMBER:
  301. for idx2 in range(idx + 1, len(translated)):
  302. _, _, token_type = \
  303. self.get_token(translated[idx2])
  304. if token_type == TokenType.TOKEN and \
  305. translated[idx2] not in ('(', ')'):
  306. if idx2 == idx + 1:
  307. # Skip boolean operator if it's the
  308. # first token on the right
  309. continue
  310. # Boolean operator, leave left as integer
  311. token = str(int(token))
  312. break
  313. elif token_type == TokenType.STRING or \
  314. token_type == TokenType.NUMBER:
  315. if token_type == TokenType.STRING:
  316. # Found string on far right, turn left
  317. # into string as well
  318. token = "'" + token + "'"
  319. elif token_type == TokenType.NUMBER:
  320. token = str(int(token))
  321. break
  322. translated[idx] = token
  323. q = ' '.join(translated)
  324. # Always run validate_ast over the entire AST before
  325. # evaluating anything. eval() is dangerous to use on
  326. # unsanitized inputs. The validate_ast function has a fairly
  327. # strict whitelist so it should be safe in what it accepts as
  328. # valid.
  329. m = ast.parse(q, "<string>", "exec")
  330. valid_filter = True
  331. for node in m.body:
  332. valid_filter = self.validate_ast(node, False)
  333. if not valid_filter:
  334. # Return only anything matched up until this point.
  335. logger.log(logging.WARNING,
  336. "Invalid filter(s): %s",
  337. filters)
  338. # stop_search = True
  339. continue
  340. else:
  341. # Use Python to evaluate the query. This method may take a
  342. # little time but it shouldn't be all that big of a
  343. # difference, I think. It takes about 0.0004 seconds per
  344. # server to determine whether or not it's a match on my
  345. # computer. Usually there's a low max_servers set when the
  346. # game searches for servers, so assuming something like
  347. # the game is asking for 6 servers, it would take about
  348. # 0.0024 seconds total. These times will obviously be
  349. # different per computer. It's not ideal, but it shouldn't
  350. # be a huge bottleneck. A possible way to speed it up is
  351. # to make validate_ast also evaluate the expressions at
  352. # the same time as it validates it.
  353. result = eval(q)
  354. else:
  355. # There are no filters, so just return the server.
  356. result = True
  357. valid_filter = True
  358. if stop_search:
  359. break
  360. if valid_filter and result:
  361. matched_servers.append(server)
  362. if max_count and len(matched_servers) >= max_count:
  363. break
  364. servers = []
  365. for server in matched_servers:
  366. # Create a result with only the fields requested
  367. result = {}
  368. # Return all localips
  369. i = 0
  370. while 'localip' + str(i) in server:
  371. result['localip' + str(i)] = server['localip' + str(i)]
  372. i += 1
  373. attrs = [
  374. "localport", "natneg",
  375. "publicip", "publicport",
  376. "__session__", "__console__"
  377. ]
  378. result.update({name: server[name]
  379. for name in attrs if name in server})
  380. requested = {}
  381. for field in fields:
  382. # if not field in result:
  383. if field in server:
  384. requested[field] = server[field]
  385. else:
  386. # Return a dummy value. What's the normal behavior of
  387. # the real server in this case?
  388. requested[field] = ""
  389. result['requested'] = requested
  390. servers.append(result)
  391. logger.log(logging.DEBUG,
  392. "Matched %d servers in %s seconds",
  393. len(servers), (time.time() - start))
  394. return servers
  395. def update_server_list(self, gameid, session, value, console):
  396. """Make sure the user isn't hosting multiple servers or there isn't
  397. some left over server information that never got handled properly
  398. (game crashed, etc)."""
  399. self.delete_server(gameid, session)
  400. # If the game doesn't exist already, create a new list.
  401. if gameid not in self.server_list:
  402. self.server_list[gameid] = []
  403. # Add new server
  404. value['__session__'] = session
  405. value['__console__'] = console
  406. logger.log(logging.DEBUG,
  407. "Added %s to the server list for %s",
  408. value, gameid)
  409. self.server_list[gameid].append(value)
  410. logger.log(logging.DEBUG,
  411. "%s servers: %d",
  412. gameid, len(self.server_list[gameid]))
  413. return value
  414. def delete_server(self, gameid, session):
  415. if gameid not in self.server_list:
  416. # Nothing to do if no servers for that game even exist.
  417. return
  418. # Remove all servers hosted by the given session id.
  419. count = len(self.server_list[gameid])
  420. self.server_list[gameid] = [x for x in self.server_list[gameid]
  421. if x['__session__'] != session]
  422. count -= len(self.server_list[gameid])
  423. logger.log(logging.DEBUG,
  424. "Deleted %d %s servers where session = %d",
  425. count, gameid, session)
  426. def find_server_by_address(self, ip, port, gameid=None):
  427. if gameid is None:
  428. # Search all servers
  429. for gameid in self.server_list:
  430. for server in self.server_list[gameid]:
  431. if server['publicip'] == ip and \
  432. (not port or server['publicport'] == str(port)):
  433. return server
  434. else:
  435. for server in self.server_list[gameid]:
  436. if server['publicip'] == ip and \
  437. (not port or server['publicport'] == str(port)):
  438. return server
  439. return None
  440. def find_server_by_local_address(self, publicip, localaddr, gameid=None):
  441. localip = localaddr[0]
  442. localport = localaddr[1]
  443. localip_int_le = localaddr[2]
  444. localip_int_be = localaddr[3]
  445. def find_server(gameid):
  446. if gameid not in self.server_list:
  447. return None
  448. best_match = None
  449. for server in self.server_list[gameid]:
  450. logger.log(logging.DEBUG,
  451. "publicip: %s == %s ? %d localport: %s == %s ? %d",
  452. server['publicip'], publicip,
  453. server['publicip'] == publicip,
  454. server['localport'],
  455. str(localport),
  456. server['localport'] == str(localport))
  457. if server['publicip'] == publicip:
  458. if server['localport'] == str(localport):
  459. best_match = server
  460. break
  461. for x in range(0, 10):
  462. s = 'localip%d' % x
  463. if s in server:
  464. if server[s] == localip:
  465. best_match = server
  466. if not localport and best_match is None:
  467. # Kinda hackish. This sometimes happens.
  468. # Assuming two clients aren't trying to connect from
  469. # the same IP, this might be safe. The server wasn't
  470. # verified to be the *correct* server, but it's on the
  471. # same IP so it has a chance of being correct. At
  472. # least make an attempt to establish the connection.
  473. best_match = server
  474. if best_match is None:
  475. logger.log(logging.DEBUG,
  476. "Couldn't find a match for %s",
  477. publicip)
  478. return best_match
  479. if gameid is None:
  480. # Search all servers
  481. for gameid in self.server_list:
  482. return find_server(gameid)
  483. else:
  484. return find_server(gameid)
  485. return None
  486. def add_natneg_server(self, cookie, server):
  487. if cookie not in self.natneg_list:
  488. self.natneg_list[cookie] = []
  489. logger.log(logging.DEBUG, "Added natneg server %d", cookie)
  490. self.natneg_list[cookie].append(server)
  491. def get_natneg_server(self, cookie):
  492. if cookie in self.natneg_list:
  493. return self.natneg_list[cookie]
  494. return None
  495. def delete_natneg_server(self, cookie):
  496. """TODO: Find a good time to prune the natneg server listing."""
  497. if cookie in self.natneg_list:
  498. del self.natneg_list[cookie]
  499. logger.log(logging.DEBUG, "Deleted natneg server %d", cookie)
  500. if __name__ == '__main__':
  501. freeze_support()
  502. backend_server = GameSpyBackendServer()
  503. backend_server.start()