initial commit

This commit is contained in:
vance 2023-01-05 21:17:59 -08:00
commit 0358f86cda
7 changed files with 734 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/.idea
/__pycache__

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# akpacketparser
an experiment with parsing packets from aura kingdom (and other games) using struct and dataclasses

BIN
aura_local_1600222217.xlog.gz Executable file

Binary file not shown.

BIN
eden_1615699924.xlog.gz Executable file

Binary file not shown.

BIN
keepalive.bin Executable file

Binary file not shown.

BIN
keepalive2.bin Executable file

Binary file not shown.

730
main.py Executable file
View File

@ -0,0 +1,730 @@
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)