Initial support for multiple clients and cross-client play

This commit is contained in:
Ben 2019-08-06 22:55:11 +01:00
parent fedf855f74
commit 4fa0f4dfe0
7 changed files with 98 additions and 23 deletions

View File

@ -6,6 +6,7 @@ import config
from houdini.houdini import Houdini from houdini.houdini import Houdini
from houdini import ConflictResolution from houdini import ConflictResolution
from houdini import Language from houdini import Language
from houdini import ClientType
if __name__ == '__main__': if __name__ == '__main__':
logger = logging.getLogger('houdini') 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', command_group.add_argument('-ccm', '--command-conflict-mode', action='store', dest='command_conflict_mode',
default=config.commands['ConflictMode'].name, default=config.commands['ConflictMode'].name,
help='Command conflict mode', choices=['Silent', 'Append', 'Exception']) 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() args = parser.parse_args()
database = { database = {
@ -101,6 +129,18 @@ if __name__ == '__main__':
'ConflictMode': getattr(ConflictResolution, args.command_conflict_mode) '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 = { server = {
'Address': args.address or config.servers[args.server]['Address'], 'Address': args.address or config.servers[args.server]['Address'],
'Port': args.port or config.servers[args.server]['Port'], 'Port': args.port or config.servers[args.server]['Port'],
@ -129,6 +169,7 @@ if __name__ == '__main__':
database=database, database=database,
redis=redis, redis=redis,
commands=commands, commands=commands,
client=client,
server=server) server=server)
try: try:
asyncio.run(factory_instance.start()) asyncio.run(factory_instance.start())

View File

@ -3,6 +3,7 @@ import enum
import itertools import itertools
import importlib import importlib
import sys import sys
import config
from types import FunctionType from types import FunctionType
from houdini.converters import _listener, _ArgumentDeserializer, get_converter, do_conversion, _ConverterContext from houdini.converters import _listener, _ArgumentDeserializer, get_converter, do_conversion, _ConverterContext
@ -51,7 +52,7 @@ class Priority(enum.Enum):
class _Listener(_ArgumentDeserializer): class _Listener(_ArgumentDeserializer):
__slots__ = ['priority', 'packet', 'overrides', 'before', 'after'] __slots__ = ['priority', 'packet', 'overrides', 'before', 'after', 'client_type']
def __init__(self, packet, callback, **kwargs): def __init__(self, packet, callback, **kwargs):
super().__init__(packet.id, callback, **kwargs) super().__init__(packet.id, callback, **kwargs)
@ -60,6 +61,7 @@ class _Listener(_ArgumentDeserializer):
self.priority = kwargs.get('priority', Priority.Low) self.priority = kwargs.get('priority', Priority.Low)
self.before = kwargs.get('before') self.before = kwargs.get('before')
self.after = kwargs.get('after') self.after = kwargs.get('after')
self.client_type = kwargs.get('client')
self.overrides = kwargs.get('overrides', []) self.overrides = kwargs.get('overrides', [])

View File

@ -1,6 +1,6 @@
import config import config
from houdini import handlers from houdini import handlers, ClientType
from houdini.handlers import XMLPacket from houdini.handlers import XMLPacket
from houdini.converters import VersionChkConverter from houdini.converters import VersionChkConverter
@ -10,7 +10,15 @@ from houdini.data.buddy import BuddyList
@handlers.handler(XMLPacket('verChk')) @handlers.handler(XMLPacket('verChk'))
@handlers.allow_once @handlers.allow_once
async def handle_version_check(p, version: VersionChkConverter): 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.send_xml({'body': {'action': 'apiKO', 'r': '0'}})
await p.close() await p.close()
else: else:
@ -20,7 +28,7 @@ async def handle_version_check(p, version: VersionChkConverter):
@handlers.handler(XMLPacket('rndK')) @handlers.handler(XMLPacket('rndK'))
@handlers.allow_once @handlers.allow_once
async def handle_random_key(p, data): 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): async def get_server_presence(p, pid):

View File

@ -1,4 +1,4 @@
from houdini import handlers from houdini import handlers, ClientType
from houdini.handlers import XMLPacket, login from houdini.handlers import XMLPacket, login
from houdini.handlers.login import get_server_presence from houdini.handlers.login import get_server_presence
from houdini.converters import Credentials 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() data = await Penguin.query.where(Penguin.username == username).gino.first()
if data is None: 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) return await p.send_error_and_disconnect(100)
password_correct = await loop.run_in_executor(None, bcrypt.checkpw, 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)) confirmation_hash = Crypto.hash(os.urandom(24))
tr = p.server.redis.multi_exec() tr = p.server.redis.multi_exec()
tr.setex('{}.lkey'.format(data.id), p.server.server_config['KeyTTL'], login_key) tr.setex('{}.lkey'.format(data.username), p.server.server_config['KeyTTL'], login_key)
tr.setex('{}.ckey'.format(data.id), p.server.server_config['KeyTTL'], confirmation_hash) tr.setex('{}.ckey'.format(data.username), p.server.server_config['KeyTTL'], confirmation_hash)
await tr.execute() await tr.execute()
world_populations, buddy_presence = await get_server_presence(p, data.id) world_populations, buddy_presence = await get_server_presence(p, data.id)
if p.client_type == ClientType.Vanilla:
raw_login_data = '|'.join([str(data.id), str(data.id), data.username, login_key, str(data.approval), raw_login_data = '|'.join([str(data.id), str(data.id), data.username, login_key, str(data.approval),
str(data.rejection)]) str(data.rejection)])
await p.send_xt('l', raw_login_data, confirmation_hash, '', world_populations, buddy_presence, await p.send_xt('l', raw_login_data, confirmation_hash, '', world_populations, buddy_presence,
data.email) data.email)
else:
await p.send_xt('l', data.id, login_key, world_populations, buddy_presence)
handle_version_check = login.handle_version_check handle_version_check = login.handle_version_check
handle_random_key = login.handle_random_key handle_random_key = login.handle_random_key

View File

@ -1,8 +1,11 @@
from houdini import handlers import config
from houdini import handlers, ClientType
from houdini.handlers import XMLPacket, login 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.penguin import Penguin
from houdini.data.moderator import Ban from houdini.data.moderator import Ban
from houdini.crypto import Crypto
from datetime import datetime from datetime import datetime
@ -10,20 +13,26 @@ handle_version_check = login.handle_version_check
handle_random_key = login.handle_random_key handle_random_key = login.handle_random_key
@handlers.handler(XMLPacket('login')) @handlers.handler(XMLPacket('login'), client=ClientType.Vanilla)
@handlers.allow_once @handlers.allow_once
@handlers.depends_on_packet(XMLPacket('verChk'), XMLPacket('rndK')) @handlers.depends_on_packet(XMLPacket('verChk'), XMLPacket('rndK'))
async def handle_login(p, credentials: WorldCredentials): async def handle_login(p, credentials: WorldCredentials):
tr = p.server.redis.multi_exec() tr = p.server.redis.multi_exec()
tr.get('{}.lkey'.format(credentials.id)) tr.get('{}.lkey'.format(credentials.username))
tr.get('{}.ckey'.format(credentials.id)) tr.get('{}.ckey'.format(credentials.username))
tr.delete('{}.lkey'.format(credentials.id), '{}.ckey'.format(credentials.id)) tr.delete('{}.lkey'.format(credentials.username), '{}.ckey'.format(credentials.username))
login_key, confirmation_hash, _ = await tr.execute() login_key, confirmation_hash, _ = await tr.execute()
if login_key is None or confirmation_hash is None: if login_key is None or confirmation_hash is None:
return await p.close() 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() return await p.close()
data = await Penguin.get(credentials.id) 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.logger.info('{} logged in successfully'.format(data.username))
p.data = data p.data = data
p.login_key = credentials.login_key p.login_key = login_key
await p.send_xt('l') await p.send_xt('l')

View File

@ -58,6 +58,7 @@ class Houdini:
self.database_config_override = kwargs.get('database') self.database_config_override = kwargs.get('database')
self.redis_config_override = kwargs.get('redis') self.redis_config_override = kwargs.get('redis')
self.commands_config_override = kwargs.get('commands') self.commands_config_override = kwargs.get('commands')
self.client_config_override = kwargs.get('client')
self.server_config_override = kwargs.get('server') self.server_config_override = kwargs.get('server')
self.server_config = None self.server_config = None
@ -100,6 +101,7 @@ class Houdini:
self.config.database.update(self.database_config_override) self.config.database.update(self.database_config_override)
self.config.redis.update(self.redis_config_override) self.config.redis.update(self.redis_config_override)
self.config.commands.update(self.commands_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"]) general_log_directory = os.path.dirname(self.server_config["Logging"]["General"])
errors_log_directory = os.path.dirname(self.server_config["Logging"]["Errors"]) errors_log_directory = os.path.dirname(self.server_config["Logging"]["Errors"])
@ -215,8 +217,14 @@ class Houdini:
self.configure_observers([handlers_path, ListenerFileEventHandler], self.configure_observers([handlers_path, ListenerFileEventHandler],
[plugins_path, PluginFileEventHandler]) [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'])) 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) self.plugins.setup(houdini.plugins)
async with self.server: async with self.server:

View File

@ -13,7 +13,8 @@ from houdini.cooldown import CooldownError
class Spheniscidae: class Spheniscidae:
__slots__ = ['__reader', '__writer', 'server', 'logger', __slots__ = ['__reader', '__writer', 'server', 'logger',
'peer_name', 'received_packets', 'joined_world'] 'peer_name', 'received_packets', 'joined_world',
'client_type']
Delimiter = b'\x00' Delimiter = b'\x00'
@ -28,6 +29,7 @@ class Spheniscidae:
self.server.peers_by_ip[self.peer_name] = self self.server.peers_by_ip[self.peer_name] = self
self.joined_world = False self.joined_world = False
self.client_type = None
self.received_packets = set() self.received_packets = set()
@ -93,6 +95,7 @@ class Spheniscidae:
packet_data = parsed_data[4:] packet_data = parsed_data[4:]
for listener in xt_listeners: for listener in xt_listeners:
if listener.client_type is None or listener.client_type == self.client_type:
await listener(self, packet_data) await listener(self, packet_data)
self.received_packets.add(packet) self.received_packets.add(packet)
else: else:
@ -118,6 +121,7 @@ class Spheniscidae:
xml_listeners = self.server.xml_listeners[packet] xml_listeners = self.server.xml_listeners[packet]
for listener in xml_listeners: for listener in xml_listeners:
if listener.client_type is None or listener.client_type == self.client_type:
await listener(self, body_tag) await listener(self, body_tag)
self.received_packets.add(packet) self.received_packets.add(packet)