diff --git a/protocol.py b/protocol.py new file mode 100644 index 0000000..80e27b4 --- /dev/null +++ b/protocol.py @@ -0,0 +1,20 @@ +class MasterProtocol: + + # Client To Master + clientQuery = '1' + + # Server To Master + challengeRequest = 'q' + addServer = '0' + removeServer = 'b' + + # Master To Client + queryPacketHeader = b'\xff\xff\xff\xff\x66\x0a' + + # Master To Server + challengePacketHeader = b'\xff\xff\xff\xff\x73\x0a' + + #Debug + statusRequest = 's' + + \ No newline at end of file diff --git a/pymaster.py b/pymaster.py old mode 100644 new mode 100755 index 56664da..da1e49c --- a/pymaster.py +++ b/pymaster.py @@ -1,59 +1,81 @@ +# Basic networking import socket + +# Challenge generator import random -import struct + +# System important... things import sys +import traceback +import logging + +# Network packet creating +from struct import pack + +# Server time control +from time import time + +# ServerEntry class module from server_entry import ServerEntry -from util import getAttrOrNone +# Protocol class +from protocol import MasterProtocol UDP_IP = "127.0.0.1" UDP_PORT = 27010 - -sock = [] +LOG_FILENAME = 'pymaster.log' +logging.basicConfig( filename = LOG_FILENAME, level = logging.DEBUG ) def logPrint( msg ): - if DEBUG: - print( msg ) + logging.debug( msg ) class PyMaster: - self.serverList = [] - self.sock = [] + serverList = [] + sock = socket.socket( socket.AF_INET, socket.SOCK_DGRAM ) def __init__(self): - sock = socket.socket( socket.AF_INET, socket.SOCK_DGRAM ) - sock.bind( (UDP_IP, UDP_PORT) ) + self.sock.bind( (UDP_IP, UDP_PORT) ) - logprint("Welcome to PyMaster!") - logprint("I ask you again, are you my master?") - logprint("Running on %s:%i" % UDP_IP, UDP_PORT) + logPrint("Welcome to PyMaster!") + logPrint("I ask you again, are you my master?") + logPrint("Running on {0}:{1}".format( UDP_IP, UDP_PORT)) - def serverLoop(self) - data, addr = sock.recvfrom(1024)4 - logprint("received message: %s" % data) + def serverLoop(self): + data, addr = self.sock.recvfrom(1024) + data = data.decode('latin_1') - if data[0] == '1': + if( data[0] == MasterProtocol.clientQuery ): + logPrint("Client Query: from {0}:{1}".format(addr[0], addr[1])) self.clientQuery(data, addr); - elif data[0] == 'q' + elif( data[0] == MasterProtocol.challengeRequest ): + logPrint("Challenge Request: from {0}:{1}".format(addr[0], addr[1])) self.sendChallengeToServer(data, addr); - elif data[0] == '0' + elif( data[0] == MasterProtocol.addServer ): + logPrint("Add Server: from {0}:{1}".format(addr[0], addr[1])) self.addServerToList(data, addr); - elif data[0] == 'b' + elif( data[0] == MasterProtocol.removeServer ): + logPrint("Remove Server: from {0}:{1}".format(addr[0], addr[1])) self.removeServerFromList(data, addr); - else - logprint("Unknown message: %s from %s" % data, addr) + elif( data[0] == MasterProtocol.statusRequest ): + logPrint("Status Request: from {0}:{1}".format(addr[0], addr[1])) + self.sendStatus(data, addr); + else: + logPrint("Unknown message: {0} from {1}:{2}".format(data, addr[0], addr[1])) def clientQuery(self, data, addr): - data = data.strip('1') + data = data.strip('1\xff') region = data[0] - queryAddr, rawFilter = data.split('\0') + try: + queryAddr, rawFilter = data.split('\0') + except ValueError: + return + + rawFilter = rawFilter.strip('\\') + split = rawFilter.split('\\') - rawFilter = rawFilter.split('\\') - queryFilter = namedtuple('QueryFilter', rawFilter[0::2])._make(rawFilter[1::2]) - #nor = getAttrOrNone(queryFilter, 'nor') #nand = getAttrOrNone(queryFilter, 'nand') #dedicated = getAttrOrNone(queryFilter, 'dedicated') - #gamedir = getAttrOrNone(queryFilter, 'gamedir') #gamemap = getAttrOrNone(queryFilter, 'map') #linux = getAttrOrNone(queryFilter, 'linux') #empty = getAttrOrNone(queryFilter, 'empty') @@ -64,21 +86,51 @@ class PyMaster: #name = getAttrOrNone(queryFilter, 'name') #version = getAttrOrNone(queryFilter, 'version_match') #gameaddr = getAttrOrNone(queryFilter, 'gameaddr') - secure = getAttrOrNone(queryFilter, 'secure') + #secure = getAttrOrNone(queryFilter, 'secure') + + # Use NoneType as undefined + gamedir = None + gamemap = None + + for i in range( 0, len(split), 2 ): + try: + key = split[i + 1] + if( split[i] == 'gamedir' ): + gamedir = key + elif( split[i] == 'map' ): + gamemap = key + else: + logPrint('Unhandled info string entry: {0}/{1}'.format(split[i], key)) + except IndexError: + pass - packet = '\xff\xff\xff\xff\x66\x0a' + packet = MasterProtocol.queryPacketHeader for i in self.serverList: - if( !i.check ): + if( time() > i.die ): + self.serverList.remove(i) + continue + + if( not i.check ): continue if( gamedir != None ): - if( gamedir != i.serverInfo.gamedir): + if( gamedir != i.gamedir): continue # Use pregenerated address string - packet += self.queryAddr + packet += i.queryAddr + + self.sock.sendto(packet, addr) + + def removeServerFromList(self, data, addr): + for i in self.serverList: + if (i.addr == addr): + self.serverList.remove(i) def sendChallengeToServer(self, data, addr): + # At first, remove old server data from list + self.removeServerFromList(None, addr) + # Generate a 32 bit challenge number challenge = random.randint(0, 2**32-1) @@ -86,23 +138,30 @@ class PyMaster: self.serverList.append(ServerEntry(addr, challenge)) # And send him a challenge - packet = '\xff\xff\xff\xff\x73\x0a' - packet += struct.pack('I', challenge) - socket.sendto(packet, addr) + packet = MasterProtocol.challengePacketHeader + packet += pack('I', challenge) + self.sock.sendto(packet, addr) def addServerToList(self, data, addr): - # Remove the header + # Remove the header. Just for better parsing. serverInfo = data.strip('\x30\x0a\x5c') - # Find a server with + # Find a server with same address for serverEntry in self.serverList: - if (serverEntry.addr == addr): - serverEntry.setInfoString(serverInfo) + if( serverEntry.addr == addr ): + serverEntry.setInfoString( serverInfo ) - def removeServerFromList(self, data, addr): + def sendStatus( self, data, addr ): + count = len(self.serverList) + + packet = b'Server\t\t\tGame\tMap\tPlayers\tVersion\tChallenge\tCheck\n' for i in self.serverList: - if (i.addr == addr): - self.serverList.remove(i) + line = '{0}:{1}\t{2}\t{3}\t{4}/{5}\t{6}\n'.format(i.addr[0], i.addr[1], + i.gamedir, i.gamemap, i.players, + i.maxplayers, i.version, i.challenge, i.check) + packet += line.encode('latin_1') + self.sock.sendto(packet, addr) + def main( argv = None ): if argv is None: @@ -110,7 +169,11 @@ def main( argv = None ): masterMain = PyMaster() while True: - masterMain.serverLoop() + try: + masterMain.serverLoop() + except Exception: + logging.exception() + pass if __name__ == "__main__": sys.exit( main( ) ) \ No newline at end of file diff --git a/server_entry.py b/server_entry.py index 9e0b57d..997b958 100644 --- a/server_entry.py +++ b/server_entry.py @@ -1,22 +1,73 @@ -from collections import namedtuple from time import time +from struct import pack class ServerEntry: + challenge2 = 0 + gamedir = '' + protocol = 0 + players = 0 + maxplayers = 0 + bots = 0 + gamemap = '' + version = '0' + servtype = 'd' + password = 0 + os = 'l' + secure = 0 + lan = 0 + region = 255 + product = '' + def setInfoString(self, data): - infostring = data.translate(None, '\n\r\0') + infostring = data.replace('\n', '').replace('\r', '').replace('\0', '') split = infostring.split('\\') - self.serverInfo = namedtuple('ServerInfo', split[0::2])._make(split[1::2]) - self.check = int(self.serverInfo.challenge) == self.challenge + logPrint( split ) + for i in range(0, len(split), 2): + try: + key = split[i + 1] + if( split[i] == 'challenge' ): + self.challenge2 = int(key) + elif( split[i] == 'gamedir' ): + self.gamedir = key + elif( split[i] == 'protocol' ): + self.protocol = int(key) + elif( split[i] == 'players' ): + self.players = int(key) + elif( split[i] == 'max' ): + self.maxplayers = int(key) + elif( split[i] == 'bots' ): + self.bots = int(key) + elif( split[i] == 'map' ): + self.gamemap = key + elif( split[i] == 'version' ): + self.version = key + elif( split[i] == 'type' ): + self.servtype = key + elif( split[i] == 'password' ): + self.password = key + elif( split[i] == 'os' ): + self.os = key + elif( split[i] == 'secure' ): + self.secure = key + elif( split[i] == 'lan' ): + self.lan = key + elif( split[i] == 'region' ): + self.region = key + elif( split[i] == 'product' ): + self.product = key + except IndexError: + pass + self.check = self.challenge == self.challenge2 def __init__(self, addr, challenge): # Address self.addr = addr # Shortcuts for generating query - self.queryAddr = "" + self.queryAddr = b'' for i in addr[0].split('.'): - self.queryAddr += struct.pack('B', int(i)) - self.queryAddr += struct.pack('H', addr[1]) + self.queryAddr += pack('B', int(i)) + self.queryAddr += pack('H', addr[1]) # Random number that server must return self.challenge = challenge diff --git a/util.py b/util.py index 475dc7c..e69de29 100644 --- a/util.py +++ b/util.py @@ -1,2 +0,0 @@ -def getAttrOrNone(obj, prop): - return getattr(obj, prop, None) \ No newline at end of file