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 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())

View File

@ -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', [])

View File

@ -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):

View File

@ -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

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.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')

View File

@ -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:

View File

@ -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: