Compare commits

...

2 Commits

Author SHA1 Message Date
Casey d6772f6db2
Uhh i forgot 2023-09-04 22:03:31 +03:00
Casey 7ac026dccc
1.7.7.0_02 and some other fixes 2023-08-28 22:07:33 +03:00
13 changed files with 238 additions and 36 deletions

1
.gitignore vendored
View File

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

78
README.md Normal file
View File

@ -0,0 +1,78 @@
| 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 from argparse import ArgumentParser, Namespace
from sys import argv
from bta_proxy.proxy import BTAProxy from bta_proxy.proxy import BTAProxy
MAX_SIZE = 0x400000 parser = ArgumentParser("bta_proxy", description="Better Than Adventure proxy with Deep Packet Inspection")
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()
port: int = 25565 proxy = BTAProxy(args.remote_host, args.remote_port, loop)
if len(args) >= 2: server = await asyncio.start_server(proxy.handle_client, args.bind, args.bind_port)
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[0], port) print("forwarding to", args.remote_host, args.remote_port)
async with server: async with server:
await server.serve_forever() await server.serve_forever()
if __name__ == '__main__': if __name__ == '__main__':
asyncio.run(main(argv[1:])) asyncio.run(main(parser.parse_args()))

View File

@ -1,27 +1,39 @@
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:
if len(self._buffer) < n: logger.debug(f"trying to read {n} bytes")
self._last = (await self._queue.get()) if len(self.buffer) < n:
if not self._last: self.last = (await self.queue.get())
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
out, self._buffer = self._buffer[:n], self._buffer[n:] self.offset -= len(self.last)
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
@ -73,7 +85,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-%s-%d-client.txt.gz" % addr, "wt") f = open_gzip("packets-%d-%s-%d-client.txt.gz" % (int(time.time()), addr[0], addr[1]), "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-%s-%d-server.txt.gz" % addr, "wt") f = open_gzip("packets-%d-%s-%d-server.txt.gz" % (int(time.time()), addr[0], addr[1]), "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,6 +6,10 @@ 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
@ -25,7 +29,8 @@ 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()) != 0x7F: while (data := await dis.read()) not in (127, 255):
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:
@ -49,7 +54,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()) != 0x7F: while (data := dis.read()) not in (127, 255):
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,3 +80,4 @@ 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,7 +4,9 @@ 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:
@ -24,6 +26,7 @@ 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:
@ -35,6 +38,7 @@ 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
@ -46,14 +50,21 @@ 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)
length = sizekey if isinstance(try_int(sizekey), int) else fields[sizekey] size = try_int(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":
@ -156,6 +167,7 @@ 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)
@ -166,13 +178,15 @@ 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.read_rest()[:16]}...)" f"invalid packet 0x{packet_id:02x} ({packet_id}) (rest: {stream.peek_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):
@ -181,7 +195,8 @@ 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} if {cond}") fields.append(f"{key}={getattr(self, key, None)!r} depending on {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 = [
('players', 'str'), ('n_players', 'int'),
('scores', 'str'), ('players', ('list', 'n_players', 'tuple', 'str', 'int')),
] ]

View File

@ -1,12 +1,11 @@
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', 'items') __slots__ = ('x', 'y', 'z', 'colors')
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

@ -0,0 +1,7 @@
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', 'byte'), ('chatcolor', 'ubyte'),
] ]

84
tools/packetreader.py Normal file
View File

@ -0,0 +1,84 @@
# 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])