From 4fa0f4dfe0ec4446d6cd94de56984c2c94c9f412 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 6 Aug 2019 22:55:11 +0100 Subject: [PATCH] Initial support for multiple clients and cross-client play --- bootstrap.py | 41 ++++++++++++++++++++++++++++++ houdini/handlers/__init__.py | 4 ++- houdini/handlers/login/__init__.py | 14 +++++++--- houdini/handlers/login/login.py | 19 ++++++++------ houdini/handlers/login/world.py | 25 ++++++++++++------ houdini/houdini.py | 8 ++++++ houdini/spheniscidae.py | 10 +++++--- 7 files changed, 98 insertions(+), 23 deletions(-) diff --git a/bootstrap.py b/bootstrap.py index 30bb75c..1ddc015 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -6,6 +6,7 @@ import config from houdini.houdini import Houdini from houdini import ConflictResolution from houdini import Language +from houdini import ClientType if __name__ == '__main__': logger = logging.getLogger('houdini') @@ -81,6 +82,33 @@ if __name__ == '__main__': command_group.add_argument('-ccm', '--command-conflict-mode', action='store', dest='command_conflict_mode', default=config.commands['ConflictMode'].name, help='Command conflict mode', choices=['Silent', 'Append', 'Exception']) + + client_group = parser.add_argument_group('client') + client_mode = client_group.add_mutually_exclusive_group() + client_mode.add_argument('--multi-client-mode', action='store_true', + help='Run server with support for both clients') + client_mode.add_argument('--single-client-mode', action='store_true', + help='Run server with support for default client only') + client_group.add_argument('--legacy-version', action='store', + type=int, + default=config.client['LegacyVersionChk'], + help='Legacy client version to identify legacy clients') + client_group.add_argument('--vanilla-version', action='store', + type=int, + default=config.client['VanillaVersionChk'], + help='Vanilla client version to identify vanilla clients') + client_group.add_argument('--default-version', action='store', + type=int, + default=config.client['DefaultVersionChk'], + help='Default version to identify clients when multi-client is off') + client_group.add_argument('--default-client', action='store', + choices=['Legacy', 'Vanilla'], + default=config.client['DefaultClientType'].name, + help='Default client when multi-client is off') + client_group.add_argument('-k', '--auth-key', action='store', + default='houdini', + help='Static key to use in place of the deprecated random key') + args = parser.parse_args() database = { @@ -101,6 +129,18 @@ if __name__ == '__main__': 'ConflictMode': getattr(ConflictResolution, args.command_conflict_mode) } + client = { + 'MultiClientSupport': True if args.multi_client_mode else False if args.single_client_mode + else config.client['MultiClientSupport'], + 'LegacyVersionChk': args.legacy_version, + 'VanillaVersionChk': args.vanilla_version, + + 'DefaultVersionChk': args.default_version, + 'DefaultClientType': getattr(ClientType, args.default_client), + + 'AuthStaticKey': args.auth_key + } + server = { 'Address': args.address or config.servers[args.server]['Address'], 'Port': args.port or config.servers[args.server]['Port'], @@ -129,6 +169,7 @@ if __name__ == '__main__': database=database, redis=redis, commands=commands, + client=client, server=server) try: asyncio.run(factory_instance.start()) diff --git a/houdini/handlers/__init__.py b/houdini/handlers/__init__.py index ab48f9d..2c0813b 100644 --- a/houdini/handlers/__init__.py +++ b/houdini/handlers/__init__.py @@ -3,6 +3,7 @@ import enum import itertools import importlib import sys +import config from types import FunctionType from houdini.converters import _listener, _ArgumentDeserializer, get_converter, do_conversion, _ConverterContext @@ -51,7 +52,7 @@ class Priority(enum.Enum): class _Listener(_ArgumentDeserializer): - __slots__ = ['priority', 'packet', 'overrides', 'before', 'after'] + __slots__ = ['priority', 'packet', 'overrides', 'before', 'after', 'client_type'] def __init__(self, packet, callback, **kwargs): super().__init__(packet.id, callback, **kwargs) @@ -60,6 +61,7 @@ class _Listener(_ArgumentDeserializer): self.priority = kwargs.get('priority', Priority.Low) self.before = kwargs.get('before') self.after = kwargs.get('after') + self.client_type = kwargs.get('client') self.overrides = kwargs.get('overrides', []) diff --git a/houdini/handlers/login/__init__.py b/houdini/handlers/login/__init__.py index 12a0394..4847ca3 100644 --- a/houdini/handlers/login/__init__.py +++ b/houdini/handlers/login/__init__.py @@ -1,6 +1,6 @@ import config -from houdini import handlers +from houdini import handlers, ClientType from houdini.handlers import XMLPacket from houdini.converters import VersionChkConverter @@ -10,7 +10,15 @@ from houdini.data.buddy import BuddyList @handlers.handler(XMLPacket('verChk')) @handlers.allow_once async def handle_version_check(p, version: VersionChkConverter): - if not version == 153: + if config.client['MultiClientSupport']: + if config.client['LegacyVersionChk'] == version: + p.client_type = ClientType.Legacy + elif config.client['VanillaVersionChk'] == version: + p.client_type = ClientType.Vanilla + elif config.client['DefaultVersionChk'] == version: + p.client_type = config.client['DefaultClientType'] + + if p.client_type is None: await p.send_xml({'body': {'action': 'apiKO', 'r': '0'}}) await p.close() else: @@ -20,7 +28,7 @@ async def handle_version_check(p, version: VersionChkConverter): @handlers.handler(XMLPacket('rndK')) @handlers.allow_once async def handle_random_key(p, data): - await p.send_xml({'body': {'action': 'rndK', 'r': '-1'}, 'k': 'houdini'}) + await p.send_xml({'body': {'action': 'rndK', 'r': '-1'}, 'k': config.client['AuthStaticKey']}) async def get_server_presence(p, pid): diff --git a/houdini/handlers/login/login.py b/houdini/handlers/login/login.py index 5a82641..99f982e 100644 --- a/houdini/handlers/login/login.py +++ b/houdini/handlers/login/login.py @@ -1,4 +1,4 @@ -from houdini import handlers +from houdini import handlers, ClientType from houdini.handlers import XMLPacket, login from houdini.handlers.login import get_server_presence from houdini.converters import Credentials @@ -27,7 +27,7 @@ async def handle_login(p, credentials: Credentials): data = await Penguin.query.where(Penguin.username == username).gino.first() if data is None: - p.logger.info('{} failed to login: penguin does not exist') + p.logger.info('{} failed to login: penguin does not exist'.format(username)) return await p.send_error_and_disconnect(100) password_correct = await loop.run_in_executor(None, bcrypt.checkpw, @@ -84,16 +84,19 @@ async def handle_login(p, credentials: Credentials): confirmation_hash = Crypto.hash(os.urandom(24)) tr = p.server.redis.multi_exec() - tr.setex('{}.lkey'.format(data.id), p.server.server_config['KeyTTL'], login_key) - tr.setex('{}.ckey'.format(data.id), p.server.server_config['KeyTTL'], confirmation_hash) + tr.setex('{}.lkey'.format(data.username), p.server.server_config['KeyTTL'], login_key) + tr.setex('{}.ckey'.format(data.username), p.server.server_config['KeyTTL'], confirmation_hash) await tr.execute() world_populations, buddy_presence = await get_server_presence(p, data.id) - raw_login_data = '|'.join([str(data.id), str(data.id), data.username, login_key, str(data.approval), - str(data.rejection)]) - await p.send_xt('l', raw_login_data, confirmation_hash, '', world_populations, buddy_presence, - data.email) + if p.client_type == ClientType.Vanilla: + raw_login_data = '|'.join([str(data.id), str(data.id), data.username, login_key, str(data.approval), + str(data.rejection)]) + await p.send_xt('l', raw_login_data, confirmation_hash, '', world_populations, buddy_presence, + data.email) + else: + await p.send_xt('l', data.id, login_key, world_populations, buddy_presence) handle_version_check = login.handle_version_check handle_random_key = login.handle_random_key diff --git a/houdini/handlers/login/world.py b/houdini/handlers/login/world.py index 2942d08..71fb68f 100644 --- a/houdini/handlers/login/world.py +++ b/houdini/handlers/login/world.py @@ -1,8 +1,11 @@ -from houdini import handlers +import config + +from houdini import handlers, ClientType from houdini.handlers import XMLPacket, login -from houdini.converters import WorldCredentials +from houdini.converters import WorldCredentials, Credentials from houdini.data.penguin import Penguin from houdini.data.moderator import Ban +from houdini.crypto import Crypto from datetime import datetime @@ -10,20 +13,26 @@ handle_version_check = login.handle_version_check handle_random_key = login.handle_random_key -@handlers.handler(XMLPacket('login')) +@handlers.handler(XMLPacket('login'), client=ClientType.Vanilla) @handlers.allow_once @handlers.depends_on_packet(XMLPacket('verChk'), XMLPacket('rndK')) async def handle_login(p, credentials: WorldCredentials): tr = p.server.redis.multi_exec() - tr.get('{}.lkey'.format(credentials.id)) - tr.get('{}.ckey'.format(credentials.id)) - tr.delete('{}.lkey'.format(credentials.id), '{}.ckey'.format(credentials.id)) + tr.get('{}.lkey'.format(credentials.username)) + tr.get('{}.ckey'.format(credentials.username)) + tr.delete('{}.lkey'.format(credentials.username), '{}.ckey'.format(credentials.username)) login_key, confirmation_hash, _ = await tr.execute() if login_key is None or confirmation_hash is None: return await p.close() - if login_key.decode() != credentials.login_key or confirmation_hash.decode() != credentials.confirmation_hash: + login_key = login_key.decode() + login_hash = Crypto.encrypt_password(login_key + config.client['AuthStaticKey']) + login_key + + if credentials.client_key != login_hash: + return await p.close() + + if login_key != credentials.login_key or confirmation_hash.decode() != credentials.confirmation_hash: return await p.close() data = await Penguin.get(credentials.id) @@ -47,5 +56,5 @@ async def handle_login(p, credentials: WorldCredentials): p.logger.info('{} logged in successfully'.format(data.username)) p.data = data - p.login_key = credentials.login_key + p.login_key = login_key await p.send_xt('l') diff --git a/houdini/houdini.py b/houdini/houdini.py index 5ceaea4..ff3567b 100644 --- a/houdini/houdini.py +++ b/houdini/houdini.py @@ -58,6 +58,7 @@ class Houdini: self.database_config_override = kwargs.get('database') self.redis_config_override = kwargs.get('redis') self.commands_config_override = kwargs.get('commands') + self.client_config_override = kwargs.get('client') self.server_config_override = kwargs.get('server') self.server_config = None @@ -100,6 +101,7 @@ class Houdini: self.config.database.update(self.database_config_override) self.config.redis.update(self.redis_config_override) self.config.commands.update(self.commands_config_override) + self.config.client.update(self.client_config_override) general_log_directory = os.path.dirname(self.server_config["Logging"]["General"]) errors_log_directory = os.path.dirname(self.server_config["Logging"]["Errors"]) @@ -215,8 +217,14 @@ class Houdini: self.configure_observers([handlers_path, ListenerFileEventHandler], [plugins_path, PluginFileEventHandler]) + self.logger.info('Multi-client support is {}'.format( + 'enabled' if self.config.client['MultiClientSupport'] else 'disabled')) self.logger.info('Listening on {}:{}'.format(self.server_config['Address'], self.server_config['Port'])) + if self.config.client['AuthStaticKey'] != 'houdini': + self.logger.warning('The static key has been changed from the default, ' + 'this may cause authentication issues!') + self.plugins.setup(houdini.plugins) async with self.server: diff --git a/houdini/spheniscidae.py b/houdini/spheniscidae.py index 677cb54..92b7f7b 100644 --- a/houdini/spheniscidae.py +++ b/houdini/spheniscidae.py @@ -13,7 +13,8 @@ from houdini.cooldown import CooldownError class Spheniscidae: __slots__ = ['__reader', '__writer', 'server', 'logger', - 'peer_name', 'received_packets', 'joined_world'] + 'peer_name', 'received_packets', 'joined_world', + 'client_type'] Delimiter = b'\x00' @@ -28,6 +29,7 @@ class Spheniscidae: self.server.peers_by_ip[self.peer_name] = self self.joined_world = False + self.client_type = None self.received_packets = set() @@ -93,7 +95,8 @@ class Spheniscidae: packet_data = parsed_data[4:] for listener in xt_listeners: - await listener(self, packet_data) + if listener.client_type is None or listener.client_type == self.client_type: + await listener(self, packet_data) self.received_packets.add(packet) else: self.logger.warn('Handler for %s doesn\'t exist!', packet_id) @@ -118,7 +121,8 @@ class Spheniscidae: xml_listeners = self.server.xml_listeners[packet] for listener in xml_listeners: - await listener(self, body_tag) + if listener.client_type is None or listener.client_type == self.client_type: + await listener(self, body_tag) self.received_packets.add(packet) else: