Source code for server

"""
	This is the main ScalyMUCK server code,
	it performs the initialisation of various
	systems and is the binding of everything.

	Copyright (c) 2013 Robert MacGregor
	This software is licensed under the GNU General
	Public License version 3. Please refer to gpl.txt 
	for more information.
"""

import sys
import string
import logging

import bcrypt
from blinker import signal
from miniboa import TelnetServer
from sqlalchemy import create_engine
from sqlalchemy.exc import OperationalError
from sqlalchemy.engine.reflection import Inspector

import daemon
import game.models
from game import interface, world

[docs]class Server(daemon.Daemon): """ Server class that is initialized by the main.py script to act as the MUCK server. It performs all of the core functions of the MUCK server, which is mainly accepting connections and performing the login sequence before giving them access to the server and its world. """ is_running = False telnet_server = None logger = None connection_logger = None world = None interface = None work_factor = 10 welcome_message_data = 'Unable to load welcome message!\n' exit_message_data = 'Unable to load exit message!\n' pending_connection_list = [ ] established_connection_list = [ ] post_client_connect = signal('post_client_connect') pre_client_disconnect = signal('pre_client_disconnect') post_client_authenticated = signal('post_client_authenticated') world_tick = signal('world_tick') auth_low_argc = None auth_invalid_combination = None auth_connected = None auth_replace_Connection = None auth_connection_replaced = None auth_connect_suggestion = None game_client_disconnect = None
[docs] def __init__(self, config=None, path=None, workdir=None): """ The server class is created and managed by the main.py script. When created, the server automatically initiates a Telnet server provided by Miniboa which immediately listens for incoming connections and will query them for login details upon connection. Keyword arguments: config -- An instance of game.Settings that is to be used when loading configuration data. path -- The data path that all permenant data should be written to. workdir -- The current working directory of the server. This should be an absolute path to application/. """ self.connection_logger = logging.getLogger('Connections') self.logger = logging.getLogger('Server') # Loading all of the configuration variables database_type = string.lower(config.get_index(index='DatabaseType', datatype=str)) database = config.get_index(index='DatabaseName', datatype=str) user = config.get_index(index='DatabaseUser', datatype=str) password = config.get_index(index='DatabasePassword', datatype=str) self.work_factor = config.get_index(index='WorkFactor', datatype=int) if (database_type == 'sqlite'): database_location = path + config.get_index(index='TargetDatabase', datatype=str) else: database_location = config.get_index(index='TargetDatabase', datatype=str) # Load server messages self.auth_low_argc = config.get_index('AuthLowArgC', str) self.auth_invalid_combination = config.get_index('AuthInvalidCombination', str) self.auth_connected = config.get_index('AuthConnected', str) self.auth_replace_Connection = config.get_index('AuthReplaceConnection', str) self.auth_connection_replaced = config.get_index('AuthConnectionReplaced', str) self.auth_connect_suggestion = config.get_index('AuthConnectSuggestion', str).replace('\\n','\n') self.game_client_disconnect = config.get_index('GameClientDisconnect', str) # Loading welcome/exit messages with open(workdir + 'config/welcome_message.txt') as f: self.welcome_message_data = f.read() + '\n' with open(workdir + 'config/exit_message.txt') as f: self.exit_message_data = f.read() + '\n' # Connect/Create our database is required database_exists = True if (database_type == 'sqlite'): try: with open(database_location) as f: pass except IOError as e: self.logger.info('This appears to be your first time running the ScalyMUCK server. We must initialise your database ...') database_exists = False database_engine = create_engine('sqlite:////%s' % (database_location), echo=False) else: url = database_type + '://' + user + ':' + password + '@' + database_location + '/' + database try: database_engine = create_engine(url, echo=False) connection = database_engine.connect() except OperationalError as e: self.logger.error(str(e)) self.logger.error('URL: ' + url) self.is_running = False return self.world = world.World(database_engine) self.interface = interface.Interface(config=config, world=self.world, workdir=workdir, session=self.world.session, server=self) game.models.Base.metadata.create_all(database_engine) # Check to see if our root user exists if (database_type != 'sqlite'): root_user = self.world.find_player(name='RaptorJesus') if (root_user is None): database_exists = False if (database_exists is False): room = self.world.create_room('Portal Room Main') user = self.world.create_player(name='RaptorJesus', password='ChangeThisPasswordNowPlox', workfactor=self.work_factor, location=room, admin=True, sadmin=True, owner=True) self.logger.info('The database has been successfully initialised.') self.telnet_server = TelnetServer(port=config.get_index(index='ServerPort', datatype=int), address=config.get_index(index='ServerAddress', datatype=str), on_connect = self.on_client_connect, on_disconnect = self.on_client_disconnect, timeout = 0.05) self.logger.info('ScalyMUCK successfully initialised.') self.is_running = True game.models.server = self game.models.world = self.world
[docs] def update(self): """ The update command is called by the main.py script file. The update command does as it says, it causes the server to go through and poll for data from any of the clients and processes this data differently based on whether or not that they had actually logged in. When this function finishes calling, a single world tick has passed. """ try: self.telnet_server.poll() except UnicodeDecodeError: return for connection in self.pending_connection_list: if (connection.cmd_ready is True): data = "".join(filter(lambda x: ord(x)<128, connection.get_command())) command_data = string.split(data, ' ') # Try and perform the authentification process if (len(command_data) < 3): connection.send('%s\n' % (self.auth_low_argc)) elif (len(command_data) >= 3 and string.lower(command_data[0]) == 'connect'): name = string.lower(command_data[1]) password = command_data[2] target_player = self.world.find_player(name=name) if (target_player is None): connection.send('%s\n' % (self.auth_invalid_combination)) else: player_hash = target_player.hash if (player_hash == bcrypt.hashpw(password, player_hash) == player_hash): connection.id = target_player.id target_player.connection = connection # Check if our work factors differ work_factor = int(player_hash.split('$')[2]) if (work_factor != self.work_factor): target_player.set_password(password) self.logger.info('%s had their hash updated.' % (target_player.display_name)) self.connection_logger.info('Client %s:%u signed in as user %s.' % (connection.address, connection.port, target_player.display_name)) self.post_client_authenticated.send(None, sender=target_player) for player in target_player.location.players: if (player is not target_player): player.send('%s %s' % (target_player.display_name, self.auth_connected)) for player in self.established_connection_list: if (player.id == connection.id): player.send('%s\n' % (self.auth_replace_connection)) player.socket_send() player.deactivate() player.sock.close() connection.send('%s\n' % (self.auth_connection_replaced)) self.established_connection_list.remove(player) break self.pending_connection_list.remove(connection) self.established_connection_list.append(connection) else: connection.send('You have specified an invalid username/password combination.\n') elif (len(command_data) >= 3 and string.lower(command_data[0]) != 'connect'): connection.send('%s\n' % (self.auth_connect_suggestion)) #connection.send('You must use the "connect" command:\n') #connection.send('connect <username> <password>\n') # With already connected clients, we'll now deploy the command interface. for connection in self.established_connection_list: if (connection.cmd_ready): input = "".join(filter(lambda x: ord(x)<128, connection.get_command())) sending_player = self.world.find_player(id=connection.id) sending_player.connection = connection self.interface.parse_command(sender=sending_player, input=input) self.world_tick.send(None)
[docs] def shutdown(self): """ Shuts down the ScalyMUCK server. This command shuts down the ScalyMUCK server and gracefully disconnects all connected clients by sending a message before their disconnection which currently reads: "The server has been shutdown adruptly by the server owner." This message cannot be changed. """ self.is_running = False for connection in self.established_connection_list: connection.send('The server has been shutdown adruptly by the server owner.\n') connection.socket_send()
[docs] def find_connection(self, id): """ Finds a player connection by their database ID. """ for player in self.established_connection_list: if (player.id == id): return player
[docs] def on_client_connect(self, client): """ This is merely a callback for Miniboa to refer to when receiving a client connection from somewhere. """ self.connection_logger.info('Received client connection from %s:%u' % (client.address, client.port)) client.send(self.welcome_message_data) self.pending_connection_list.append(client) self.post_client_connect.send(sender=client)
[docs] def on_client_disconnect(self, client): """ This is merely a callback for Miniboa to refer to when receiving a client disconnection. """ self.pre_client_disconnect.send(sender=client) self.connection_logger.info('Received client disconnection from %s:%u' % (client.address, client.port)) # Iterate over anyone who had connected but did not authenticate if (client in self.pending_connection_list): self.pending_connection_list.remove(client) # Otherwise run over the list of people who had authenticated elif (client in self.established_connection_list): player = self.world.find_player(id=client.id) room = self.world.find_room(id=player.location_id) room.broadcast('%s %s' % (player.display_name, self.game_client_disconnect), player) self.established_connection_list.remove(client)