Initial commit

This commit is contained in:
Ben
2019-03-01 19:41:40 +00:00
parent a29799503f
commit 9d7c437845
22 changed files with 2037 additions and 0 deletions

155
Houdini/Converters.py Normal file
View File

@@ -0,0 +1,155 @@
import zope.interface
from zope.interface import implementer
class IConverter(zope.interface.Interface):
description = zope.interface.Attribute("""A short description of the purpose of the converter""")
async def convert(self):
raise NotImplementedError('Converter must derive this class!')
class Converter:
__slots__ = ['p', 'argument']
def __init__(self, p, argument):
self.p = p
self.argument = argument
@implementer(IConverter)
class CredentialsConverter(Converter):
description = """Used for obtaining login credentials from XML login data"""
async def convert(self):
username = self.argument[0][0].text
password = self.argument[0][1].text
return username, password
@implementer(IConverter)
class VersionChkConverter(Converter):
description = """Used for checking the verChk version number"""
async def convert(self):
return self.argument[0].get('v')
@implementer(IConverter)
class ConnectedPenguinConverter(Converter):
description = """Converts a penguin ID into a live penguin instance
or none if the player is offline"""
async def convert(self):
penguin_id = int(self.argument)
if penguin_id in self.p.server.penguins_by_id:
return self.p.server.penguins_by_id[penguin_id]
return None
@implementer(IConverter)
class ConnectedIglooConverter(Converter):
description = """Converts a penguin ID into a live igloo instance or
none if it's not available"""
async def convert(self):
igloo_id = int(self.argument)
if igloo_id in self.p.server.igloo_map:
return self.p.server.igloo_map[igloo_id]
return None
@implementer(IConverter)
class RoomConverter(Converter):
description = """Converts a room ID into a Houdini.Data.Room instance"""
async def convert(self):
room_id = int(self.argument)
if room_id in self.p.server.rooms:
return self.p.server.rooms[room_id]
return None
@implementer(IConverter)
class ItemConverter(Converter):
description = """Converts an item ID into a Houdini.Data.Item instance"""
async def convert(self):
item_id = int(self.argument)
if item_id in self.p.server.items:
return self.p.server.items[item_id]
return None
@implementer(IConverter)
class IglooConverter(Converter):
description = """Converts an igloo ID into a Houdini.Data.Igloo instance"""
async def convert(self):
igloo_id = int(self.argument)
if igloo_id in self.p.server.igloos:
return self.p.server.igloos[igloo_id]
return None
@implementer(IConverter)
class FurnitureConverter(Converter):
description = """Converts a furniture ID into a Houdini.Data.Furniture instance"""
async def convert(self):
furniture_id = int(self.argument)
if furniture_id in self.p.server.furniture:
return self.p.server.furniture[furniture_id]
return None
@implementer(IConverter)
class FlooringConverter(Converter):
description = """Converts a flooring ID into a Houdini.Data.Flooring instance"""
async def convert(self):
flooring_id = int(self.argument)
if flooring_id in self.p.server.flooring:
return self.p.server.flooring[flooring_id]
return None
@implementer(IConverter)
class StampConverter(Converter):
description = """Converts a stamp ID into a Houdini.Data.Stamp instance"""
async def convert(self):
stamp_id = int(self.argument)
if stamp_id in self.p.server.stamps:
return self.p.server.stamps[stamp_id]
return None
@implementer(IConverter)
class VerticalConverter(Converter):
description = """Converts vertically separated values into an int list"""
async def convert(self):
return map(int, self.argument.split('|'))
@implementer(IConverter)
class CommaConverter(Converter):
description = """Converts comma separated values into an int list"""
async def convert(self):
return map(int, self.argument.split(','))

38
Houdini/Crypto.py Normal file
View File

@@ -0,0 +1,38 @@
import hashlib
from random import choice
from string import ascii_letters, digits
class Crypto:
@staticmethod
def hash(string):
if isinstance(string, int):
string = str(string)
return hashlib.md5(string.encode("utf-8")).hexdigest()
@staticmethod
def generate_random_key():
character_selection = ascii_letters + digits
return "".join(choice(character_selection) for _ in range(16))
@staticmethod
def encrypt_password(password, digest=True):
if digest:
password = Crypto.hash(password)
swapped_hash = password[16:32] + password[0:16]
return swapped_hash
@staticmethod
def get_login_hash(password, rndk):
key = Crypto.encrypt_password(password, False)
key += rndk
key += "Y(02.>'H}t\":E1"
login_hash = Crypto.encrypt_password(key)
return login_hash

4
Houdini/Data/__init__.py Normal file
View File

@@ -0,0 +1,4 @@
from gino import Gino
db = Gino()

View File

@@ -0,0 +1,79 @@
import sys
import importlib
from watchdog.events import FileSystemEventHandler
from Houdini.Handlers import listeners_from_module
from Houdini.Events import evaluate_handler_file_event, remove_handlers_by_module
class HandlerFileEventHandler(FileSystemEventHandler):
def __init__(self, server):
self.logger = server.logger
self.server = server
def on_created(self, event):
handler_module_details = evaluate_handler_file_event(event)
if not handler_module_details:
return
handler_module_path, handler_module = handler_module_details
if "__init__.py" in handler_module_path:
return
self.logger.debug("New handler module detected %s", handler_module)
try:
module = importlib.import_module(handler_module)
listeners_from_module(self.server.xt_listeners, self.server.xml_listeners, module)
except Exception as import_error:
self.logger.error("%s detected in %s, not importing.", import_error.__class__.__name__, handler_module)
def on_deleted(self, event):
handler_module_details = evaluate_handler_file_event(event)
if not handler_module_details:
return
handler_module_path, handler_module = handler_module_details
if handler_module not in sys.modules:
return
self.logger.debug("Deleting listeners registered by %s..", handler_module)
remove_handlers_by_module(self.server.xt_listeners, self.server.xml_listeners, handler_module_path)
def on_modified(self, event):
handler_module_details = evaluate_handler_file_event(event)
if not handler_module_details:
return
handler_module_path, handler_module = handler_module_details
if handler_module not in sys.modules:
return False
self.logger.info("Reloading %s", handler_module)
xt_listeners, xml_listeners = remove_handlers_by_module(self.server.xt_listeners, self.server.xml_listeners,
handler_module_path)
handler_module_object = sys.modules[handler_module]
try:
module = importlib.reload(handler_module_object)
listeners_from_module(self.server.xt_listeners, self.server.xml_listeners, module)
self.logger.info("Successfully reloaded %s!", handler_module)
except Exception as rebuild_error:
self.logger.error("%s detected in %s, not reloading.", rebuild_error.__class__.__name__, handler_module)
self.logger.info("Restoring handler references...")
self.server.xt_listeners = xt_listeners
self.server.xml_listeners = xml_listeners
self.logger.info("Handler references restored. Phew!")

View File

@@ -0,0 +1,54 @@
import os
import copy
def evaluate_handler_file_event(handler_file_event):
# Ignore all directory events
if handler_file_event.is_directory:
return False
handler_module_path = handler_file_event.src_path[2:]
# Ignore non-Python files
if handler_module_path[-3:] != ".py":
return False
handler_module = handler_module_path.replace(os.path.sep, ".")[:-3]
return handler_module_path, handler_module
def evaluate_plugin_file_event(plugin_file_event):
# Ignore all directory events
if plugin_file_event.is_directory:
return False
handler_module_path = plugin_file_event.src_path[2:]
# Ignore non-Python files
if handler_module_path[-3:] != ".py":
return False
# Remove file extension and replace path separator with dots. Then make like a banana.. and split.
handler_module_tokens = handler_module_path.replace(os.path.sep, ".")[:-3].split(".")
if handler_module_tokens.pop() == "__init__":
return handler_module_path, ".".join(handler_module_tokens)
return False
def remove_handlers_by_module(xt_listeners, xml_listeners, handler_module_path):
def remove_handlers(remove_handler_items):
for handler_id, handler_listeners in remove_handler_items:
for handler_listener in handler_listeners:
if handler_listener.handler_file == handler_module_path:
handler_listeners.remove(handler_listener)
xt_handler_collection = copy.copy(xt_listeners)
remove_handlers(xt_listeners.items())
xml_handler_collection = copy.copy(xml_listeners)
remove_handlers(xml_listeners.items())
return xt_handler_collection, xml_handler_collection

View File

@@ -0,0 +1,15 @@
from Houdini import Handlers
from Houdini.Handlers import XTPacket, XMLPacket
from Houdini.Converters import CredentialsConverter, CommaConverter
@Handlers.handler(XMLPacket('login'))
async def handle_login(p, credentials: CredentialsConverter):
username, password = credentials
p.logger.info('{}:{} is logging in!'.format(username, password))
@Handlers.handler(XTPacket('t', 'c'), pre_login=True)
@Handlers.player_in_room(100)
async def handle_test(p, numbers: CommaConverter):
print(list(numbers))

View File

View File

@@ -0,0 +1,12 @@
from Houdini.Handlers import Handlers, XTPacket
from Houdini.Converters import RoomConverter
@Handlers.handler(XTPacket('j', 'js'))
async def handle_join_world(p, is_moderator: bool, is_mascot: bool, is_member: bool):
print(p, is_moderator, is_mascot, is_member)
@Handlers.handler(XTPacket('j', 'jr'))
async def handle_join_room(p, room: RoomConverter):
print(room)

View File

View File

@@ -0,0 +1,353 @@
import inspect
import time
import enum
import os
import asyncio
from types import FunctionType
from Houdini.Converters import IConverter
def get_relative_function_path(function_obj):
abs_function_file = inspect.getfile(function_obj)
rel_function_file = os.path.relpath(abs_function_file)
return rel_function_file
def get_converter(component):
if component.annotation is component.empty:
return str
return component.annotation
async def do_conversion(converter, p, component_data):
if IConverter.implementedBy(converter):
converter_instance = converter(p, component_data)
if asyncio.iscoroutinefunction(converter_instance.convert):
return await converter_instance.convert()
return converter_instance.convert()
return converter(component_data)
class _Packet:
__slots__ = ['id']
def __init__(self):
self.id = None
def __eq__(self, other):
return self.id == other.id
def __hash__(self):
return hash(self.id)
class XTPacket(_Packet):
def __init__(self, *packet_id):
super().__init__()
self.id = '#'.join(packet_id)
def __hash__(self):
return hash(self.id)
class XMLPacket(_Packet):
def __init__(self, packet_id):
super().__init__()
self.id = packet_id
class Priority(enum.Enum):
Override = 3
High = 2
Low = 1
class BucketType(enum.Enum):
Default = 1
Penguin = 1
Server = 2
class _Cooldown:
__slots__ = ['rate', 'per', 'bucket_type', 'last',
'_window', '_tokens']
def __init__(self, per, rate, bucket_type):
self.per = float(per)
self.rate = int(rate)
self.bucket_type = bucket_type
self.last = 0.0
self._window = 0.0
self._tokens = self.rate
@property
def is_cooling(self):
current = time.time()
self.last = current
if self._tokens == self.rate:
self._window = current
if current > self._window + self.per:
self._tokens = self.rate
self._window = current
if self._tokens == 0:
return self.per - (current - self._window)
self._tokens -= 1
if self._tokens == 0:
self._window = current
def reset(self):
self._tokens = self.rate
self.last = 0.0
def copy(self):
return _Cooldown(self.per, self.rate, self.bucket_type)
class _CooldownMapping:
__slots__ = ['_cooldown', '_cache']
def __init__(self, cooldown_object):
self._cooldown = cooldown_object
self._cache = {}
def _get_bucket_key(self, p):
if self._cooldown.bucket_type == BucketType.Default:
return p
return p.server
def _verify_cache_integrity(self):
current = time.time()
self._cache = {cache_key: bucket for cache_key, bucket in
self._cache.items() if current < bucket.last + bucket.per}
def get_bucket(self, p):
self._verify_cache_integrity()
cache_key = self._get_bucket_key(p)
if cache_key not in self._cache:
bucket = self._cooldown.copy()
self._cache[cache_key] = bucket
else:
bucket = self._cache[cache_key]
return bucket
class _Listener:
__slots__ = ['packet', 'components', 'handler', 'priority',
'cooldown', 'pass_packet', 'handler_file',
'overrides', 'pre_login', 'checklist', 'instance']
def __init__(self, packet, components, handler_function, **kwargs):
self.packet = packet
self.components = components
self.handler = handler_function
self.priority = kwargs.get('priority', Priority.Low)
self.overrides = kwargs.get('overrides', [])
self.cooldown = kwargs.get('cooldown')
self.pass_packet = kwargs.get('pass_packet', False)
self.checklist = kwargs.get('checklist', [])
self.instance = None
if type(self.overrides) is not list:
self.overrides = [self.overrides]
self.handler_file = get_relative_function_path(handler_function)
def _can_run(self, p):
return True if not self.checklist else all(predicate(self.packet, p) for predicate in self.checklist)
def __hash__(self):
return hash(self.__name__())
def __name__(self):
return "{}.{}".format(self.handler.__module__, self.handler.__name__)
def __call__(self, p, packet_data):
if self.cooldown is not None:
bucket = self.cooldown.get_bucket(p)
if bucket.is_cooling:
raise RuntimeError('{} sent packet during cooldown'.format(p.peer_name))
if not self._can_run(p):
raise RuntimeError('Could not handle packet due to checklist failure')
class _XTListener(_Listener):
__slots__ = ['pre_login']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pre_login = kwargs.get('pre_login')
async def __call__(self, p, packet_data):
if not self.pre_login and not p.joined_world:
p.logger.warn('{} tried sending XT packet before authentication!'.format(p.peer_name))
await p.close()
return
super().__call__(p, packet_data)
handler_call_arguments = [self.instance] if self.instance is not None else []
handler_call_arguments += [self.packet, p] if self.pass_packet else [p]
arguments = iter(packet_data)
for index, component in enumerate(self.components):
if component.default is not component.empty:
handler_call_arguments.append(component.default)
next(arguments)
elif component.kind == component.POSITIONAL_OR_KEYWORD:
component_data = next(arguments)
converter = get_converter(component)
handler_call_arguments.append(await do_conversion(converter, p, component_data))
elif component.kind == component.VAR_POSITIONAL:
for component_data in arguments:
converter = get_converter(component)
handler_call_arguments.append(await do_conversion(converter, p, component_data))
break
return await self.handler(*handler_call_arguments)
class _XMLListener(_Listener):
__slots__ = ['pre_login']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
async def __call__(self, p, packet_data):
super().__call__(p, packet_data)
handler_call_arguments = [self.instance] if self.instance is not None else []
handler_call_arguments += [self.packet, p] if self.pass_packet else [p]
for index, component in enumerate(self.components):
if component.default is not component.empty:
handler_call_arguments.append(component.default)
elif component.kind == component.POSITIONAL_OR_KEYWORD:
converter = get_converter(component)
handler_call_arguments.append(await do_conversion(converter, p, packet_data))
return await self.handler(*handler_call_arguments)
def handler(packet, **kwargs):
def decorator(handler_function):
if not asyncio.iscoroutinefunction(handler_function):
raise TypeError('All handlers must be a coroutine.')
components = list(inspect.signature(handler_function).parameters.values())[1:]
if not issubclass(type(packet), _Packet):
raise TypeError('All handlers can only listen for either XMLPacket or XTPacket.')
listener_class = _XTListener if isinstance(packet, XTPacket) else _XMLListener
try:
cooldown_object = handler_function.cooldown
del handler_function.cooldown
except AttributeError:
cooldown_object = None
try:
checklist = handler_function.checks
del handler_function.checks
except AttributeError:
checklist = []
listener_object = listener_class(packet, components, handler_function,
cooldown=cooldown_object, checklist=checklist,
**kwargs)
return listener_object
return decorator
def listener_exists(xt_listeners, xml_listeners, packet):
listener_collection = xt_listeners if isinstance(packet, XTPacket) else xml_listeners
return packet in listener_collection
def is_listener(listener):
return issubclass(type(listener), _Listener)
def listeners_from_module(xt_listeners, xml_listeners, module):
listener_objects = inspect.getmembers(module, is_listener)
for listener_name, listener_object in listener_objects:
listener_collection = xt_listeners if type(listener_object) == _XTListener else xml_listeners
if listener_object.packet not in listener_collection:
listener_collection[listener_object.packet] = []
if listener_object not in listener_collection[listener_object.packet]:
if listener_object.priority == Priority.High:
listener_collection[listener_object.packet].insert(0, listener_object)
elif listener_object.priority == Priority.Override:
listener_collection[listener_object.packet] = [listener_object]
else:
listener_collection[listener_object.packet].append(listener_object)
for listener_name, listener_object in listener_objects:
listener_collection = xt_listeners if type(listener_object) == _XTListener else xml_listeners
for override in listener_object.overrides:
listener_collection[override.packet].remove(override)
def cooldown(per=1.0, rate=1, bucket_type=BucketType.Default):
def decorator(handler_function):
handler_function.cooldown = _CooldownMapping(_Cooldown(per, rate, bucket_type))
return handler_function
return decorator
def check(predicate):
def decorator(handler_function):
if not hasattr(handler_function, 'checks'):
handler_function.checks = []
if not type(predicate) == FunctionType:
raise TypeError('All handler checks must be a function')
handler_function.checks.append(predicate)
return handler_function
return decorator
def allow_once():
def check_for_packet(packet, p):
return packet not in p.received_packets
return check(check_for_packet)
def player_attribute(**attrs):
def check_for_attributes(_, p):
for attr, value in attrs.items():
if not getattr(p, attr) == value:
return False
return True
return check(check_for_attributes)
def player_data_attribute(**attrs):
def check_for_attributes(_, p):
for attr, value in attrs.items():
if not getattr(p.data, attr) == value:
return False
return True
return check(check_for_attributes)
def player_in_room(*room_ids):
def check_room_id(_, p):
return p.room.ID in room_ids
return check(check_room_id)

181
Houdini/HoudiniFactory.py Normal file
View File

@@ -0,0 +1,181 @@
import asyncio
import os
import sys
import pkgutil
import importlib
from Houdini.Spheniscidae import Spheniscidae
from Houdini.Penguin import Penguin
from Houdini import PenguinStringCompiler
import config
from aiologger import Logger
from aiologger.handlers.files import AsyncTimedRotatingFileHandler, RolloverInterval, AsyncFileHandler
from aiologger.handlers.streams import AsyncStreamHandler
import logging
from logging.handlers import RotatingFileHandler
import aioredis
from aiocache import SimpleMemoryCache
from watchdog.observers import Observer
from gino import Gino
try:
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError:
uvloop = None
import Houdini.Handlers
from Houdini.Handlers import listeners_from_module
from Houdini.Events.HandlerFileEvent import HandlerFileEventHandler
class HoudiniFactory:
def __init__(self, **kwargs):
self.server = None
self.redis = None
self.config = None
self.cache = None
self.db = Gino()
self.peers_by_ip = {}
self.server_name = kwargs['server']
self.server_config = None
self.logger = None
self.client_class = Spheniscidae
self.penguin_string_compiler = None
self.penguins_by_id = {}
self.penguins_by_username = {}
self.xt_listeners, self.xml_listeners = {}, {}
async def start(self):
self.config = config
self.server_config = self.config.servers[self.server_name]
self.server = await asyncio.start_server(
self.client_connected, self.server_config['Address'],
self.server_config['Port']
)
await self.db.set_bind('postgresql://{}:{}@{}/{}'.format(
self.config.database['Username'], self.config.database['Password'],
self.config.database['Address'],
self.config.database['Name']))
general_log_directory = os.path.dirname(self.server_config["Logging"]["General"])
errors_log_directory = os.path.dirname(self.server_config["Logging"]["Errors"])
if not os.path.exists(general_log_directory):
os.mkdir(general_log_directory)
if not os.path.exists(errors_log_directory):
os.mkdir(errors_log_directory)
if sys.platform != 'win32':
self.logger = Logger.with_default_handlers(name='Houdini')
universal_handler = AsyncTimedRotatingFileHandler(
filename=self.server_config['Logging']['General'],
backup_count=3,
when=RolloverInterval.HOURS
)
error_handler = AsyncFileHandler(filename=self.server_config['Logging']['General'])
console_handler = AsyncStreamHandler(stream=sys.stdout)
else:
self.logger = logging.getLogger('Houdini')
universal_handler = RotatingFileHandler(self.server_config['Logging']['General'],
maxBytes=2097152, backupCount=3, encoding='utf-8')
error_handler = logging.FileHandler(self.server_config['Logging']['Errors'])
console_handler = logging.StreamHandler(stream=sys.stdout)
log_formatter = logging.Formatter('%(asctime)s [%(levelname)-5.5s] %(message)s')
error_handler.setLevel(logging.ERROR)
universal_handler.setFormatter(log_formatter)
console_handler.setFormatter(log_formatter)
self.logger.addHandler(universal_handler)
self.logger.addHandler(console_handler)
level = logging.getLevelName(self.server_config['Logging']['Level'])
self.logger.setLevel(level)
self.logger.info('Houdini module instantiated')
if self.server_config['World']:
self.redis = await aioredis.create_redis_pool('redis://{}:{}'.format(
self.config.redis['Address'], self.redis['Port']),
minsize=5, maxsize=10)
await self.redis.delete('{}.players'.format(self.server_name))
await self.redis.delete('{}.population'.format(self.server_name))
self.cache = SimpleMemoryCache(namespace='houdini', ttl=self.server_config['CacheExpiry'])
self.client_class = Penguin
self.penguin_string_compiler = PenguinStringCompiler()
self.load_handler_modules(exclude_load="Houdini.Handlers.Login.Login")
self.logger.info('World server started')
else:
self.load_handler_modules("Houdini.Handlers.Login.Login")
self.logger.info('Login server started')
handlers_path = './Houdini{}Handlers'.format(os.path.sep)
plugins_path = './Houdini{}Plugins'.format(os.path.sep)
self.configure_obvservers([handlers_path, HandlerFileEventHandler])
self.logger.info('Listening on {}:{}'.format(self.server_config['Address'], self.server_config['Port']))
async with self.server:
await self.server.serve_forever()
async def client_connected(self, reader, writer):
client_object = self.client_class(self, reader, writer)
await client_object.run()
def load_handler_modules(self, strict_load=None, exclude_load=None):
for handler_module in self.get_package_modules(Houdini.Handlers):
if not (strict_load and handler_module not in strict_load or exclude_load and handler_module in exclude_load):
if handler_module not in sys.modules.keys():
module = importlib.import_module(handler_module)
listeners_from_module(self.xt_listeners, self.xml_listeners, module)
self.logger.info("Handler modules loaded")
def get_package_modules(self, package):
package_modules = []
for importer, module_name, is_package in pkgutil.iter_modules(package.__path__):
full_module_name = "{0}.{1}".format(package.__name__, module_name)
if is_package:
subpackage_object = importlib.import_module(full_module_name, package=package.__path__)
subpackage_object_directory = dir(subpackage_object)
if "Plugin" in subpackage_object_directory:
package_modules.append((subpackage_object, module_name))
continue
sub_package_modules = self.get_package_modules(subpackage_object)
package_modules = package_modules + sub_package_modules
else:
package_modules.append(full_module_name)
return package_modules
def configure_obvservers(self, *observer_settings):
for observer_path, observer_class in observer_settings:
event_observer = Observer()
event_observer.schedule(observer_class(self), observer_path, recursive=True)
event_observer.start()

36
Houdini/Penguin.py Normal file
View File

@@ -0,0 +1,36 @@
from Houdini.Spheniscidae import Spheniscidae
class Penguin(Spheniscidae):
__slots__ = ['x', 'y', 'room', 'waddle', 'table', 'data']
def __init__(self, *args):
super().__init__(*args)
self.x, self.y = (0, 0)
self.room = None
self.waddle = None
self.table = None
self.data = None
self.logger.debug('New penguin created')
async def add_inventory(self, item):
pass
async def add_igloo(self, igloo):
pass
async def add_furniture(self, furniture):
pass
async def add_flooring(self, flooring):
pass
async def add_buddy(self, buddy):
pass
async def add_inbox(self, postcard):
pass

View File

@@ -0,0 +1,11 @@
from Houdini.Handlers import Handlers, XTPacket
class Commands:
def __init__(self, server):
self.server = server
@Handlers.handler(XTPacket('s', 'sm'))
async def handle_send_message(self, message: str):
print('Do stuff with {}'.format(message))

View File

159
Houdini/Spheniscidae.py Normal file
View File

@@ -0,0 +1,159 @@
from Houdini import Handlers
from Houdini.Handlers import XMLPacket, XTPacket
from asyncio import IncompleteReadError
import defusedxml.cElementTree as Et
from xml.etree.cElementTree import Element, SubElement, tostring
class Spheniscidae:
__slots__ = ['__reader', '__writer', 'server', 'logger',
'peer_name', 'received_packets', 'joined_world']
Delimiter = b'\x00'
def __init__(self, server, reader, writer):
self.__reader = reader
self.__writer = writer
self.server = server
self.logger = server.logger
self.peer_name = writer.get_extra_info('peername')
self.server.peers_by_ip[self.peer_name] = self
self.joined_world = False
self.received_packets = set()
async def send_error_and_disconnect(self, error):
await self.send_xt('e', error)
await self.close()
async def send_error(self, error):
await self.send_xt('e', error)
async def send_policy_file(self):
await self.send_line('<cross-domain-policy><allow-access-from domain="*" to-ports="{}" /></cross-domain-policy>'
.format(self.server.server_config['Port']))
await self.close()
async def send_xt(self, *data):
data = list(data)
handler_id = data.pop(0)
internal_id = -1
mapped_data = map(str, data)
xt_data = '%'.join(mapped_data)
line = '%xt%{0}%{1}%{2}%'.format(handler_id, internal_id, xt_data)
await self.send_line(line)
async def send_xml(self, xml_dict):
data_root = Element('msg')
data_root.set('t', 'sys')
sub_element_parent = data_root
for sub_element, sub_element_attribute in xml_dict.iteritems():
sub_element_object = SubElement(sub_element_parent, sub_element)
if type(xml_dict[sub_element]) is dict:
for sub_element_attribute_key, sub_element_attribute_value in xml_dict[sub_element].iteritems():
sub_element_object.set(sub_element_attribute_key, sub_element_attribute_value)
else:
sub_element_object.text = xml_dict[sub_element]
sub_element_parent = sub_element_object
xml_data = tostring(data_root)
await self.send_line(xml_data)
async def send_line(self, data):
self.logger.debug('Outgoing data: %s', data)
self.__writer.write(data.encode() + Spheniscidae.Delimiter)
async def close(self):
self.__writer.close()
async def __handle_xt_data(self, data):
self.logger.debug("Received XT data: {0}".format(data))
parsed_data = data.split("%")[1:-1]
packet_id = parsed_data[2]
packet = XTPacket(packet_id)
if Handlers.listener_exists(self.server.xt_listeners, self.server.xml_listeners, packet):
xt_listeners = self.server.xt_listeners[packet]
packet_data = parsed_data[4:]
for listener in xt_listeners:
await listener(self, packet_data)
self.received_packets.add(packet)
else:
self.logger.debug("Handler for {0} doesn't exist!".format(packet_id))
async def __handle_xml_data(self, data):
self.logger.debug("Received XML data: {0}".format(data))
element_tree = Et.fromstring(data)
if element_tree.tag == "policy-file-request":
await self.send_policy_file()
elif element_tree.tag == "msg":
self.logger.debug("Received valid XML data")
try:
body_tag = element_tree[0]
action = body_tag.get("action")
packet = XMLPacket(action)
if Handlers.listener_exists(self.server.xt_listeners, self.server.xml_listeners, packet):
xml_listeners = self.server.xml_listeners[packet]
for listener in xml_listeners:
await listener(self, body_tag)
self.received_packets.add(packet)
else:
self.logger.warn("Packet did not contain a valid action attribute!")
except IndexError:
self.logger.warn("Received invalid XML data (didn't contain a body tag)")
else:
self.logger.warn("Received invalid XML data!")
async def __client_connected(self):
self.logger.info('Client %s connected', self.peer_name)
async def __client_disconnected(self):
del self.server.peers_by_ip[self.peer_name]
self.logger.info('Client %s disconnected', self.peer_name)
async def __data_received(self, data):
data = data.decode()[:-1]
if data.startswith('<'):
await self.__handle_xml_data(data)
else:
await self.__handle_xt_data(data)
async def run(self):
await self.__client_connected()
while not self.__writer.is_closing():
try:
data = await self.__reader.readuntil(
separator=Spheniscidae.Delimiter)
if data:
await self.__data_received(data)
else:
self.__writer.close()
await self.__writer.drain()
except IncompleteReadError:
self.__writer.close()
except ConnectionResetError:
self.__writer.close()
await self.__client_disconnected()

40
Houdini/__init__.py Normal file
View File

@@ -0,0 +1,40 @@
from collections import OrderedDict
from aiocache import cached
from types import FunctionType
import asyncio
class PenguinStringCompiler(OrderedDict):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __setitem__(self, key, compiler_method):
assert type(compiler_method) == FunctionType
super().__setitem__(key, compiler_method)
@cached(namespace='houdini')
async def compile(self, p):
compiler_method_results = []
for compiler_method in self.values():
if asyncio.iscoroutinefunction(compiler_method):
compiler_method_result = await compiler_method(p)
else:
compiler_method_result = compiler_method(p)
compiler_method_results.append(str(compiler_method_result))
compiler_result = '|'.join(compiler_method_results)
return compiler_result
@classmethod
def attribute_by_name(cls, attribute_name):
async def attribute_method(p):
return getattr(p, attribute_name)
return attribute_method
@classmethod
def data_attribute_by_name(cls, attribute_name):
async def attribute_method(p):
return getattr(p.data, attribute_name)
return attribute_method