391 lines
11 KiB
Python
Executable File
391 lines
11 KiB
Python
Executable File
#!/usr/bin/env python
|
|
#
|
|
# This file is part of the Warzone 2100 Resurrection Project.
|
|
# Copyright (c) 2007-2009 Warzone 2100 Resurrection Project
|
|
#
|
|
# Warzone 2100 is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with Warzone 2100; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
#
|
|
# --------------------------------------------------------------------------
|
|
# MasterServer v0.1 by Gerard Krol (gerard_) and Tim Perrei (Kamaze)
|
|
# v1.0 by Freddie Witherden (EvilGuru)
|
|
# v2.0 by Gerhard Schaden (gschaden)
|
|
# v2.0a by Buginator (fixed endian issue)
|
|
# v2.2 add motd, bigger GAMESTRUCT, private game notification
|
|
# --------------------------------------------------------------------------
|
|
#
|
|
################################################################################
|
|
# import from __future__
|
|
from __future__ import with_statement
|
|
|
|
#
|
|
################################################################################
|
|
#
|
|
|
|
__author__ = "Gerard Krol, Tim Perrei, Freddie Witherden, Gerhard Schaden, Dennis Schridde, Buginator"
|
|
__version__ = "2.2"
|
|
__bpydoc__ = """\
|
|
This script runs a Warzone 2100 2.2.x+ masterserver
|
|
"""
|
|
|
|
#
|
|
################################################################################
|
|
# Get the things we need.
|
|
import sys
|
|
from SocketServer import ThreadingTCPServer
|
|
import SocketServer
|
|
import struct
|
|
import socket
|
|
from threading import Lock, RLock, Thread, Event, Timer
|
|
from select import select
|
|
import logging
|
|
import traceback
|
|
import cmd
|
|
from game import *
|
|
from protocol import *
|
|
#
|
|
################################################################################
|
|
# Settings.
|
|
|
|
checkInterval = 100 # Interval between requests causing a gamedb check
|
|
|
|
MOTDstring = None # our message of the day (max 255 bytes)
|
|
|
|
logging.basicConfig(level = logging.DEBUG, format = "%(asctime)-15s %(levelname)s %(message)s")
|
|
|
|
#
|
|
################################################################################
|
|
# Game DB
|
|
|
|
gamedb = None
|
|
|
|
class MultiAddressGame(Game):
|
|
def __init__(self, gameId):
|
|
super(MultiAddressGame, self).__init__()
|
|
self.addressCount = 0
|
|
self.gameId = gameId
|
|
|
|
assert self.gameId > 0, "Invalid game ID"
|
|
|
|
class GameDB:
|
|
def __init__(self):
|
|
self.list = set()
|
|
self.index = {}
|
|
self.lock = RLock()
|
|
self.numgen = self.genNum()
|
|
|
|
def genNum(self):
|
|
while True:
|
|
for n in xrange(1, pow(2, 31) - 1):
|
|
if n != 0xBAD01 and n not in self.index:
|
|
yield n
|
|
|
|
def __remove(self, g):
|
|
# if g is not in the list, ignore the KeyError exception
|
|
try:
|
|
self.list.remove(g)
|
|
except KeyError:
|
|
pass
|
|
|
|
def addGame(self, g):
|
|
""" add a game """
|
|
with self.lock:
|
|
try:
|
|
g.addressCount += 1
|
|
except AttributeError:
|
|
self.list.add(g)
|
|
|
|
def removeGame(self, g):
|
|
""" remove a game from the list"""
|
|
with self.lock:
|
|
try:
|
|
if g.addressCount == 0:
|
|
self.__remove(g)
|
|
del self.index[g.gameId]
|
|
else:
|
|
g.addressCount -= 1
|
|
except AttributeError:
|
|
self.__remove(g)
|
|
|
|
def getGameID(self):
|
|
with self.lock:
|
|
g = MultiAddressGame(self.numgen.next())
|
|
|
|
self.list.add(g)
|
|
self.index[g.gameId] = g
|
|
|
|
return g
|
|
|
|
def getGameByID(self, gameId):
|
|
with self.lock:
|
|
return self.index[gameId]
|
|
|
|
# only games with a valid description
|
|
def getGames(self):
|
|
""" filter all games with a valid description """
|
|
for game in self.getAllGames():
|
|
if game.description:
|
|
yield game
|
|
|
|
def getAllGames(self):
|
|
""" return all known games """
|
|
with self.lock:
|
|
for game in self.list:
|
|
yield game
|
|
|
|
def getGamesByHost(self, host):
|
|
""" filter all games of a certain host"""
|
|
for game in self.getGames():
|
|
if host in game.hosts:
|
|
yield game
|
|
|
|
def checkGames(self):
|
|
with self.lock:
|
|
games = list(self.getGames())
|
|
logging.debug("Checking: %i game(s)" % (len(games)))
|
|
for game in games:
|
|
for host in game.hosts:
|
|
if not host:
|
|
continue
|
|
|
|
if not protocol.check(game, host):
|
|
logging.debug("Removing unreachable game: %s" % game)
|
|
self.__remove(game)
|
|
break
|
|
|
|
def listGames(self):
|
|
with self.lock:
|
|
games = list(self.getGames())
|
|
logging.debug("Gameserver list: %i game(s)" % (len(games)))
|
|
for game in games:
|
|
logging.debug(" %s" % game)
|
|
|
|
#
|
|
################################################################################
|
|
# Socket Handler.
|
|
|
|
requests = 0
|
|
requestlock = Lock()
|
|
protocol = Protocol()
|
|
|
|
class RequestHandler(SocketServer.ThreadingMixIn, SocketServer.StreamRequestHandler):
|
|
SUCCESS_OK = 200
|
|
|
|
CLIENT_ERROR_NOT_ACCEPTABLE = 406
|
|
|
|
def sendStatusMessage(self, status, message):
|
|
logging.debug("(%s) Sending response status (%d) and message: %s" % (self.gameHost, status, message))
|
|
try:
|
|
protocol.sendStatusMessage(self.gameHost, status, message, self.wfile)
|
|
except NotImplementedError:
|
|
# Ignore the case where sending of status messages isn't supported
|
|
pass
|
|
|
|
def __init__(self, request, client_address, server):
|
|
self.request = request
|
|
self.client_address = client_address
|
|
self.server = server
|
|
|
|
# Client's address
|
|
self.gameHost = self.client_address[0]
|
|
|
|
try:
|
|
self.setup()
|
|
try:
|
|
self.handle()
|
|
except struct.error, e:
|
|
logging.warning("(%s) Data decoding error: %s" % (self.gameHost, traceback.format_exc()))
|
|
except:
|
|
logging.error("(%s) Unhandled exception: %s" % (self.gameHost, traceback.format_exc()))
|
|
raise
|
|
finally:
|
|
self.finish()
|
|
finally:
|
|
sys.exc_traceback = None # Help garbage collection
|
|
|
|
def setup(self):
|
|
for parent in (SocketServer.ThreadingMixIn, SocketServer.StreamRequestHandler):
|
|
if hasattr(parent, 'setup'):
|
|
parent.setup(self)
|
|
self.g = None
|
|
self.GameId = None
|
|
|
|
def handle(self):
|
|
global requests, requestlock, gamedb
|
|
|
|
with requestlock:
|
|
requests += 1
|
|
if requests >= checkInterval:
|
|
gamedb.checkGames()
|
|
requests = 0
|
|
|
|
while True:
|
|
# Read the incoming command.
|
|
netCommand = self.rfile.read(4)
|
|
if netCommand == None:
|
|
return
|
|
|
|
# Skip the trailing NULL.
|
|
self.rfile.read(1)
|
|
|
|
#################################
|
|
# Process the incoming command. #
|
|
#################################
|
|
|
|
logging.debug("(%s) Command: [%s] " % (self.gameHost, netCommand))
|
|
# TODO: When releasing a new 2.2 version remove the "motd" command
|
|
# Give the MOTD
|
|
if netCommand == 'motd':
|
|
self.wfile.write(MOTDstring[0:255])
|
|
logging.debug("(%s) sending MOTD (%s)" % (self.gameHost, MOTDstring[0:255]))
|
|
|
|
# Get a game ID (for multi address games)
|
|
elif netCommand == 'gaId':
|
|
self.GameId = gamedb.getGameID()
|
|
self.GameId.requestHandlers = [self]
|
|
self.GameId.hosts.append(self.gameHost)
|
|
logging.debug("(%s) Created game ID: %d" % (self.gameHost, self.GameId.gameId))
|
|
protocol.encodeGameID(self.GameId.gameId, self.wfile)
|
|
# Add a game.
|
|
elif netCommand == 'addg':
|
|
# The host is valid
|
|
logging.debug("(%s) Adding gameserver..." % self.gameHost)
|
|
if self.GameId:
|
|
self.g = self.GameId
|
|
if self.g.requestHandlers[0] is not self:
|
|
self.g.requestHandlers.append(self)
|
|
else:
|
|
# create a game object
|
|
self.g = Game()
|
|
self.g.requestHandlers = [self]
|
|
# put it in the database
|
|
gamedb.addGame(self.g)
|
|
|
|
# and start receiving updates about the game
|
|
while True:
|
|
hosts = self.g.hosts
|
|
newGameData = protocol.decodeSingle(self.rfile, self.g)
|
|
if not newGameData:
|
|
return
|
|
self.g.hosts = hosts
|
|
|
|
if not hasattr(self.g, 'addressCount') and self.g.gameId > 0:
|
|
try:
|
|
self.GameId = gamedb.getGameByID(self.g.gameId)
|
|
logging.debug("(%s) Game with same ID already present, linking games...", (self.gameHost))
|
|
|
|
# Apparently a game with this ID exists already, so remove this duplicate game
|
|
gamedb.removeGame(self.g)
|
|
self.g = None
|
|
|
|
# Attach this request handler to the existing game
|
|
(self.g, self.GameId) = (self.GameId, self.g)
|
|
gamedb.addGame(self.g)
|
|
self.g.hosts.append(self.gameHost)
|
|
except KeyError:
|
|
pass
|
|
|
|
logging.debug("(%s) Updated game: %s" % (self.gameHost, self.g))
|
|
#set gamehost
|
|
if not len(self.g.hosts) or not self.g.hosts[0]:
|
|
self.g.hosts.append(self.gameHost)
|
|
|
|
if not protocol.check(self.g, self.gameHost):
|
|
logging.debug("(%s) Removing unreachable game" % self.gameHost)
|
|
self.sendStatusMessage(self.CLIENT_ERROR_NOT_ACCEPTABLE, 'Game unreachable, failed to open a connection to: [%s]:%d' % (self.gameHost, protocol.gamePort))
|
|
return
|
|
|
|
self.sendStatusMessage(self.SUCCESS_OK, MOTDstring)
|
|
gamedb.listGames()
|
|
return
|
|
# Get a game list.
|
|
elif netCommand == 'list':
|
|
# Lock the gamelist to prevent new games while output.
|
|
with gamedb.lock:
|
|
games = list(gamedb.getGames())
|
|
logging.debug("(%s) Gameserver list: %i game(s)" % (self.gameHost, len(games)))
|
|
|
|
# Transmit the games.
|
|
for game in games:
|
|
logging.debug(" %s" % game)
|
|
protocol.encodeMultiple(games, self.wfile, hideGameID=True)
|
|
return
|
|
|
|
# If something unknown appears.
|
|
else:
|
|
raise Exception("(%s) Received an unknown command: %s" % (self.gameHost, netCommand))
|
|
|
|
def finish(self):
|
|
if self.g:
|
|
self.g.hosts = filter(lambda s: s != self.gameHost, self.g.hosts)
|
|
if not filter(None, self.g.hosts):
|
|
logging.debug("(%s) Removing aborted game" % self.gameHost)
|
|
gamedb.removeGame(self.g)
|
|
if self.GameId:
|
|
gamedb.removeGame(self.GameId)
|
|
for parent in (SocketServer.ThreadingMixIn, SocketServer.StreamRequestHandler):
|
|
if hasattr(parent, 'finish'):
|
|
parent.finish(self)
|
|
|
|
class TCP6Server(SocketServer.TCPServer):
|
|
"""Class to enable listening on both IPv4 and IPv6 addresses."""
|
|
|
|
address_family = socket.AF_INET6
|
|
|
|
def server_bind(self):
|
|
"""Called by constructor to bind the socket.
|
|
|
|
Overridden to enable IPV6_V6ONLY.
|
|
|
|
"""
|
|
try:
|
|
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
|
except AttributeError:
|
|
# Apparently IPV6_V6ONLY isn't supported on this
|
|
# platform. Lets hope that IPv6 mapping of IPv4
|
|
# addresses isn't supported either...
|
|
pass
|
|
SocketServer.TCPServer.server_bind(self)
|
|
|
|
class ThreadingTCP6Server(SocketServer.ThreadingMixIn, TCP6Server): pass
|
|
|
|
#
|
|
################################################################################
|
|
# The legendary Main.
|
|
|
|
if __name__ == '__main__':
|
|
logging.info("Starting Warzone 2100 lobby server on port %d" % (protocol.lobbyPort))
|
|
|
|
# Read in the Message of the Day, max is 1 line, 255 chars
|
|
in_file = open("motd.txt", "r")
|
|
MOTDstring = in_file.read()
|
|
in_file.close()
|
|
logging.info("The MOTD is (%s)" % MOTDstring)
|
|
|
|
|
|
gamedb = GameDB()
|
|
|
|
ThreadingTCPServer.allow_reuse_address = True
|
|
ThreadingTCP6Server.allow_reuse_address = True
|
|
tcpservers = [ThreadingTCPServer(('0.0.0.0', protocol.lobbyPort), RequestHandler),
|
|
ThreadingTCP6Server(('::', protocol.lobbyPort), RequestHandler)]
|
|
try:
|
|
while True:
|
|
(readyServers, wlist, xlist) = select(tcpservers, [], [])
|
|
for tcpserver in readyServers:
|
|
tcpserver.handle_request()
|
|
except KeyboardInterrupt:
|
|
pass
|
|
logging.info("Shutting down lobby server, cleaning up")
|
|
for game in gamedb.getAllGames():
|
|
for requestHandler in game.requestHandlers:
|
|
requestHandler.finish()
|
|
for tcpserver in tcpservers:
|
|
tcpserver.server_close()
|