pymaster/pymaster.py

203 lines
6.1 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
2016-01-19 21:32:16 +00:00
import socket
import random
import sys
import traceback
import logging
import os
from optparse import OptionParser
from struct import pack
from time import time
2016-01-19 21:32:16 +00:00
from server_entry import ServerEntry
from protocol import MasterProtocol
2016-01-19 21:32:16 +00:00
LOG_FILENAME = 'pymaster.log'
2016-01-19 21:32:16 +00:00
def logPrint( msg ):
logging.debug( msg )
2016-01-19 21:32:16 +00:00
class PyMaster:
def __init__(self, ip, port):
self.serverList = []
self.sock = socket.socket( socket.AF_INET, socket.SOCK_DGRAM )
self.sock.bind( (ip, port) )
2016-01-19 21:32:16 +00:00
logPrint("Welcome to PyMaster!")
logPrint("I ask you again, are you my master?")
logPrint("Running on %s:%d" % (ip, port))
def serverLoop(self):
data, addr = self.sock.recvfrom(1024)
data = data.decode('latin_1')
if( data[0] == MasterProtocol.clientQuery ):
self.clientQuery(data, addr)
elif( data[0] == MasterProtocol.challengeRequest ):
self.sendChallengeToServer(data, addr)
elif( data[0] == MasterProtocol.addServer ):
self.addServerToList(data, addr)
elif( data[0] == MasterProtocol.removeServer ):
self.removeServerFromList(data, addr)
else:
logPrint("Unknown message: {0} from {1}:{2}".format(data, addr[0], addr[1]))
2016-01-19 21:32:16 +00:00
def clientQuery(self, data, addr):
region = data[1] # UNUSED
data = data.strip('1' + region)
try:
query = data.split('\0')
except ValueError:
logPrint(traceback.format_exc())
return
queryAddr = query[0] # UNUSED
rawFilter = query[1]
# Remove first \ character
rawFilter = rawFilter.strip('\\')
split = rawFilter.split('\\')
# Use NoneType as undefined
2018-07-30 18:59:29 +00:00
gamedir = 'valve' # halflife, by default
2018-07-26 20:53:30 +00:00
clver = None
2017-03-23 21:30:03 +00:00
nat = 0
for i in range( 0, len(split), 2 ):
try:
key = split[i + 1]
if( split[i] == 'gamedir' ):
gamedir = key.lower() # keep gamedir in lowercase
2017-03-23 21:30:03 +00:00
elif( split[i] == 'nat' ):
nat = int(key)
2018-07-26 20:53:30 +00:00
elif( split[i] == 'clver' ):
clver = key
else:
2017-06-10 19:11:51 +00:00
logPrint('Unhandled info string entry: {0}/{1}. Infostring was: {2}'.format(split[i], key, split))
except IndexError:
pass
2018-07-26 20:53:30 +00:00
if( clver == None ): # Probably an old vulnerable version
2018-07-26 21:25:26 +00:00
self.fakeInfoForOldVersions( gamedir, addr )
2018-07-26 20:53:30 +00:00
return
packet = MasterProtocol.queryPacketHeader
2016-01-19 21:32:16 +00:00
for i in self.serverList:
if( time() > i.die ):
self.serverList.remove(i)
continue
if( not i.check ):
2016-01-19 21:32:16 +00:00
continue
2017-03-23 21:30:03 +00:00
if( nat != i.nat ):
continue
2016-01-19 21:32:16 +00:00
if( gamedir != None ):
2017-03-23 21:30:03 +00:00
if( gamedir != i.gamedir ):
2016-01-19 21:32:16 +00:00
continue
2017-03-23 21:30:03 +00:00
if( nat ):
2017-06-10 19:04:53 +00:00
reply = '\xff\xff\xff\xffc {0}:{1}'.format( addr[0], addr[1] )
data = reply.encode( 'latin_1' )
2017-03-23 21:30:03 +00:00
# Tell server to send info reply
2017-06-10 19:04:53 +00:00
self.sock.sendto( data, i.addr )
2017-03-23 21:30:03 +00:00
2016-01-19 21:32:16 +00:00
# Use pregenerated address string
packet += i.queryAddr
2016-01-22 10:54:23 +00:00
packet += b'\0\0\0\0\0\0' # Fill last IP:Port with \0
self.sock.sendto(packet, addr)
2018-07-26 20:53:30 +00:00
def fakeInfoForOldVersions(self, gamedir, addr):
2018-07-26 21:03:07 +00:00
def sendFakeInfo(sock, warnmsg, gamedir, addr):
2018-07-26 21:25:26 +00:00
baseReply = b"\xff\xff\xff\xffinfo\n\host\\" + warnmsg.encode('utf-8') + b"\map\\update\dm\\0\\team\\0\coop\\0\\numcl\\32\maxcl\\32\\gamedir\\" + gamedir.encode('latin-1') + b"\\"
sock.sendto(baseReply, addr)
sendFakeInfo(self.sock, "This version is not", gamedir, addr)
sendFakeInfo(self.sock, "supported anymore", gamedir, addr)
sendFakeInfo(self.sock, "Please update Xash3DFWGS", gamedir, addr)
sendFakeInfo(self.sock, "From GooglePlay or GitHub", gamedir, addr)
sendFakeInfo(self.sock, "Эта версия", gamedir, addr)
sendFakeInfo(self.sock, "устарела", gamedir, addr)
sendFakeInfo(self.sock, "Обновите Xash3DFWGS c", gamedir, addr)
sendFakeInfo(self.sock, "GooglePlay или GitHub", gamedir, addr)
2018-07-26 20:53:30 +00:00
def removeServerFromList(self, data, addr):
for i in self.serverList:
if (i.addr == addr):
logPrint("Remove Server: from {0}:{1}".format(addr[0], addr[1]))
self.serverList.remove(i)
2016-01-19 21:32:16 +00:00
def sendChallengeToServer(self, data, addr):
logPrint("Challenge Request: from {0}:{1}".format(addr[0], addr[1]))
# At first, remove old server- data from list
#self.removeServerFromList(None, addr)
count = 0
for i in self.serverList:
if ( i.addr[0] == addr[0] ):
if( i.addr[1] == addr[1] ):
self.serverList.remove(i)
else:
count += 1
if( count > 7 ):
return
2016-01-19 21:32:16 +00:00
# Generate a 32 bit challenge number
challenge = random.randint(0, 2**32-1)
2016-01-19 21:32:16 +00:00
# Add server to list
self.serverList.append(ServerEntry(addr, challenge))
2016-01-19 21:32:16 +00:00
# And send him a challenge
packet = MasterProtocol.challengePacketHeader
packet += pack('I', challenge)
self.sock.sendto(packet, addr)
2016-01-19 21:32:16 +00:00
def addServerToList(self, data, addr):
logPrint("Add Server: from {0}:{1}".format(addr[0], addr[1]))
# Remove the header. Just for better parsing.
2016-01-19 21:32:16 +00:00
serverInfo = data.strip('\x30\x0a\x5c')
# Find a server with same address
2016-01-19 21:32:16 +00:00
for serverEntry in self.serverList:
if( serverEntry.addr == addr ):
break
serverEntry.setInfoString( serverInfo )
def spawn_pymaster(verbose, ip, port):
if verbose:
logging.getLogger().addHandler(logging.StreamHandler())
logging.getLogger().addHandler(logging.FileHandler(LOG_FILENAME))
logging.getLogger().setLevel(logging.DEBUG)
masterMain = PyMaster(ip, port)
while True:
try:
masterMain.serverLoop()
except Exception:
logPrint(traceback.format_exc())
pass
2016-01-19 21:32:16 +00:00
if __name__ == "__main__":
parser = OptionParser()
parser.add_option('-i', '--ip', action='store', dest='ip', default='0.0.0.0',
help='ip to listen [default: %default]')
parser.add_option('-p', '--port', action='store', dest='port', type='int', default=27010,
help='port to listen [default: %default]')
parser.add_option('-d', '--daemonize', action='store_true', dest='daemonize', default=False,
help='run in background, argument is uid [default: %default]')
parser.add_option('-q', '--quiet', action='store_false', dest='verbose', default=True,
help='don\'t print to stdout [default: %default]')
(options, args) = parser.parse_args()
if options.daemonize != 0:
from daemon import pidfile, DaemonContext
with DaemonContext(stdout=sys.stdout, stderr=sys.stderr, working_directory=os.getcwd()) as context:
spawn_pymaster(options.verbose, options.ip, options.port)
else:
sys.exit(spawn_pymaster(options.verbose, options.ip, options.port))