Compare commits
2 Commits
36b2264471
...
d6772f6db2
Author | SHA1 | Date |
---|---|---|
Casey | d6772f6db2 | |
Casey | 7ac026dccc |
|
@ -6,3 +6,4 @@ packets.txt
|
|||
packets.txt.gz
|
||||
packets*.txt
|
||||
packets*.txt.gz
|
||||
/packets/
|
||||
|
|
|
@ -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 |
|
|
@ -1,27 +1,27 @@
|
|||
# x-run: cd .. && python -m bta_proxy '201:4f8c:4ea:0:71ec:6d7:6f1b:a4f9'
|
||||
import asyncio
|
||||
from argparse import ArgumentParser
|
||||
from sys import argv
|
||||
from argparse import ArgumentParser, Namespace
|
||||
|
||||
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: list[str]):
|
||||
async def main(args: Namespace):
|
||||
loop = asyncio.get_running_loop()
|
||||
port: int = 25565
|
||||
if len(args) >= 2:
|
||||
port = int(args[1])
|
||||
server = await asyncio.start_server(BTAProxy(args[0], port, loop).handle_client, "localhost", 25565)
|
||||
proxy = BTAProxy(args.remote_host, args.remote_port, loop)
|
||||
server = await asyncio.start_server(proxy.handle_client, args.bind, args.bind_port)
|
||||
|
||||
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:
|
||||
await server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main(argv[1:]))
|
||||
asyncio.run(main(parser.parse_args()))
|
||||
|
|
|
@ -1,27 +1,39 @@
|
|||
from asyncio.queues import Queue
|
||||
from logging import getLogger
|
||||
import struct
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
class AsyncDataInputStream:
|
||||
def __init__(self, queue: Queue):
|
||||
self._queue = queue
|
||||
self._buffer = b''
|
||||
self._last = b''
|
||||
self.queue = queue
|
||||
self.buffer = b''
|
||||
self.last = b''
|
||||
self.offset = 0
|
||||
|
||||
def peek_rest(self):
|
||||
return self.buffer
|
||||
|
||||
def read_rest(self):
|
||||
out = self._buffer
|
||||
self._buffer = b''
|
||||
out = self.buffer
|
||||
self.buffer = b''
|
||||
return out
|
||||
|
||||
async def read_bytes(self, n: int) -> bytes:
|
||||
if len(self._buffer) < n:
|
||||
self._last = (await self._queue.get())
|
||||
if not self._last:
|
||||
logger.debug(f"trying to read {n} bytes")
|
||||
if len(self.buffer) < n:
|
||||
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')
|
||||
self._buffer += self._last
|
||||
out, self._buffer = self._buffer[:n], self._buffer[n:]
|
||||
self.buffer += self.last
|
||||
self.offset -= len(self.last)
|
||||
out, self.buffer = self.buffer[:n], self.buffer[n:]
|
||||
self.offset += n
|
||||
return out
|
||||
|
||||
async def read(self) -> int:
|
||||
self.offset += 1
|
||||
return (await self.read_bytes(1))[0]
|
||||
|
||||
read_ubyte = read
|
||||
|
@ -73,7 +85,7 @@ class AsyncDataInputStream:
|
|||
return value
|
||||
|
||||
async def read_string(self) -> str:
|
||||
last = self._last
|
||||
last = self.last
|
||||
size = await self.read_short()
|
||||
try:
|
||||
return (await self.read_bytes(size)).decode('utf-8')
|
||||
|
|
|
@ -25,7 +25,7 @@ async def inspect_client(queue: Queue, addr: tuple[str, int]):
|
|||
|
||||
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))
|
||||
|
||||
stats: dict[int, int] = {}
|
||||
|
@ -68,7 +68,7 @@ async def inspect_server(queue: Queue, addr: tuple[str, int]):
|
|||
|
||||
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))
|
||||
|
||||
stats: dict[int, int] = {}
|
||||
|
@ -102,8 +102,8 @@ async def inspect_server(queue: Queue, addr: tuple[str, int]):
|
|||
continue
|
||||
case Packet33RelEntityMoveLook.packet_id:
|
||||
continue
|
||||
case Packet73WeatherStatus.packet_id:
|
||||
continue
|
||||
# case Packet73WeatherStatus.packet_id:
|
||||
# continue
|
||||
case Packet52MultiBlockChange.packet_id:
|
||||
continue
|
||||
case _:
|
||||
|
|
|
@ -6,6 +6,10 @@ from dataclasses import dataclass
|
|||
|
||||
from bta_proxy.itemstack import ItemStack
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
class DataItemType(Enum):
|
||||
BYTE = 0
|
||||
SHORT = 1
|
||||
|
@ -25,7 +29,8 @@ class EntityData:
|
|||
@classmethod
|
||||
async def read_from(cls, dis: AsyncDataInputStream) -> list[DataItem]:
|
||||
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_id: int = data & 0x1F
|
||||
match item_type:
|
||||
|
@ -49,7 +54,7 @@ class EntityData:
|
|||
@classmethod
|
||||
def read_from_sync(cls, dis: SyncDataInputStream) -> list[DataItem]:
|
||||
items = []
|
||||
while (data := dis.read()) != 0x7F:
|
||||
while (data := dis.read()) not in (127, 255):
|
||||
item_type = DataItemType((data & 0xE0) >> 5)
|
||||
item_id: int = data & 0x1F
|
||||
match item_type:
|
||||
|
|
|
@ -80,3 +80,4 @@ from .packet108sethotbaroffset import Packet108SetHotbarOffset
|
|||
from .packet5playerinventory import Packet5PlayerInventory
|
||||
from .packet5playerinventory import Packet5PlayerInventory
|
||||
from .packet5playerinventory import Packet5PlayerInventory
|
||||
from .packet143photomode import Packet143PhotoMode
|
||||
|
|
|
@ -4,7 +4,9 @@ import gzip
|
|||
from bta_proxy.entitydata import EntityData
|
||||
from bta_proxy.itemstack import ItemStack
|
||||
from ..datainputstream import AsyncDataInputStream
|
||||
from logging import getLogger
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
def try_int(v: str) -> Union[str, int]:
|
||||
try:
|
||||
|
@ -24,6 +26,7 @@ class Packet:
|
|||
|
||||
@classmethod
|
||||
async def read_data_from(cls, stream: AsyncDataInputStream) -> "Packet":
|
||||
logger.debug("Packet.read_data_from(%r)", stream)
|
||||
fields: dict = {}
|
||||
for key, datatype in cls.FIELDS:
|
||||
if "?" in key:
|
||||
|
@ -35,6 +38,7 @@ class Packet:
|
|||
elif not fields[cond]:
|
||||
continue
|
||||
try:
|
||||
logger.debug(f"reading {key=} of type {datatype!r} ({fields=})")
|
||||
fields[key] = await cls.read_field(stream, datatype, fields)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed getting key {key} on {cls}") from e
|
||||
|
@ -46,14 +50,21 @@ class Packet:
|
|||
datatype: Any,
|
||||
fields: dict[str, Any] = {},
|
||||
):
|
||||
logger.debug(f"Packet.read_field(_, {datatype=}, {fields=})")
|
||||
match datatype:
|
||||
case "list", sizekey, *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 [
|
||||
await Packet.read_field(stream, args, fields)
|
||||
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":
|
||||
return await stream.read_uint()
|
||||
case "int":
|
||||
|
@ -156,6 +167,7 @@ class Packet:
|
|||
raise ValueError(f"unknown type {datatype}")
|
||||
|
||||
def __init_subclass__(cls, packet_id: int, **kwargs) -> None:
|
||||
logger.debug(f"registered packet {cls} with id = {packet_id}")
|
||||
Packet.REGISTRY[packet_id] = cls
|
||||
cls.packet_id = packet_id
|
||||
super().__init_subclass__(**kwargs)
|
||||
|
@ -166,13 +178,15 @@ class Packet:
|
|||
@classmethod
|
||||
async def read_packet(cls, stream: AsyncDataInputStream) -> "Packet":
|
||||
packet_id: int = await stream.read()
|
||||
logger.debug(f"incoming {packet_id=}")
|
||||
if packet_id not in cls.REGISTRY:
|
||||
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.packet_id = packet_id
|
||||
pkt.post_creation()
|
||||
logger.debug(f"received {pkt}")
|
||||
return pkt
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -181,7 +195,8 @@ class Packet:
|
|||
for key, _ in self.FIELDS:
|
||||
if "?" in key:
|
||||
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:
|
||||
fields.append(f"{key}={getattr(self, key)!r}")
|
||||
return f'<{pkt_name} {str.join(", ", fields)}>'
|
||||
|
||||
|
|
|
@ -2,6 +2,6 @@ from .base import Packet
|
|||
|
||||
class Packet138PlayerList(Packet, packet_id=138):
|
||||
FIELDS = [
|
||||
('players', 'str'),
|
||||
('scores', 'str'),
|
||||
('n_players', 'int'),
|
||||
('players', ('list', 'n_players', 'tuple', 'str', 'int')),
|
||||
]
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
from .base import Packet
|
||||
|
||||
class Packet141UpdateFlag(Packet, packet_id=141):
|
||||
__slots__ = ('x', 'y', 'z', 'colors', 'items')
|
||||
__slots__ = ('x', 'y', 'z', 'colors')
|
||||
FIELDS = [
|
||||
('x', 'int'),
|
||||
('y', 'short'),
|
||||
('z', 'int'),
|
||||
('colors', ('bytes', 384)),
|
||||
('items', ('list', 3, 'nbt')),
|
||||
('owner', 'string')
|
||||
]
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
from .base import Packet
|
||||
|
||||
class Packet143PhotoMode(Packet, packet_id=143):
|
||||
__slots__ = ('disabled',)
|
||||
FIELDS = [
|
||||
('disabled', 'bool'),
|
||||
]
|
|
@ -11,5 +11,5 @@ class Packet24MobSpawn(Packet, packet_id=24):
|
|||
('pitch', 'byte'),
|
||||
('metadata', 'entitydata'),
|
||||
('nickname', 'string'),
|
||||
('chatcolor', 'byte'),
|
||||
('chatcolor', 'ubyte'),
|
||||
]
|
||||
|
|
|
@ -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])
|
Loading…
Reference in New Issue