Support parsing compressed packets
This commit is contained in:
parent
f2ab08bfa6
commit
9d2524c199
|
@ -23,52 +23,91 @@ def test_parse_7_real_map_change():
|
|||
assert len(packet.messages) == 1
|
||||
assert packet.messages[0].message_name == 'map_change'
|
||||
|
||||
# Teeworlds 0.7 Protocol packet
|
||||
# Flags: none (..00 00..)
|
||||
# ..0. .... = Connection-oriented
|
||||
# ...0 .... = Not compressed
|
||||
# .... 0... = No resend requested
|
||||
# .... .0.. = Not a control message
|
||||
# Acknowledged sequence number: 1 (.... ..00 0000 0001)
|
||||
# Number of chunks: 1
|
||||
# Token: 58eb9af4
|
||||
# Payload (59 bytes)
|
||||
# Teeworlds 0.7 Protocol chunk: sys.map_change
|
||||
# Header (vital: 1)
|
||||
# Flags: vital (01.. ....)
|
||||
# Size: 56 bytes (..00 0000 ..11 1000)
|
||||
# Sequence number: 1 (00.. .... 0000 0001)
|
||||
# Message: sys.map_change
|
||||
# Name: "BlmapChill"
|
||||
# Crc: -1592087519
|
||||
# Size: 1134475
|
||||
# Num response chunks per request: 8
|
||||
# Chunk size: 1384
|
||||
# Sha256: 817dbf48c5f19437c4582c6f98c9c204c1f1697632f04458745455898400fb28
|
||||
# Teeworlds 0.7 Protocol packet
|
||||
# Flags: none (..00 00..)
|
||||
# ..0. .... = Connection-oriented
|
||||
# ...0 .... = Not compressed
|
||||
# .... 0... = No resend requested
|
||||
# .... .0.. = Not a control message
|
||||
# Acknowledged sequence number: 1 (.... ..00 0000 0001)
|
||||
# Number of chunks: 1
|
||||
# Token: 58eb9af4
|
||||
# Payload (59 bytes)
|
||||
# Teeworlds 0.7 Protocol chunk: sys.map_change
|
||||
# Header (vital: 1)
|
||||
# Flags: vital (01.. ....)
|
||||
# Size: 56 bytes (..00 0000 ..11 1000)
|
||||
# Sequence number: 1 (00.. .... 0000 0001)
|
||||
# Message: sys.map_change
|
||||
# Name: "BlmapChill"
|
||||
# Crc: -1592087519
|
||||
# Size: 1134475
|
||||
# Num response chunks per request: 8
|
||||
# Chunk size: 1384
|
||||
# Sha256: 817dbf48c5f19437c4582c6f98c9c204c1f1697632f04458745455898400fb28
|
||||
|
||||
|
||||
# def test_parse_7_real_multi_chunk_compressed():
|
||||
# # 0.7 motd, srv settings, ready
|
||||
# packet = parse7(b'\x10\x02\x03\x58\xeb\x9a\xf4\x4a\x42\x88\x4a\x6e\x16\xba\x31\x46\xa2\x84\x9e\xbf\xe2\x06')
|
||||
# # ^ ^ ^ ^ ^ ^ ^
|
||||
# # |ack=2 | \_____________/ \_________________________________________________________/
|
||||
# # | | | |
|
||||
# # | chunks=3 token huffman compressed
|
||||
# # | 3 chunks:
|
||||
# # compression=true game.sv_motd, game.sv_server_settings, sys.con_ready
|
||||
# assert packet.version == '0.7'
|
||||
#
|
||||
# assert packet.header.token == b'\x58\xeb\x9a\xf4'
|
||||
#
|
||||
# assert packet.header.num_chunks == 3
|
||||
# assert packet.header.ack == 2
|
||||
#
|
||||
# assert packet.header.flags.compression == True
|
||||
# assert packet.header.flags.control == False
|
||||
#
|
||||
# # TODO: uncomment
|
||||
# # assert len(packet.messages) == 3
|
||||
# # assert packet.messages[0].message_name == 'sv_motd'
|
||||
# # assert packet.messages[1].message_name == 'sv_server_settings'
|
||||
# # assert packet.messages[2].message_name == 'con_ready'
|
||||
def test_parse_7_real_multi_chunk_compressed():
|
||||
# 0.7 motd, srv settings, ready
|
||||
packet = parse7(b'\x10\x02\x03\x58\xeb\x9a\xf4\x4a\x42\x88\x4a\x6e\x16\xba\x31\x46\xa2\x84\x9e\xbf\xe2\x06')
|
||||
# ^ ^ ^ ^ ^ ^ ^
|
||||
# |ack=2 | \_____________/ \_________________________________________________________/
|
||||
# | | | |
|
||||
# | chunks=3 token huffman compressed
|
||||
# | 3 chunks:
|
||||
# compression=true game.sv_motd, game.sv_server_settings, sys.con_ready
|
||||
#
|
||||
# payload should decompress
|
||||
# from: b'\x4a\x42\x88\x4a\x6e\x16\xba\x31\x46\xa2\x84\x9e\xbf\xe2\x06'
|
||||
# to: b'\x40\x02\x02\x02\x00\x40\x07\x03\x22\x01\x00\x01\x00\x01\x08\x40\x01\x04\x0b'
|
||||
# ^ ^ ^ ^ ^ ^
|
||||
# \_________________/ \_____________________________________/ \_____________/
|
||||
# | | |
|
||||
# motd server_settings ready
|
||||
|
||||
assert packet.payload_raw == b'\x4a\x42\x88\x4a\x6e\x16\xba\x31\x46\xa2\x84\x9e\xbf\xe2\x06'
|
||||
assert packet.payload_decompressed == b'\x40\x02\x02\x02\x00\x40\x07\x03\x22\x01\x00\x01\x00\x01\x08\x40\x01\x04\x0b'
|
||||
|
||||
# Teeworlds 0.7 Protocol chunk: game.sv_motd
|
||||
# Header (vital: 2)
|
||||
# Flags: vital (01.. ....)
|
||||
# Size: 2 bytes (..00 0000 ..00 0010)
|
||||
# Sequence number: 2 (00.. .... 0000 0010)
|
||||
# Message: game.sv_motd
|
||||
# Message: ""
|
||||
# Teeworlds 0.7 Protocol chunk: game.sv_server_settings
|
||||
# Header (vital: 3)
|
||||
# Flags: vital (01.. ....)
|
||||
# Size: 7 bytes (..00 0000 ..00 0111)
|
||||
# Sequence number: 3 (00.. .... 0000 0011)
|
||||
# Message: game.sv_server_settings
|
||||
# Kick vote: true
|
||||
# Kick min: 0
|
||||
# Spec vote: true
|
||||
# Team lock: false
|
||||
# Team balance: true
|
||||
# Player slots: 8
|
||||
# Teeworlds 0.7 Protocol chunk: sys.con_ready
|
||||
# Header (vital: 4)
|
||||
# Flags: vital (01.. ....)
|
||||
# Size: 1 byte (..00 0000 ..00 0001)
|
||||
# Sequence number: 4 (00.. .... 0000 0100)
|
||||
# Message: sys.con_ready
|
||||
|
||||
|
||||
|
||||
assert packet.version == '0.7'
|
||||
|
||||
assert packet.header.token == b'\x58\xeb\x9a\xf4'
|
||||
|
||||
assert packet.header.num_chunks == 3
|
||||
assert packet.header.ack == 2
|
||||
|
||||
assert packet.header.flags.compression == True
|
||||
assert packet.header.flags.control == False
|
||||
|
||||
assert len(packet.messages) == 3
|
||||
assert packet.messages[0].message_name == 'sv_motd'
|
||||
assert packet.messages[1].message_name == 'sv_server_settings'
|
||||
assert packet.messages[2].message_name == 'con_ready'
|
||||
|
||||
|
|
|
@ -3,6 +3,18 @@ from typing import cast
|
|||
import twnet_parser.msg7
|
||||
import twnet_parser.messages7.system.map_change
|
||||
from twnet_parser.net_message import NetMessage
|
||||
from twnet_parser.chunk_header import ChunkHeader
|
||||
|
||||
# TODO: remove when msg class generation is done
|
||||
# this is just a placeholder to scaffold code
|
||||
class TodoMessage():
|
||||
def __init__(self, name: str) -> None:
|
||||
self.message_name = name
|
||||
self.header: ChunkHeader
|
||||
def unpack(self, data: bytes) -> bool:
|
||||
return len(data) > 0
|
||||
def pack(self) -> bytes:
|
||||
return b'\x00'
|
||||
|
||||
# could also be named ChunkParser
|
||||
class MessageParser():
|
||||
|
@ -11,11 +23,17 @@ class MessageParser():
|
|||
# NOT the whole packet with packet header
|
||||
# and NOT the whole message with chunk header
|
||||
def parse_game_message(self, msg_id: int, data: bytes) -> NetMessage:
|
||||
if msg_id == twnet_parser.msg7.SV_MOTD:
|
||||
return TodoMessage('sv_motd')
|
||||
if msg_id == twnet_parser.msg7.SV_SERVERSETTINGS:
|
||||
return TodoMessage('sv_server_settings')
|
||||
raise ValueError(f"Error: unknown message game.id={msg_id} data={data[0]}")
|
||||
def parse_sys_message(self, msg_id: int, data: bytes) -> NetMessage:
|
||||
if msg_id == twnet_parser.msg7.MAP_CHANGE:
|
||||
msg = twnet_parser.messages7.system.map_change.MsgMapChange()
|
||||
msg.unpack(data)
|
||||
return cast(NetMessage, msg)
|
||||
if msg_id == twnet_parser.msg7.CON_READY:
|
||||
return TodoMessage('con_ready')
|
||||
raise ValueError(f"Error: unknown message sys.id={msg_id} data={data[0]}")
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# system
|
||||
NULL = 0
|
||||
INFO = 1
|
||||
MAP_CHANGE = 2 # sent when client should switch map
|
||||
|
@ -15,3 +16,45 @@ RCON_LINE = 13 # line that should be printed to the remote console
|
|||
RCON_CMD_ADD = 14
|
||||
RCON_CMD_REM = 15
|
||||
|
||||
# game
|
||||
INVALID = 0
|
||||
SV_MOTD = 1
|
||||
SV_BROADCAST = 2
|
||||
SV_CHAT = 3
|
||||
SV_TEAM = 4
|
||||
SV_KILLMSG = 5
|
||||
SV_TUNEPARAMS = 6
|
||||
SV_EXTRAPROJECTILE = 7
|
||||
SV_READYTOENTER = 8
|
||||
SV_WEAPONPICKUP = 19
|
||||
SV_EMOTICON = 10
|
||||
SV_VOTECLEAROPTIONS = 11
|
||||
SV_VOTEOPTIONLISTADD = 12
|
||||
SV_VOTEOPTIONADD = 13
|
||||
SV_VOTEOPTIONREMOVE = 14
|
||||
SV_VOTESET = 15
|
||||
SV_VOTESTATUS = 16
|
||||
SV_SERVERSETTINGS = 17
|
||||
SV_CLIENTINFO = 18
|
||||
SV_GAMEINFO = 19
|
||||
SV_CLIENTDROP = 20
|
||||
SV_GAMEMSG = 21
|
||||
DE_CLIENTENTER = 22
|
||||
DE_CLIENTLEAVE = 23
|
||||
CL_SAY = 24
|
||||
CL_SETTEAM = 25
|
||||
CL_SETSPECTATORMODE = 26
|
||||
CL_STARTINFO = 27
|
||||
CL_KILL = 28
|
||||
CL_READYCHANGE = 29
|
||||
CL_EMOTICON = 30
|
||||
CL_VOTE = 31
|
||||
CL_CALLVOTE = 32
|
||||
SV_SKINCHANGE = 33
|
||||
CL_SKINCHANGE = 34
|
||||
SV_RACEFINISH = 35
|
||||
SV_CHECKPOINT = 36
|
||||
SV_COMMANDINFO = 37
|
||||
SV_COMMANDINFOREMOVE = 38
|
||||
CL_COMMAND = 39
|
||||
NUM_GAMEMESSAGES = 40
|
||||
|
|
|
@ -9,6 +9,8 @@ from twnet_parser.message_parser import MessageParser
|
|||
from twnet_parser.net_message import NetMessage
|
||||
from twnet_parser.chunk_header import ChunkHeader, ChunkFlags
|
||||
|
||||
from twnet_parser.external.huffman import huffman
|
||||
|
||||
# TODO: what is a nice pythonic way of storing those?
|
||||
# also does some version:: namespace thing make sense?
|
||||
PACKETFLAG7_CONTROL = 1
|
||||
|
@ -51,6 +53,8 @@ class PacketHeader(PrettyPrint):
|
|||
class TwPacket(PrettyPrint):
|
||||
def __init__(self) -> None:
|
||||
self.version: str = 'unknown'
|
||||
self.payload_raw: bytes = b''
|
||||
self.payload_decompressed: bytes = b''
|
||||
self.header: PacketHeader = PacketHeader()
|
||||
self.messages: list[Union[CtrlMessage, NetMessage]] = []
|
||||
|
||||
|
@ -152,15 +156,20 @@ class PacketParser():
|
|||
# methods that do not share state seems like a waste of performance
|
||||
# would this be nicer with class methods?
|
||||
pck.header = PacketHeaderParser7().parse_header(data)
|
||||
pck.payload_raw = data[PACKET_HEADER7_SIZE:]
|
||||
pck.payload_decompressed = pck.payload_raw
|
||||
if pck.header.flags.control:
|
||||
if data[7] == 0x04: # close
|
||||
msg_dc = CtrlMessage('close')
|
||||
pck.messages.append(msg_dc)
|
||||
return pck
|
||||
else:
|
||||
if pck.header.flags.compression:
|
||||
payload = bytearray(pck.payload_raw)
|
||||
pck.payload_decompressed = huffman.decompress(payload)
|
||||
pck.messages = cast(
|
||||
list[Union[CtrlMessage, NetMessage]],
|
||||
self.get_messages(data[PACKET_HEADER7_SIZE:]))
|
||||
self.get_messages(pck.payload_decompressed))
|
||||
return pck
|
||||
|
||||
def parse6(data: bytes) -> TwPacket:
|
||||
|
|
Loading…
Reference in a new issue