731 lines
21 KiB
Python
Executable File
731 lines
21 KiB
Python
Executable File
import argparse
|
|
import asyncio
|
|
import datetime
|
|
import logging
|
|
import sys
|
|
from dataclasses import dataclass, fields
|
|
from io import BytesIO
|
|
from struct import unpack, pack
|
|
from types import SimpleNamespace
|
|
from typing import IO, List, Any, Sequence, Optional, Union, Type, ClassVar
|
|
|
|
|
|
# class FlexQueue(asyncio.Queue):
|
|
# def _putleft(self, item):
|
|
# self._queue.appendleft(item)
|
|
#
|
|
# def putleft_nowait(self, item):
|
|
# """Put an item into the queue without blocking.
|
|
#
|
|
# If no free slot is immediately available, raise QueueFull.
|
|
# """
|
|
# if self.full():
|
|
# raise asyncio.QueueFull
|
|
# self._putleft(item)
|
|
# self._unfinished_tasks += 1
|
|
# self._finished.clear()
|
|
# self._wakeup_next(self._getters)
|
|
#
|
|
# async def putleft(self, item):
|
|
# """Put an item into the queue.
|
|
#
|
|
# Put an item into the queue. If the queue is full, wait until a free
|
|
# slot is available before adding item.
|
|
# """
|
|
# while self.full():
|
|
# putter = self._loop.create_future()
|
|
# self._putters.appendleft(putter)
|
|
# try:
|
|
# await putter
|
|
# except:
|
|
# putter.cancel() # Just in case putter is not done yet.
|
|
# try:
|
|
# # Clean self._putters from canceled putters.
|
|
# self._putters.remove(putter)
|
|
# except ValueError:
|
|
# # The putter could be removed from self._putters by a
|
|
# # previous get_nowait call.
|
|
# pass
|
|
# if not self.full() and not putter.cancelled():
|
|
# # We were woken up by get_nowait(), but can't take
|
|
# # the call. Wake up the next in line.
|
|
# self._wakeup_next(self._putters)
|
|
# raise
|
|
# return self.putleft_nowait(item)
|
|
|
|
|
|
class MutableInt(int):
|
|
|
|
def __new__(cls, value=0):
|
|
|
|
obj = super().__new__(cls, value)
|
|
|
|
if isinstance(value, int):
|
|
obj.data = value
|
|
elif isinstance(value, MutableInt):
|
|
obj.data = value.data
|
|
else:
|
|
obj.data = int(value)
|
|
|
|
def __str__(self):
|
|
return str(self.data)
|
|
|
|
def __repr__(self):
|
|
return repr(self.data)
|
|
|
|
def __int__(self):
|
|
return int(self.data)
|
|
|
|
def __float__(self):
|
|
return float(self.data)
|
|
|
|
def __hash__(self):
|
|
return hash(self.data)
|
|
|
|
def __getnewargs__(self):
|
|
return (self.data[:],)
|
|
|
|
def __eq__(self, value):
|
|
if isinstance(value, MutableInt):
|
|
return self.data == value.data
|
|
return self.data == value
|
|
|
|
def __lt__(self, value):
|
|
if isinstance(value, MutableInt):
|
|
return self.data < value.data
|
|
return self.data < value
|
|
|
|
def __le__(self, value):
|
|
if isinstance(value, MutableInt):
|
|
return self.data <= value.data
|
|
return self.data <= value
|
|
|
|
def __gt__(self, value):
|
|
if isinstance(value, MutableInt):
|
|
return self.data > value.data
|
|
return self.data > value
|
|
|
|
def __ge__(self, value):
|
|
if isinstance(value, MutableInt):
|
|
return self.data >= value.data
|
|
return self.data >= value
|
|
|
|
def __add__(self, other):
|
|
if isinstance(other, MutableInt):
|
|
return self.__class__(self.data + other.data)
|
|
elif isinstance(other, int):
|
|
return self.__class__(self.data + other)
|
|
return self.__class__(self.data + int(other))
|
|
|
|
def __radd__(self, other):
|
|
if isinstance(other, int):
|
|
return self.__class__(other + self.data)
|
|
return self.__class__(int(other) + self.data)
|
|
|
|
def __mul__(self, n):
|
|
return self.__class__(self.data * n)
|
|
|
|
def __truediv__(self, n):
|
|
return self.__class__(self.data / n)
|
|
|
|
def __floordiv__(self, n):
|
|
return self.__class__(self.data // n)
|
|
|
|
__rmul__ = __mul__
|
|
|
|
def __mod__(self, args):
|
|
return self.__class__(self.data % args)
|
|
|
|
def __rmod__(self, template):
|
|
return self.__class__(int(template) % self)
|
|
|
|
|
|
class MutableString(str):
|
|
|
|
def __new__(cls, seq=''):
|
|
|
|
obj = super().__new__(cls, seq)
|
|
|
|
if isinstance(seq, str):
|
|
obj.data = seq
|
|
elif isinstance(seq, MutableString):
|
|
obj.data = seq.data[:]
|
|
else:
|
|
obj.data = str(seq)
|
|
|
|
|
|
def __str__(self):
|
|
return str(self.data)
|
|
|
|
def __repr__(self):
|
|
return repr(self.data)
|
|
|
|
def __int__(self):
|
|
return int(self.data)
|
|
|
|
def __float__(self):
|
|
return float(self.data)
|
|
|
|
def __complex__(self):
|
|
return complex(self.data)
|
|
|
|
def __hash__(self):
|
|
return hash(self.data)
|
|
|
|
def __getnewargs__(self):
|
|
return (self.data[:],)
|
|
|
|
def __eq__(self, value):
|
|
if isinstance(value, MutableString):
|
|
return self.data == value.data
|
|
return self.data == value
|
|
|
|
def __lt__(self, value):
|
|
if isinstance(value, MutableString):
|
|
return self.data < value.data
|
|
return self.data < value
|
|
|
|
def __le__(self, value):
|
|
if isinstance(value, MutableString):
|
|
return self.data <= value.data
|
|
return self.data <= value
|
|
|
|
def __gt__(self, value):
|
|
if isinstance(value, MutableString):
|
|
return self.data > value.data
|
|
return self.data > value
|
|
|
|
def __ge__(self, value):
|
|
if isinstance(value, MutableString):
|
|
return self.data >= value.data
|
|
return self.data >= value
|
|
|
|
def __contains__(self, char):
|
|
if isinstance(char, MutableString):
|
|
char = char.data
|
|
return char in self.data
|
|
|
|
def __len__(self):
|
|
return len(self.data)
|
|
|
|
def __getitem__(self, index):
|
|
return self.__class__(self.data[index])
|
|
|
|
def __add__(self, other):
|
|
if isinstance(other, MutableString):
|
|
return self.__class__(self.data + other.data)
|
|
elif isinstance(other, str):
|
|
return self.__class__(self.data + other)
|
|
return self.__class__(self.data + str(other))
|
|
|
|
def __radd__(self, other):
|
|
if isinstance(other, str):
|
|
return self.__class__(other + self.data)
|
|
return self.__class__(str(other) + self.data)
|
|
|
|
def __mul__(self, n):
|
|
return self.__class__(self.data * n)
|
|
|
|
__rmul__ = __mul__
|
|
|
|
def __mod__(self, args):
|
|
return self.__class__(self.data % args)
|
|
|
|
def __rmod__(self, template):
|
|
return self.__class__(str(template) % self)
|
|
|
|
# the following methods are defined in alphabetical order:
|
|
def capitalize(self):
|
|
return self.__class__(self.data.capitalize())
|
|
|
|
def casefold(self):
|
|
return self.__class__(self.data.casefold())
|
|
|
|
def center(self, width, *args):
|
|
return self.__class__(self.data.center(width, *args))
|
|
|
|
def count(self, sub, start=0, end=sys.maxsize):
|
|
if isinstance(sub, MutableString):
|
|
sub = sub.data
|
|
return self.data.count(sub, start, end)
|
|
|
|
def removeprefix(self, prefix, /):
|
|
if isinstance(prefix, MutableString):
|
|
prefix = prefix.data
|
|
return self.__class__(self.data.removeprefix(prefix))
|
|
|
|
def removesuffix(self, suffix, /):
|
|
if isinstance(suffix, MutableString):
|
|
suffix = suffix.data
|
|
return self.__class__(self.data.removesuffix(suffix))
|
|
|
|
def encode(self, encoding='utf-8', errors='strict'):
|
|
encoding = 'utf-8' if encoding is None else encoding
|
|
errors = 'strict' if errors is None else errors
|
|
return self.data.encode(encoding, errors)
|
|
|
|
def endswith(self, suffix, start=0, end=sys.maxsize):
|
|
return self.data.endswith(suffix, start, end)
|
|
|
|
def expandtabs(self, tabsize=8):
|
|
return self.__class__(self.data.expandtabs(tabsize))
|
|
|
|
def find(self, sub, start=0, end=sys.maxsize):
|
|
if isinstance(sub, MutableString):
|
|
sub = sub.data
|
|
return self.data.find(sub, start, end)
|
|
|
|
def format(self, /, *args, **kwds):
|
|
return self.data.format(*args, **kwds)
|
|
|
|
def format_map(self, mapping):
|
|
return self.data.format_map(mapping)
|
|
|
|
def index(self, sub, start=0, end=sys.maxsize):
|
|
return self.data.index(sub, start, end)
|
|
|
|
def isalpha(self):
|
|
return self.data.isalpha()
|
|
|
|
def isalnum(self):
|
|
return self.data.isalnum()
|
|
|
|
def isascii(self):
|
|
return self.data.isascii()
|
|
|
|
def isdecimal(self):
|
|
return self.data.isdecimal()
|
|
|
|
def isdigit(self):
|
|
return self.data.isdigit()
|
|
|
|
def isidentifier(self):
|
|
return self.data.isidentifier()
|
|
|
|
def islower(self):
|
|
return self.data.islower()
|
|
|
|
def isnumeric(self):
|
|
return self.data.isnumeric()
|
|
|
|
def isprintable(self):
|
|
return self.data.isprintable()
|
|
|
|
def isspace(self):
|
|
return self.data.isspace()
|
|
|
|
def istitle(self):
|
|
return self.data.istitle()
|
|
|
|
def isupper(self):
|
|
return self.data.isupper()
|
|
|
|
def join(self, seq):
|
|
return self.data.join(seq)
|
|
|
|
def ljust(self, width, *args):
|
|
return self.__class__(self.data.ljust(width, *args))
|
|
|
|
def lower(self):
|
|
return self.__class__(self.data.lower())
|
|
|
|
def lstrip(self, chars=None):
|
|
return self.__class__(self.data.lstrip(chars))
|
|
|
|
maketrans = str.maketrans
|
|
|
|
def partition(self, sep):
|
|
return self.data.partition(sep)
|
|
|
|
def replace(self, old, new, maxsplit=-1):
|
|
if isinstance(old, MutableString):
|
|
old = old.data
|
|
if isinstance(new, MutableString):
|
|
new = new.data
|
|
return self.__class__(self.data.replace(old, new, maxsplit))
|
|
|
|
def rfind(self, sub, start=0, end=sys.maxsize):
|
|
if isinstance(sub, MutableString):
|
|
sub = sub.data
|
|
return self.data.rfind(sub, start, end)
|
|
|
|
def rindex(self, sub, start=0, end=sys.maxsize):
|
|
return self.data.rindex(sub, start, end)
|
|
|
|
def rjust(self, width, *args):
|
|
return self.__class__(self.data.rjust(width, *args))
|
|
|
|
def rpartition(self, sep):
|
|
return self.data.rpartition(sep)
|
|
|
|
def rstrip(self, chars=None):
|
|
return self.__class__(self.data.rstrip(chars))
|
|
|
|
def split(self, sep=None, maxsplit=-1):
|
|
return self.data.split(sep, maxsplit)
|
|
|
|
def rsplit(self, sep=None, maxsplit=-1):
|
|
return self.data.rsplit(sep, maxsplit)
|
|
|
|
def splitlines(self, keepends=False):
|
|
return self.data.splitlines(keepends)
|
|
|
|
def startswith(self, prefix, start=0, end=sys.maxsize):
|
|
return self.data.startswith(prefix, start, end)
|
|
|
|
def strip(self, chars=None):
|
|
return self.__class__(self.data.strip(chars))
|
|
|
|
def swapcase(self):
|
|
return self.__class__(self.data.swapcase())
|
|
|
|
def title(self):
|
|
return self.__class__(self.data.title())
|
|
|
|
def translate(self, *args):
|
|
return self.__class__(self.data.translate(*args))
|
|
|
|
def upper(self):
|
|
return self.__class__(self.data.upper())
|
|
|
|
def zfill(self, width):
|
|
return self.__class__(self.data.zfill(width))
|
|
|
|
|
|
class base_primitive:
|
|
SIZE_TYPE: ClassVar[Optional[Type[Any]]] = None
|
|
SIZE: ClassVar[Optional[int]] = None
|
|
FORMAT_STRING: ClassVar[Optional[str]] = None
|
|
|
|
def __bytes__(self):
|
|
return pack(self.FORMAT_STRING, self.data)
|
|
|
|
def __new__(cls, *args, **kwargs):
|
|
args = list(args)
|
|
|
|
data = None
|
|
if len(args) > 0:
|
|
data = args.pop()
|
|
|
|
if type(data) is bytes and len(data) == cls.SIZE:
|
|
args.insert(0, unpack(cls.FORMAT_STRING, data)[0])
|
|
return super().__new__(cls, *args, **kwargs)
|
|
|
|
|
|
class i16(base_primitive, MutableInt):
|
|
SIZE = 2
|
|
FORMAT_STRING = '<h'
|
|
|
|
|
|
class u16(base_primitive, MutableInt):
|
|
SIZE = 2
|
|
FORMAT_STRING = '<H'
|
|
|
|
|
|
class i32(base_primitive, MutableInt):
|
|
SIZE = 4
|
|
FORMAT_STRING = '<i'
|
|
|
|
|
|
class string(base_primitive, MutableString):
|
|
SIZE_TYPE = u16
|
|
|
|
def __bytes__(self):
|
|
return bytes(self.SIZE_TYPE(len(self))) + self.encode('utf-8')
|
|
|
|
def __new__(cls, *args, **kwargs):
|
|
args = list(args)
|
|
|
|
data = None
|
|
if len(args) > 0:
|
|
data = args.pop()
|
|
|
|
if data and type(data) is bytes:
|
|
args.insert(0, data.decode('utf-8'))
|
|
|
|
return super().__new__(cls, *args, **kwargs)
|
|
|
|
|
|
@dataclass
|
|
class Packet:
|
|
timestamp: datetime.datetime = datetime.datetime.now()
|
|
length: int = 0
|
|
opcode: int = 0
|
|
payload: IO[bytes] = BytesIO()
|
|
primitive_types: ClassVar[Sequence[Any]] = {i16, u16, i32, string}
|
|
|
|
def __bytes__(self):
|
|
return self.packet
|
|
|
|
def __post_init__(self):
|
|
self.read_packet()
|
|
|
|
def read_packet(self):
|
|
for field in fields(self):
|
|
if field.type in self.primitive_types:
|
|
setattr(self,
|
|
field.name,
|
|
self.read(field)
|
|
)
|
|
|
|
@property
|
|
def packet(self):
|
|
payload = self.dump_payload()
|
|
|
|
if not payload:
|
|
for field in fields(self):
|
|
if field.type in self.primitive_types:
|
|
self.write(getattr(self, field.name))
|
|
payload = self.dump_payload()
|
|
self.length = len(payload) + 2
|
|
|
|
packet_data = pack('<H', self.length) + pack('<H', self.opcode) + payload
|
|
|
|
return packet_data
|
|
|
|
def dump_payload(self):
|
|
self.payload.seek(0)
|
|
return self.payload.read()
|
|
|
|
def write(self, field):
|
|
if type(field) in self.primitive_types:
|
|
self.payload.write(bytes(field))
|
|
|
|
def read(self, field):
|
|
if field.type in self.primitive_types:
|
|
if field.type.SIZE is None:
|
|
return field.type(
|
|
self.payload.read(
|
|
field.type.SIZE_TYPE(self.payload.read(field.type.SIZE_TYPE.SIZE))
|
|
)
|
|
)
|
|
return field.type(self.payload.read(field.type.SIZE))
|
|
|
|
|
|
@dataclass
|
|
class KeepAlive(Packet):
|
|
opcode: int = 2
|
|
account_name: string = None
|
|
server_name: string = None
|
|
player_name: string = None
|
|
module_name: string = None
|
|
exe_path_info: string = None
|
|
client_version: string = None
|
|
player_host_name: string = None
|
|
host_name: string = None
|
|
key: i32 = None
|
|
player_net_addr: string = None
|
|
windows_version: string = None
|
|
world_id: i16 = None
|
|
|
|
|
|
class Module:
|
|
ALLOWED_PACKETS: ClassVar[Sequence[Any]] = {}
|
|
|
|
def __init__(self):
|
|
self.task: Optional[asyncio.Task] = None
|
|
self._packet_queue: Optional[asyncio.Queue] = None
|
|
self._operation_queue: Optional[asyncio.Queue] = None
|
|
self._stopping: bool = False
|
|
self._logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
|
|
|
def run(self, packet_queue: asyncio.Queue, operation_queue: asyncio.Queue):
|
|
self._packet_queue = packet_queue
|
|
self._operation_queue = operation_queue
|
|
|
|
self.task = asyncio.create_task(self.start())
|
|
|
|
async def start(self):
|
|
while type(packet := await self._packet_queue.get()) in self.ALLOWED_PACKETS:
|
|
await self._operation_queue.put([print, [f'Print operation for {packet.packet} from {self.__class__.__name__}']])
|
|
self._packet_queue.task_done()
|
|
else:
|
|
await self._packet_queue.put(packet)
|
|
self._packet_queue.task_done()
|
|
|
|
def stop(self):
|
|
if self._stopping:
|
|
return
|
|
|
|
self._stopping = True
|
|
|
|
self.task.cancel()
|
|
|
|
self._logger.info('Shutting down')
|
|
self._logger.info('Shutdown complete')
|
|
|
|
|
|
class World(Module):
|
|
ALLOWED_PACKETS = {KeepAlive}
|
|
pass
|
|
|
|
|
|
class Login(Module):
|
|
ALLOWED_PACKETS = {Packet}
|
|
pass
|
|
|
|
|
|
class EOS:
|
|
pass
|
|
|
|
|
|
class Dispatcher:
|
|
|
|
def __init__(self, streams: List[IO[bytes]], modules: List[Module], timeout: int = 5):
|
|
self._streams = streams
|
|
self.modules = modules
|
|
self.timeout = timeout
|
|
|
|
self._logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
|
self._stopping: bool = False
|
|
self._packet_queue: Optional[asyncio.Queue] = None
|
|
self._operation_queue: Optional[asyncio.Queue] = None
|
|
self.latency: float = 0
|
|
self.last_packet_time: datetime.datetime = datetime.datetime.now()
|
|
|
|
self.opcodes = {
|
|
2: KeepAlive
|
|
}
|
|
|
|
def run(self):
|
|
asyncio.run(self.start(), debug=True)
|
|
|
|
async def start(self):
|
|
self._logger.info('Starting up')
|
|
|
|
self._packet_queue = asyncio.Queue()
|
|
self._operation_queue = asyncio.Queue()
|
|
|
|
for m in self.modules:
|
|
m.run(self._packet_queue, self._operation_queue)
|
|
|
|
tasks = [m.task for m in self.modules]
|
|
tasks.append(asyncio.create_task(self._parse_streams()))
|
|
tasks.append(asyncio.create_task(self._handle_operations()))
|
|
|
|
try:
|
|
await asyncio.gather(
|
|
*tasks,
|
|
return_exceptions=False
|
|
)
|
|
except TimeoutError:
|
|
self._logger.info(f'EOS was sustained for over {self.timeout} seconds: {self.latency}')
|
|
|
|
await self._packet_queue.join()
|
|
await self._operation_queue.join()
|
|
|
|
self.stop()
|
|
|
|
def stop(self):
|
|
if self._stopping:
|
|
return
|
|
|
|
self._stopping = True
|
|
|
|
self._logger.info('Shutting down')
|
|
|
|
for m in self.modules:
|
|
m.stop()
|
|
|
|
self._logger.info('Shutdown complete')
|
|
|
|
async def _parse_streams(self):
|
|
while packets := await asyncio.gather(*[self._read_packet(s) for s in self._streams]):
|
|
|
|
if (datetime.datetime.now() - self.last_packet_time).total_seconds() > self.timeout:
|
|
raise TimeoutError
|
|
|
|
for p in [p for p in packets if p is not EOS]:
|
|
await self._packet_queue.put(p)
|
|
self.latency = (p.timestamp - self.last_packet_time).total_seconds()
|
|
|
|
await asyncio.sleep(self.latency / 2)
|
|
|
|
async def _handle_operations(self):
|
|
while operation := await self._operation_queue.get():
|
|
operation[0](*operation[1])
|
|
self._operation_queue.task_done()
|
|
|
|
async def _read_packet(self, stream: IO[bytes]) -> Union[Packet, Type[EOS]]:
|
|
length = u16(stream.read(u16.SIZE))
|
|
opcode = u16(stream.read(u16.SIZE))
|
|
|
|
if not all([length, opcode]):
|
|
return EOS
|
|
|
|
payload = BytesIO(stream.read(length - 2))
|
|
|
|
self.last_packet_time = datetime.datetime.now()
|
|
return self.opcodes.get(opcode, Packet)(self.last_packet_time, length, opcode, payload)
|
|
|
|
|
|
def main(args: argparse.Namespace):
|
|
logging.basicConfig(
|
|
stream=sys.stdout,
|
|
level='DEBUG',
|
|
format='[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s'
|
|
)
|
|
|
|
streams = []
|
|
# file_stream = open('keepalive2.bin', 'rb')
|
|
# streams.append(file_stream)
|
|
|
|
import gzip
|
|
fstream = open('aura_local_1600222217.xlog.gz', 'rb')
|
|
fstream.read(u16(fstream.read(u16.SIZE)))
|
|
fstream.read(u16(fstream.read(u16.SIZE)))
|
|
|
|
gzip_stream = BytesIO(gzip.decompress(fstream.read()))
|
|
|
|
packet_stream = BytesIO()
|
|
i = 0
|
|
while packet := gzip_stream.read(u16(gzip_stream.read(u16.SIZE))):
|
|
i += 1
|
|
if i > 1:
|
|
break
|
|
packet = BytesIO(packet)
|
|
packet_data = SimpleNamespace(
|
|
kind=packet.read(1),
|
|
opcode=u16(packet.read(u16.SIZE)),
|
|
timestamp=packet.read(8),
|
|
src=packet.read(u16(packet.read(u16.SIZE))),
|
|
dest=packet.read(u16(packet.read(u16.SIZE))),
|
|
length=unpack('<h', packet.read(u16.SIZE))[0]
|
|
)
|
|
setattr(packet_data, 'payload', packet.read(packet_data.length))
|
|
packet_stream.write(pack('<h', packet_data.length + 2))
|
|
packet_stream.write(bytes(packet_data.opcode))
|
|
packet_stream.write(packet_data.payload)
|
|
|
|
length = u16(b'\x06\x00')
|
|
print(length)
|
|
print(bytes(length))
|
|
length = u16(length) + u16(2)
|
|
print(length)
|
|
print(bytes(length))
|
|
packet_stream.seek(0)
|
|
print(packet_stream.read())
|
|
|
|
packet_stream.seek(0)
|
|
streams.append(packet_stream)
|
|
|
|
# file_stream3 = open('keepalive.bin', 'rb')
|
|
# streams.append(file_stream3)
|
|
|
|
modules = [
|
|
World(),
|
|
Login()
|
|
]
|
|
|
|
dispatcher = Dispatcher(streams=streams, modules=modules, timeout=5)
|
|
dispatcher.run()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser(description='Aura Kingdom client')
|
|
parent_parser = argparse.ArgumentParser(add_help=False)
|
|
subparsers = parser.add_subparsers()
|
|
args = parser.parse_args()
|
|
|
|
main(args)
|