Compare commits

..

No commits in common. "d6772f6db296dd1dc1bb24c2a0af6fe17f47ffbb" and "36b226447166c72a5bf7194e9eab66213359c2b5" have entirely different histories.

13 changed files with 36 additions and 238 deletions

1
.gitignore vendored
View File

@ -6,4 +6,3 @@ packets.txt
packets.txt.gz packets.txt.gz
packets*.txt packets*.txt
packets*.txt.gz packets*.txt.gz
/packets/

View File

@ -1,78 +0,0 @@
| ID | Packet name | Ready |
+=====+==================================+=======+
| 000 | Packet0KeepAlive | YES |
| 001 | Packet1Login | YES |
| 002 | Packet2Handshake | YES |
| 003 | Packet3Chat | YES |
| 004 | Packet4UpdateTime | YES |
| 005 | Packet5PlayerInventory | YES |
| 006 | Packet6SpawnPosition | YES |
| 007 | Packet7UseEntity | YES |
| 008 | Packet8UpdateHealth | YES |
| 009 | Packet9Respawn | YES |
| 010 | Packet10Flying | YES |
| 011 | Packet11PlayerPosition | YES |
| 012 | Packet12PlayerLook | YES |
| 013 | Packet13PlayerLookMove | YES |
| 014 | Packet14BlockDig | YES |
| 015 | Packet15Place | YES |
| 016 | Packet16BlockItemSwitch | YES |
| 017 | Packet17Sleep | YES |
| 018 | Packet18Animation | YES |
| 019 | Packet19EntityAction | YES |
| 020 | Packet20NamedEntitySpawn | YES |
| 021 | Packet21PickupSpawn | YES |
| 022 | Packet22Collect | YES |
| 023 | Packet23VehicleSpawn | YES |
| 024 | Packet24MobSpawn | YES |
| 025 | Packet25EntityPainting | YES |
| 027 | Packet27Position | No |
| 028 | Packet28EntityVelocity | YES |
| 029 | Packet29DestroyEntity | YES |
| 030 | Packet30Entity | YES |
| 031 | Packet31RelEntityMove | YES |
| 032 | Packet32EntityLook | YES |
| 033 | Packet33RelEntityMoveLook | YES |
| 034 | Packet34EntityTeleport | YES |
| 035 | Packet35EntityNickname | YES |
| 038 | Packet38EntityStatus | YES |
| 039 | Packet39AttachEntity | YES |
| 040 | Packet40EntityMetadata | YES |
| 041 | Packet41EntityPlayerGamemode | YES |
| 050 | Packet50PreChunk | YES |
| 051 | Packet51MapChunk | YES |
| 052 | Packet52MultiBlockChange | YES |
| 053 | Packet53BlockChange | YES |
| 054 | Packet54PlayNoteBlock | YES |
| 056 | Packet56RequestChunk | No |
| 060 | Packet60Explosion | YES |
| 061 | Packet61PlaySoundEffect | YES |
| 070 | Packet70Bed | No |
| 071 | Packet71Weather | No |
| 072 | Packet72UpdatePlayerProfile | YES |
| 073 | Packet73WeatherStatus | YES |
| 100 | Packet100OpenWindow | YES |
| 101 | Packet101CloseWindow | YES |
| 102 | Packet102WindowClick | YES |
| 103 | Packet103SetSlot | YES |
| 104 | Packet104WindowItems | YES |
| 105 | Packet105UpdateProgressbar | YES |
| 106 | Packet106Transaction | YES |
| 107 | Packet107UpdateCreativeInventory | YES |
| 108 | Packet108SetHotbarOffset | YES |
| 130 | Packet130UpdateSign | YES |
| 131 | Packet131MapData | YES |
| 132 | Packet132SetMobSpawner | YES |
| 133 | Packet133OpenGuidebook | YES |
| 134 | Packet134ItemData | YES |
| 135 | Packet135PlacementMode | YES |
| 136 | Packet136SendKey | YES |
| 137 | Packet137UpdateFlag | YES |
| 138 | Packet138PlayerList | YES |
| 139 | Packet139SetPaintingMotive | YES |
| 140 | Packet140TileEntityData | YES |
| 141 | Packet141UpdateFlag | YES |
| 142 | Packet142OpenFlagWindow | YES |
| 143 | Packet143PhotoMode | YES |
| 200 | Packet200Statistic | YES |
| 255 | Packet255KickDisconnect | YES |

View File

@ -1,27 +1,27 @@
# x-run: cd .. && python -m bta_proxy '201:4f8c:4ea:0:71ec:6d7:6f1b:a4f9' # x-run: cd .. && python -m bta_proxy '201:4f8c:4ea:0:71ec:6d7:6f1b:a4f9'
import asyncio import asyncio
from argparse import ArgumentParser, Namespace from argparse import ArgumentParser
from sys import argv
from bta_proxy.proxy import BTAProxy from bta_proxy.proxy import BTAProxy
parser = ArgumentParser("bta_proxy", description="Better Than Adventure proxy with Deep Packet Inspection") MAX_SIZE = 0x400000
parser.add_argument("remote_host", type=str)
parser.add_argument("remote_port", type=int, default=25565)
parser.add_argument("--bind", type=str, default="127.0.0.1")
parser.add_argument("--bind-port", type=int, default=25565)
async def main(args: Namespace):
async def main(args: list[str]):
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
proxy = BTAProxy(args.remote_host, args.remote_port, loop) port: int = 25565
server = await asyncio.start_server(proxy.handle_client, args.bind, args.bind_port) if len(args) >= 2:
port = int(args[1])
server = await asyncio.start_server(BTAProxy(args[0], port, loop).handle_client, "localhost", 25565)
print("listening on", str.join(", ", [str(s.getsockname()) for s in server.sockets])) print("listening on", str.join(", ", [str(s.getsockname()) for s in server.sockets]))
print("forwarding to", args.remote_host, args.remote_port) print("forwarding to", args[0], port)
async with server: async with server:
await server.serve_forever() await server.serve_forever()
if __name__ == '__main__': if __name__ == '__main__':
asyncio.run(main(parser.parse_args())) asyncio.run(main(argv[1:]))

View File

@ -1,39 +1,27 @@
from asyncio.queues import Queue from asyncio.queues import Queue
from logging import getLogger
import struct import struct
logger = getLogger(__name__)
class AsyncDataInputStream: class AsyncDataInputStream:
def __init__(self, queue: Queue): def __init__(self, queue: Queue):
self.queue = queue self._queue = queue
self.buffer = b'' self._buffer = b''
self.last = b'' self._last = b''
self.offset = 0
def peek_rest(self):
return self.buffer
def read_rest(self): def read_rest(self):
out = self.buffer out = self._buffer
self.buffer = b'' self._buffer = b''
return out return out
async def read_bytes(self, n: int) -> bytes: async def read_bytes(self, n: int) -> bytes:
logger.debug(f"trying to read {n} bytes") if len(self._buffer) < n:
if len(self.buffer) < n: self._last = (await self._queue.get())
self.last = (await self.queue.get()) if not self._last:
logger.debug(f"new packet from the queue {self.last!r}")
if not self.last:
raise EOFError('empty packet was received') raise EOFError('empty packet was received')
self.buffer += self.last self._buffer += self._last
self.offset -= len(self.last) out, self._buffer = self._buffer[:n], self._buffer[n:]
out, self.buffer = self.buffer[:n], self.buffer[n:]
self.offset += n
return out return out
async def read(self) -> int: async def read(self) -> int:
self.offset += 1
return (await self.read_bytes(1))[0] return (await self.read_bytes(1))[0]
read_ubyte = read read_ubyte = read
@ -85,7 +73,7 @@ class AsyncDataInputStream:
return value return value
async def read_string(self) -> str: async def read_string(self) -> str:
last = self.last last = self._last
size = await self.read_short() size = await self.read_short()
try: try:
return (await self.read_bytes(size)).decode('utf-8') return (await self.read_bytes(size)).decode('utf-8')

View File

@ -25,7 +25,7 @@ async def inspect_client(queue: Queue, addr: tuple[str, int]):
last_time = time.time() last_time = time.time()
f = open_gzip("packets-%d-%s-%d-client.txt.gz" % (int(time.time()), addr[0], addr[1]), "wt") f = open_gzip("packets-%s-%d-client.txt.gz" % addr, "wt")
get_event_loop().create_task(queue_writer(queue, stream_queue, f)) get_event_loop().create_task(queue_writer(queue, stream_queue, f))
stats: dict[int, int] = {} stats: dict[int, int] = {}
@ -68,7 +68,7 @@ async def inspect_server(queue: Queue, addr: tuple[str, int]):
last_time = time.time() last_time = time.time()
f = open_gzip("packets-%d-%s-%d-server.txt.gz" % (int(time.time()), addr[0], addr[1]), "wt") f = open_gzip("packets-%s-%d-server.txt.gz" % addr, "wt")
get_event_loop().create_task(queue_writer(queue, stream_queue, f)) get_event_loop().create_task(queue_writer(queue, stream_queue, f))
stats: dict[int, int] = {} stats: dict[int, int] = {}
@ -102,8 +102,8 @@ async def inspect_server(queue: Queue, addr: tuple[str, int]):
continue continue
case Packet33RelEntityMoveLook.packet_id: case Packet33RelEntityMoveLook.packet_id:
continue continue
# case Packet73WeatherStatus.packet_id: case Packet73WeatherStatus.packet_id:
# continue continue
case Packet52MultiBlockChange.packet_id: case Packet52MultiBlockChange.packet_id:
continue continue
case _: case _:

View File

@ -6,10 +6,6 @@ from dataclasses import dataclass
from bta_proxy.itemstack import ItemStack from bta_proxy.itemstack import ItemStack
from logging import getLogger
logger = getLogger(__name__)
class DataItemType(Enum): class DataItemType(Enum):
BYTE = 0 BYTE = 0
SHORT = 1 SHORT = 1
@ -29,8 +25,7 @@ class EntityData:
@classmethod @classmethod
async def read_from(cls, dis: AsyncDataInputStream) -> list[DataItem]: async def read_from(cls, dis: AsyncDataInputStream) -> list[DataItem]:
items = [] items = []
while (data := await dis.read()) not in (127, 255): while (data := await dis.read()) != 0x7F:
logger.debug(f"Read byte: {data} (type={(data & 0xE0) >> 5}, id={data & 0x1F})")
item_type = DataItemType((data & 0xE0) >> 5) item_type = DataItemType((data & 0xE0) >> 5)
item_id: int = data & 0x1F item_id: int = data & 0x1F
match item_type: match item_type:
@ -54,7 +49,7 @@ class EntityData:
@classmethod @classmethod
def read_from_sync(cls, dis: SyncDataInputStream) -> list[DataItem]: def read_from_sync(cls, dis: SyncDataInputStream) -> list[DataItem]:
items = [] items = []
while (data := dis.read()) not in (127, 255): while (data := dis.read()) != 0x7F:
item_type = DataItemType((data & 0xE0) >> 5) item_type = DataItemType((data & 0xE0) >> 5)
item_id: int = data & 0x1F item_id: int = data & 0x1F
match item_type: match item_type:

View File

@ -80,4 +80,3 @@ from .packet108sethotbaroffset import Packet108SetHotbarOffset
from .packet5playerinventory import Packet5PlayerInventory from .packet5playerinventory import Packet5PlayerInventory
from .packet5playerinventory import Packet5PlayerInventory from .packet5playerinventory import Packet5PlayerInventory
from .packet5playerinventory import Packet5PlayerInventory from .packet5playerinventory import Packet5PlayerInventory
from .packet143photomode import Packet143PhotoMode

View File

@ -4,9 +4,7 @@ import gzip
from bta_proxy.entitydata import EntityData from bta_proxy.entitydata import EntityData
from bta_proxy.itemstack import ItemStack from bta_proxy.itemstack import ItemStack
from ..datainputstream import AsyncDataInputStream from ..datainputstream import AsyncDataInputStream
from logging import getLogger
logger = getLogger(__name__)
def try_int(v: str) -> Union[str, int]: def try_int(v: str) -> Union[str, int]:
try: try:
@ -26,7 +24,6 @@ class Packet:
@classmethod @classmethod
async def read_data_from(cls, stream: AsyncDataInputStream) -> "Packet": async def read_data_from(cls, stream: AsyncDataInputStream) -> "Packet":
logger.debug("Packet.read_data_from(%r)", stream)
fields: dict = {} fields: dict = {}
for key, datatype in cls.FIELDS: for key, datatype in cls.FIELDS:
if "?" in key: if "?" in key:
@ -38,7 +35,6 @@ class Packet:
elif not fields[cond]: elif not fields[cond]:
continue continue
try: try:
logger.debug(f"reading {key=} of type {datatype!r} ({fields=})")
fields[key] = await cls.read_field(stream, datatype, fields) fields[key] = await cls.read_field(stream, datatype, fields)
except Exception as e: except Exception as e:
raise ValueError(f"Failed getting key {key} on {cls}") from e raise ValueError(f"Failed getting key {key} on {cls}") from e
@ -50,21 +46,14 @@ class Packet:
datatype: Any, datatype: Any,
fields: dict[str, Any] = {}, fields: dict[str, Any] = {},
): ):
logger.debug(f"Packet.read_field(_, {datatype=}, {fields=})")
match datatype: match datatype:
case "list", sizekey, *args: case "list", sizekey, *args:
args = args[0] if len(args) == 1 else tuple(args) args = args[0] if len(args) == 1 else tuple(args)
size = try_int(sizekey) length = sizekey if isinstance(try_int(sizekey), int) else fields[sizekey]
length = size if isinstance(size, int) else fields[sizekey]
return [ return [
await Packet.read_field(stream, args, fields) await Packet.read_field(stream, args, fields)
for _ in range(length) for _ in range(length)
] ]
case "tuple", *tuples:
out = []
for tup in tuples:
out.append(await Packet.read_field(stream, tup, fields))
return tuple(out)
case "uint": case "uint":
return await stream.read_uint() return await stream.read_uint()
case "int": case "int":
@ -167,7 +156,6 @@ class Packet:
raise ValueError(f"unknown type {datatype}") raise ValueError(f"unknown type {datatype}")
def __init_subclass__(cls, packet_id: int, **kwargs) -> None: def __init_subclass__(cls, packet_id: int, **kwargs) -> None:
logger.debug(f"registered packet {cls} with id = {packet_id}")
Packet.REGISTRY[packet_id] = cls Packet.REGISTRY[packet_id] = cls
cls.packet_id = packet_id cls.packet_id = packet_id
super().__init_subclass__(**kwargs) super().__init_subclass__(**kwargs)
@ -178,15 +166,13 @@ class Packet:
@classmethod @classmethod
async def read_packet(cls, stream: AsyncDataInputStream) -> "Packet": async def read_packet(cls, stream: AsyncDataInputStream) -> "Packet":
packet_id: int = await stream.read() packet_id: int = await stream.read()
logger.debug(f"incoming {packet_id=}")
if packet_id not in cls.REGISTRY: if packet_id not in cls.REGISTRY:
raise ValueError( raise ValueError(
f"invalid packet 0x{packet_id:02x} ({packet_id}) (rest: {stream.peek_rest()[:16]}...)" f"invalid packet 0x{packet_id:02x} ({packet_id}) (rest: {stream.read_rest()[:16]}...)"
) )
pkt = await cls.REGISTRY[packet_id].read_data_from(stream) pkt = await cls.REGISTRY[packet_id].read_data_from(stream)
pkt.packet_id = packet_id pkt.packet_id = packet_id
pkt.post_creation() pkt.post_creation()
logger.debug(f"received {pkt}")
return pkt return pkt
def __repr__(self): def __repr__(self):
@ -195,8 +181,7 @@ class Packet:
for key, _ in self.FIELDS: for key, _ in self.FIELDS:
if "?" in key: if "?" in key:
key, cond = key.split("?", 1) key, cond = key.split("?", 1)
fields.append(f"{key}={getattr(self, key, None)!r} depending on {cond}") fields.append(f"{key}={getattr(self, key, None)!r} if {cond}")
else: else:
fields.append(f"{key}={getattr(self, key)!r}") fields.append(f"{key}={getattr(self, key)!r}")
return f'<{pkt_name} {str.join(", ", fields)}>' return f'<{pkt_name} {str.join(", ", fields)}>'

View File

@ -2,6 +2,6 @@ from .base import Packet
class Packet138PlayerList(Packet, packet_id=138): class Packet138PlayerList(Packet, packet_id=138):
FIELDS = [ FIELDS = [
('n_players', 'int'), ('players', 'str'),
('players', ('list', 'n_players', 'tuple', 'str', 'int')), ('scores', 'str'),
] ]

View File

@ -1,11 +1,12 @@
from .base import Packet from .base import Packet
class Packet141UpdateFlag(Packet, packet_id=141): class Packet141UpdateFlag(Packet, packet_id=141):
__slots__ = ('x', 'y', 'z', 'colors') __slots__ = ('x', 'y', 'z', 'colors', 'items')
FIELDS = [ FIELDS = [
('x', 'int'), ('x', 'int'),
('y', 'short'), ('y', 'short'),
('z', 'int'), ('z', 'int'),
('colors', ('bytes', 384)), ('colors', ('bytes', 384)),
('items', ('list', 3, 'nbt')),
('owner', 'string') ('owner', 'string')
] ]

View File

@ -1,7 +0,0 @@
from .base import Packet
class Packet143PhotoMode(Packet, packet_id=143):
__slots__ = ('disabled',)
FIELDS = [
('disabled', 'bool'),
]

View File

@ -11,5 +11,5 @@ class Packet24MobSpawn(Packet, packet_id=24):
('pitch', 'byte'), ('pitch', 'byte'),
('metadata', 'entitydata'), ('metadata', 'entitydata'),
('nickname', 'string'), ('nickname', 'string'),
('chatcolor', 'ubyte'), ('chatcolor', 'byte'),
] ]

View File

@ -1,84 +0,0 @@
# x-run: PYTHONPATH=.. python packetreader.py ../packets-127.0.0.1-54356-server.txt.gz
from asyncio.queues import Queue
import asyncio
from bta_proxy.datainputstream import AsyncDataInputStream
from bta_proxy.packets import Packet
from sys import argv
from gzip import open as open_gzip
from json import loads
import logging
loggers = [
logging.getLogger(name)
for name in logging.root.manager.loggerDict
if name.startswith("bta_proxy")
]
class CustomFormatter(logging.Formatter):
grey = "\x1b[38;20m"
yellow = "\x1b[93;20m"
red = "\x1b[91;20m"
bold_red = "\x1b[91;1m"
reset = "\x1b[0m"
fmt = "(%(filename)s:%(lineno)d) %(name)s - %(message)s"
FORMATS = {
logging.DEBUG: "\x1b[92m" + fmt + reset,
logging.INFO: "\x1b[94m" + fmt + reset,
logging.WARNING: yellow + fmt + reset,
logging.ERROR: red + fmt + reset,
logging.CRITICAL: bold_red + fmt + reset
}
def format(self, record):
log_fmt = self.FORMATS.get(record.levelno)
formatter = logging.Formatter(log_fmt)
return formatter.format(record)
streamhandler = logging.StreamHandler()
streamhandler.setLevel(logging.DEBUG)
streamhandler.setFormatter(CustomFormatter())
for logger in loggers:
logger.setLevel(logging.DEBUG)
logger = logging.getLogger("packetreader")
logger.setLevel(logging.DEBUG)
logging.getLogger().addHandler(streamhandler)
async def amain(stream: AsyncDataInputStream):
while True:
try:
pkt = await Packet.read_packet(stream)
logger.info(f"we just got a package {pkt!r}")
except EOFError:
logger.warning("EOFError")
break
except Exception as e:
logger.error(e)
raise e
logger.info("exiting")
def main(filename: str):
queue = Queue()
with open_gzip(filename, "rt") as fp:
for line in fp:
data = loads(line.strip())
queue.put_nowait(bytes.fromhex(data["b"]))
queue.put_nowait(None)
stream = AsyncDataInputStream(queue)
loop = asyncio.get_event_loop()
loop.run_until_complete(amain(stream))
while True:
pass
if __name__ == "__main__":
main(argv[1])