mirror of
https://github.com/solero/houdini.git
synced 2024-11-22 21:47:20 +00:00
Initial support for multiple clients and cross-client play
This commit is contained in:
parent
fedf855f74
commit
4fa0f4dfe0
41
bootstrap.py
41
bootstrap.py
@ -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())
|
||||||
|
@ -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', [])
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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')
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user