diff --git a/gamespy_backend_server.py b/gamespy_backend_server.py
index b1073ec..f74445d 100644
--- a/gamespy_backend_server.py
+++ b/gamespy_backend_server.py
@@ -16,25 +16,35 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
- Master server list server
+Master server list server
- Basic idea:
- The server listing does not need to be persistent, and it must be easily searchable for any unknown parameters.
- So instead of using a SQL database, I've opted to create a server list database server which communicates between the
- server browser and the qr server. The server list database will be stored in dictionaries as to allow dynamic columns
- that can be easily searched. The main reason for this configuration is because it cannot be guaranteed what data
- a game's server will required. For example, in addition to the common fields such as publicip, numplayers, dwc_pid, etc,
- Lost Magic also uses fields such as LMname, LMsecN, LMrating, LMbtmode, and LMversion.
+Basic idea:
+ The server listing does not need to be persistent, and it must be easily
+ searchable for any unknown parameters. So instead of using a SQL database,
+ I've opted to create a server list database server which communicates
+ between the server browser and the qr server. The server list database
+ will be stored in dictionaries as to allow dynamic columns that can be
+ easily searched. The main reason for this configuration is because it
+ cannot be guaranteed what data a game's server will required. For example,
+ in addition to the common fields such as publicip, numplayers, dwc_pid,
+ etc, Lost Magic also uses fields such as LMname, LMsecN, LMrating,
+ LMbtmode, and LMversion.
- It would be possible to create game-specific databases but this would be more of a hassle and less universal. It would
- also be possible pickle a dictionary containing all of the fields and store it in a SQL database instead, but that
- would require unpickling every server each time you want to match search queries which would cause overhead if there
- are a lot of running servers. One trade off here is that we'll be using more memory by storing each server as a
- dictionary in the memory instead of storing it in a SQL database.
+ It would be possible to create game-specific databases but this would be
+ more of a hassle and less universal. It would also be possible pickle a
+ dictionary containing all of the fields and store it in a SQL database
+ instead, but that would require unpickling every server each time you want
+ to match search queries which would cause overhead if there are a lot of
+ running servers. One trade off here is that we'll be using more memory by
+ storing each server as a dictionary in the memory instead of storing it in
+ a SQL database.
- qr_server and server_browser both will act as clients to gs_server_database.
- qr_server will send an add and/or delete to add or remove servers from the server list.
- server_browser will send a request with the game name followed by optional search parameters to get a list of servers.
+ - qr_server and server_browser both will act as clients to
+ gs_server_database.
+ - qr_server will send an add and/or delete to add or remove servers from the
+ server list.
+ - server_browser will send a request with the game name followed by optional
+ search parameters to get a list of servers.
"""
import logging
@@ -45,6 +55,7 @@ from multiprocessing.managers import BaseManager
from multiprocessing import freeze_support
import other.utils as utils
+
class TokenType:
UNKNOWN = 0
FIELD = 1
@@ -57,45 +68,86 @@ logger_output_to_console = True
logger_output_to_file = True
logger_name = "GamespyBackendServer"
logger_filename = "gamespy_backend_server.log"
-logger = utils.create_logger(logger_name, logger_filename, -1, logger_output_to_console, logger_output_to_file)
+logger = utils.create_logger(logger_name, logger_filename, -1,
+ logger_output_to_console, logger_output_to_file)
+
class GameSpyServerDatabase(BaseManager):
pass
+
class GameSpyBackendServer(object):
def __init__(self):
self.server_list = {}
self.natneg_list = {}
- GameSpyServerDatabase.register("get_server_list", callable=lambda:self.server_list)
- GameSpyServerDatabase.register("find_servers", callable=self.find_servers)
- GameSpyServerDatabase.register("find_server_by_address", callable=self.find_server_by_address)
- GameSpyServerDatabase.register("find_server_by_local_address", callable=self.find_server_by_local_address)
- GameSpyServerDatabase.register("update_server_list", callable=self.update_server_list)
- GameSpyServerDatabase.register("delete_server", callable=self.delete_server)
- GameSpyServerDatabase.register("add_natneg_server", callable=self.add_natneg_server)
- GameSpyServerDatabase.register("get_natneg_server", callable=self.get_natneg_server)
- GameSpyServerDatabase.register("delete_natneg_server", callable=self.delete_natneg_server)
+ GameSpyServerDatabase.register(
+ "get_server_list",
+ callable=lambda: self.server_list
+ )
+ GameSpyServerDatabase.register(
+ "find_servers",
+ callable=self.find_servers
+ )
+ GameSpyServerDatabase.register(
+ "find_server_by_address",
+ callable=self.find_server_by_address
+ )
+ GameSpyServerDatabase.register(
+ "find_server_by_local_address",
+ callable=self.find_server_by_local_address
+ )
+ GameSpyServerDatabase.register(
+ "update_server_list",
+ callable=self.update_server_list
+ )
+ GameSpyServerDatabase.register(
+ "delete_server",
+ callable=self.delete_server
+ )
+ GameSpyServerDatabase.register(
+ "add_natneg_server",
+ callable=self.add_natneg_server
+ )
+ GameSpyServerDatabase.register(
+ "get_natneg_server",
+ callable=self.get_natneg_server
+ )
+ GameSpyServerDatabase.register(
+ "delete_natneg_server",
+ callable=self.delete_natneg_server
+ )
def start(self):
address = ("127.0.0.1", 27500)
password = ""
- logger.log(logging.INFO, "Started server on %s:%d..." % (address[0], address[1]))
+ logger.log(logging.INFO,
+ "Started server on %s:%d...",
+ address[0], address[1])
- manager = GameSpyServerDatabase(address = address, authkey = password)
+ manager = GameSpyServerDatabase(address=address,
+ authkey=password)
server = manager.get_server()
server.serve_forever()
def get_token(self, filters):
- # Complex example from Dungeon Explorer: Warriors of Ancient Arts
- # dwc_mver = 3 and dwc_pid != 474890913 and maxplayers = 2 and numplayers < 2 and dwc_mtype = 0 and dwc_mresv != dwc_pid and (MatchType='english')
- #
- # Even more complex example from Phantasy Star Zero:
- # dwc_mver = 3 and dwc_pid != 4 and maxplayers = 3 and numplayers < 3 and dwc_mtype = 0 and dwc_mresv != dwc_pid and (((20=auth)AND((1&mskdif)=mskdif)AND((14&mskstg)=mskstg)))
- #
- # Example with OR from Mario Kart Wii:
- # dwc_mver = 90 and dwc_pid != 1 and maxplayers = 11 and numplayers < 11 and dwc_mtype = 0 and dwc_hoststate = 2 and dwc_suspend = 0 and (rk = 'vs_123' and (ev > 4263 or ev <= 5763) and p = 0)
+ """Complex example from Dungeon Explorer: Warriors of Ancient Arts
+ dwc_mver = 3 and dwc_pid != 474890913 and maxplayers = 2 and
+ numplayers < 2 and dwc_mtype = 0 and dwc_mresv != dwc_pid and
+ (MatchType='english')
+
+ Even more complex example from Phantasy Star Zero:
+ dwc_mver = 3 and dwc_pid != 4 and maxplayers = 3 and
+ numplayers < 3 and dwc_mtype = 0 and dwc_mresv != dwc_pid and
+ (((20=auth)AND((1&mskdif)=mskdif)AND((14&mskstg)=mskstg)))
+
+ Example with OR from Mario Kart Wii:
+ dwc_mver = 90 and dwc_pid != 1 and maxplayers = 11 and
+ numplayers < 11 and dwc_mtype = 0 and dwc_hoststate = 2 and
+ dwc_suspend = 0 and (rk = 'vs_123' and (ev > 4263 or ev <= 5763)
+ and p = 0)
+ """
i = 0
start = i
special_chars = "_"
@@ -132,7 +184,8 @@ class GameSpyBackendServer(object):
# >= or <=
i += 1
- elif i + 1 < len(filters) and filters[i] == "!" and filters[i + 1] == "=":
+ elif i + 1 < len(filters) and filters[i] == "!" and \
+ filters[i + 1] == "=":
i += 2
token_type = TokenType.TOKEN
@@ -140,25 +193,27 @@ class GameSpyBackendServer(object):
# String literal
token_type = TokenType.STRING
- i += 1 # Skip quotation mark
+ i += 1 # Skip quotation mark
while i < len(filters) and filters[i] != "'":
i += 1
if i < len(filters) and filters[i] == "'":
- i += 1 # Skip quotation mark
+ i += 1 # Skip quotation mark
elif filters[i] == "\"":
- # I don't know if it's in the spec or not, but I added "" string literals as well just in case.
+ # I don't know if it's in the spec or not, but I added ""
+ # string literals as well just in case.
token_type = TokenType.STRING
- i += 1 # Skip quotation mark
+ i += 1 # Skip quotation mark
while i < len(filters) and filters[i] != "\"":
i += 1
if i < len(filters) and filters[i] == "\"":
- i += 1 # Skip quotation mark
+ i += 1 # Skip quotation mark
- elif i + 1 < len(filters) and filters[i] == '-' and filters[i + 1].isdigit():
+ elif i + 1 < len(filters) and filters[i] == '-' and \
+ filters[i + 1].isdigit():
# Negative number
token_type = TokenType.NUMBER
i += 1
@@ -172,11 +227,14 @@ class GameSpyBackendServer(object):
elif filters[i].isalpha():
token_type = TokenType.FIELD
- while i < len(filters) and (filters[i].isalnum() or filters[i] in special_chars) and filters[i] not in "!=>< ":
+ while i < len(filters) and (filters[i].isalnum() or
+ filters[i] in special_chars) and \
+ filters[i] not in "!=>< ":
i += 1
token = filters[start:i]
- if token_type == TokenType.FIELD and (token.lower() == "and" or token.lower() == "or"):
+ if token_type == TokenType.FIELD and \
+ (token.lower() == "and" or token.lower() == "or"):
token = token.lower()
return token, i, token_type
@@ -190,7 +248,8 @@ class GameSpyBackendServer(object):
filters = filters[i:]
if token_type == TokenType.TOKEN:
- # Python uses == instead of = for comparisons, so replace it with the proper token for compilation.
+ # Python uses == instead of = for comparisons, so replace
+ # it with the proper token for compilation.
if token == "=":
token = "=="
@@ -203,16 +262,20 @@ class GameSpyBackendServer(object):
return output, variables
def validate_ast(self, node, num_literal_only):
- # This function tries to verify that the expression is a valid expression before it gets evaluated.
- # Anything besides the whitelisted things below are strictly forbidden:
+ # This function tries to verify that the expression is a valid
+ # expression before it gets evaluated.
+ # Anything besides the whitelisted things below are strictly
+ # forbidden:
# - String literals
# - Number literals
# - Binary operators (CAN ONLY BE PERFORMED ON TWO NUMBER LITERALS)
# - Comparisons (cannot use 'in', 'not in', 'is', 'is not' operators)
#
- # Anything such as variables or arrays or function calls are NOT VALID.
- # Never run the expression received from the client before running this function on the expression first.
- #print type(node)
+ # Anything such as variables or arrays or function calls are NOT
+ # VALID.
+ # Never run the expression received from the client before running
+ # this function on the expression first.
+ # print type(node)
# Only allow literals, comparisons, and math operations
valid_node = False
@@ -220,20 +283,20 @@ class GameSpyBackendServer(object):
valid_node = True
elif isinstance(node, ast.Str):
- if num_literal_only is False:
+ if not num_literal_only:
valid_node = True
elif isinstance(node, ast.BoolOp):
for value in node.values:
valid_node = self.validate_ast(value, num_literal_only)
- if valid_node is False:
+ if not valid_node:
break
elif isinstance(node, ast.BinOp):
valid_node = self.validate_ast(node.left, True)
- if valid_node == True:
+ if valid_node:
valid_node = self.validate_ast(node.right, True)
elif isinstance(node, ast.UnaryOp):
@@ -246,16 +309,18 @@ class GameSpyBackendServer(object):
valid_node = self.validate_ast(node.left, num_literal_only)
for op in node.ops:
- #print type(op)
+ # print type(op)
- # Restrict "is", "is not", "in", and "not in" python comparison operators.
- # These are python-specific and the games have no way of knowing what they are, so there's no reason
- # to keep them around.
- if isinstance(op, ast.Is) or isinstance(op, ast.IsNot) or isinstance(op, ast.In) or isinstance(op, ast.NotIn):
+ # Restrict "is", "is not", "in", and "not in" python
+ # comparison operators. These are python-specific and the
+ # games have no way of knowing what they are, so there's no
+ # reason to keep them around.
+ if isinstance(op, ast.Is) or isinstance(op, ast.IsNot) or \
+ isinstance(op, ast.In) or isinstance(op, ast.NotIn):
valid_node = False
break
- if valid_node == True:
+ if valid_node:
for expr in node.comparators:
valid_node = self.validate_ast(expr, num_literal_only)
@@ -264,7 +329,6 @@ class GameSpyBackendServer(object):
return valid_node
-
def find_servers(self, gameid, filters, fields, max_count):
matched_servers = []
@@ -276,7 +340,7 @@ class GameSpyBackendServer(object):
for server in self.server_list[gameid]:
stop_search = False
- if filters != "":
+ if filters:
translated, variables = self.translate_expression(filters)
for idx in variables:
@@ -289,69 +353,83 @@ class GameSpyBackendServer(object):
if token_type == TokenType.FIELD:
# At this point, any field should be a string.
# This does not support stuff like:
- # dwc_test = 'test', dwc_test2 = dwc_test, dwc_test3 = dwc_test2
+ # dwc_test = 'test', dwc_test2 = dwc_test,
+ # dwc_test3 = dwc_test2
token = '"' + token + '"'
elif token_type == TokenType.NUMBER:
for idx2 in range(idx + 1, len(translated)):
- _, _, token_type = self.get_token(translated[idx2])
+ _, _, token_type = \
+ self.get_token(translated[idx2])
- if token_type == TokenType.TOKEN and translated[idx2] not in ('(', ')'):
+ if token_type == TokenType.TOKEN and \
+ translated[idx2] not in ('(', ')'):
if idx2 == idx + 1:
- # Skip boolean operator if it's the first token on the right
+ # Skip boolean operator if it's the
+ # first token on the right
continue
# Boolean operator, leave left as integer
token = str(int(token))
break
- elif token_type == TokenType.STRING or token_type == TokenType.NUMBER:
+ elif token_type == TokenType.STRING or \
+ token_type == TokenType.NUMBER:
if token_type == TokenType.STRING:
- # Found string on far right, turn left into string as well
+ # Found string on far right, turn left
+ # into string as well
token = "'" + token + "'"
elif token_type == TokenType.NUMBER:
token = str(int(token))
break
-
translated[idx] = token
q = ' '.join(translated)
- # Always run validate_ast over the entire AST before evaluating anything. eval() is dangerous to use on
- # unsanitized inputs. The validate_ast function has a fairly strict whitelist so it should be safe in what
- # it accepts as valid.
+ # Always run validate_ast over the entire AST before
+ # evaluating anything. eval() is dangerous to use on
+ # unsanitized inputs. The validate_ast function has a fairly
+ # strict whitelist so it should be safe in what it accepts as
+ # valid.
m = ast.parse(q, "", "exec")
valid_filter = True
for node in m.body:
valid_filter = self.validate_ast(node, False)
- if valid_filter is False:
+ if not valid_filter:
# Return only anything matched up until this point.
- logger.log(logging.WARNING, "Invalid filter(s): %s" % (filters))
- #stop_search = True
+ logger.log(logging.WARNING,
+ "Invalid filter(s): %s",
+ filters)
+ # stop_search = True
continue
else:
- # Use Python to evaluate the query. This method may take a little time but it shouldn't be all that
- # big of a difference, I think. It takes about 0.0004 seconds per server to determine whether or not it's a
- # match on my computer. Usually there's a low max_servers set when the game searches for servers, so assuming
- # something like the game is asking for 6 servers, it would take about 0.0024 seconds total. These times
- # will obviously be different per computer. It's not ideal, but it shouldn't be a huge bottleneck.
- # A possible way to speed it up is to make validate_ast also evaluate the expressions at the same time as it
- # validates it.
+ # Use Python to evaluate the query. This method may take a
+ # little time but it shouldn't be all that big of a
+ # difference, I think. It takes about 0.0004 seconds per
+ # server to determine whether or not it's a match on my
+ # computer. Usually there's a low max_servers set when the
+ # game searches for servers, so assuming something like
+ # the game is asking for 6 servers, it would take about
+ # 0.0024 seconds total. These times will obviously be
+ # different per computer. It's not ideal, but it shouldn't
+ # be a huge bottleneck. A possible way to speed it up is
+ # to make validate_ast also evaluate the expressions at
+ # the same time as it validates it.
result = eval(q)
else:
# There are no filters, so just return the server.
result = True
valid_filter = True
- if stop_search == True:
+ if stop_search:
break
- if valid_filter == True and result == True:
+ if valid_filter and result:
matched_servers.append(server)
- if max_count != 0 and len(matched_servers) >= max_count:
+ if max_count and len(matched_servers) >= max_count:
break
servers = []
@@ -370,70 +448,83 @@ class GameSpyBackendServer(object):
"publicip", "publicport",
"__session__", "__console__"
]
- result.update({name:server[name] for name in attrs if name in server})
+ result.update({name: server[name]
+ for name in attrs if name in server})
requested = {}
for field in fields:
- #if not field in result:
+ # if not field in result:
if field in server:
requested[field] = server[field]
else:
- # Return a dummy value. What's the normal behavior of the real server in this case?
+ # Return a dummy value. What's the normal behavior of
+ # the real server in this case?
requested[field] = ""
-
result['requested'] = requested
servers.append(result)
- logger.log(logging.DEBUG, "Matched %d servers in %s seconds" % (len(servers), (time.time() - start)))
+ logger.log(logging.DEBUG,
+ "Matched %d servers in %s seconds",
+ len(servers), (time.time() - start))
return servers
def update_server_list(self, gameid, session, value, console):
- # Make sure the user isn't hosting multiple servers or there isn't some left over server information that
- # never got handled properly (game crashed, etc).
+ """Make sure the user isn't hosting multiple servers or there isn't
+ some left over server information that never got handled properly
+ (game crashed, etc)."""
self.delete_server(gameid, session)
# If the game doesn't exist already, create a new list.
- if not gameid in self.server_list:
+ if gameid not in self.server_list:
self.server_list[gameid] = []
# Add new server
value['__session__'] = session
value['__console__'] = console
- logger.log(logging.DEBUG, "Added %s to the server list for %s" % (value, gameid))
+ logger.log(logging.DEBUG,
+ "Added %s to the server list for %s",
+ value, gameid)
self.server_list[gameid].append(value)
- logger.log(logging.DEBUG, "%s servers: %d" % (gameid, len(self.server_list[gameid])))
+ logger.log(logging.DEBUG,
+ "%s servers: %d",
+ gameid, len(self.server_list[gameid]))
return value
def delete_server(self, gameid, session):
- if not gameid in self.server_list:
+ if gameid not in self.server_list:
# Nothing to do if no servers for that game even exist.
return
# Remove all servers hosted by the given session id.
count = len(self.server_list[gameid])
- self.server_list[gameid] = [x for x in self.server_list[gameid] if x['__session__'] != session]
+ self.server_list[gameid] = [x for x in self.server_list[gameid]
+ if x['__session__'] != session]
count -= len(self.server_list[gameid])
- logger.log(logging.DEBUG, "Deleted %d %s servers where session = %d" % (count, gameid, session))
+ logger.log(logging.DEBUG,
+ "Deleted %d %s servers where session = %d",
+ count, gameid, session)
- def find_server_by_address(self, ip, port, gameid = None):
+ def find_server_by_address(self, ip, port, gameid=None):
if gameid is None:
# Search all servers
for gameid in self.server_list:
for server in self.server_list[gameid]:
- if server['publicip'] == ip and (port == 0 or server['publicport'] == str(port)):
+ if server['publicip'] == ip and \
+ (not port or server['publicport'] == str(port)):
return server
else:
for server in self.server_list[gameid]:
- if server['publicip'] == ip and (port == 0 or server['publicport'] == str(port)):
+ if server['publicip'] == ip and \
+ (not port or server['publicport'] == str(port)):
return server
return None
- def find_server_by_local_address(self, publicip, localaddr, gameid = None):
+ def find_server_by_local_address(self, publicip, localaddr, gameid=None):
localip = localaddr[0]
localport = localaddr[1]
localip_int_le = localaddr[2]
@@ -443,27 +534,37 @@ class GameSpyBackendServer(object):
best_match = None
for server in self.server_list[gameid]:
- logger.log(logging.DEBUG, "publicip: %s == %s ? %d localport: %s == %s ? %d" % (server['publicip'], publicip, server['publicip'] == publicip, server['localport'], str(localport), server['localport'] == str(localport)))
+ logger.log(logging.DEBUG,
+ "publicip: %s == %s ? %d localport: %s == %s ? %d",
+ server['publicip'], publicip,
+ server['publicip'] == publicip,
+ server['localport'],
+ str(localport),
+ server['localport'] == str(localport))
if server['publicip'] == publicip:
if server['localport'] == str(localport):
best_match = server
break
for x in range(0, 10):
- s = 'localip%d' % x
- if s in server:
- if server[s] == localip:
- best_match = server
+ s = 'localip%d' % x
+ if s in server:
+ if server[s] == localip:
+ best_match = server
- if localport == 0 and best_match is None:
+ if not localport and best_match is None:
# Kinda hackish. This sometimes happens.
- # Assuming two clients aren't trying to connect from the same IP, this might be safe.
- # The server wasn't verified to be the *correct* server, but it's on the same IP so it
- # has a chance of being correct. At least make an attempt to establish the connection.
+ # Assuming two clients aren't trying to connect from
+ # the same IP, this might be safe. The server wasn't
+ # verified to be the *correct* server, but it's on the
+ # same IP so it has a chance of being correct. At
+ # least make an attempt to establish the connection.
best_match = server
if best_match is None:
- logger.log(logging.DEBUG, "Couldn't find a match for %s" % (publicip))
+ logger.log(logging.DEBUG,
+ "Couldn't find a match for %s",
+ publicip)
return best_match
@@ -480,7 +581,7 @@ class GameSpyBackendServer(object):
if cookie not in self.natneg_list:
self.natneg_list[cookie] = []
- logger.log(logging.DEBUG, "Added natneg server %d" % (cookie))
+ logger.log(logging.DEBUG, "Added natneg server %d", cookie)
self.natneg_list[cookie].append(server)
def get_natneg_server(self, cookie):
@@ -490,10 +591,10 @@ class GameSpyBackendServer(object):
return None
def delete_natneg_server(self, cookie):
- # TODO: Find a good time to prune the natneg server listing.
+ """TODO: Find a good time to prune the natneg server listing."""
if cookie in self.natneg_list:
del self.natneg_list[cookie]
- logger.log(logging.DEBUG, "Deleted natneg server %d" % (cookie))
+ logger.log(logging.DEBUG, "Deleted natneg server %d", cookie)
if __name__ == '__main__':