msg_codec.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. #!/usr/bin/env python3
  2. """
  3. 996传奇客户端 协议编解码 统一 API
  4. 来源: cpp_reversed/ 目录下各模块汇总
  5. - hdr_codec: 24B 头编解码 (Encode6BitBuf 算法核心)
  6. - frame: 36B 头 + body 完整帧装拆
  7. - xxtea: 脚本密文加解密 (LuaBridgeCtl::SetXXTEAKeyGM 配套)
  8. - rsa: 客户端本地 RSA 密钥对 (RsaEncryptPub / RsaDecryptPri)
  9. 定位: 这是 cpp_reversed/ 体系的"使用层入口", 所有"我想发一条消息" / "我想解一条抓包"
  10. 的操作都从这里走。底层模块仍可单独 import 使用。
  11. 依赖: 同目录的 hdr_codec.py / frame.py / xxtea.py / rsa.py
  12. 跨目录 (protocol_bot/) 仅在 self-test 交叉验证时使用
  13. """
  14. from __future__ import annotations
  15. import os
  16. import sys
  17. from typing import Optional, Dict # noqa: F401
  18. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
  19. from hdr_codec import ( # type: ignore
  20. ENC_TABLE,
  21. encode_header, decode_header, parse_header,
  22. )
  23. from frame import ( # type: ignore
  24. Frame, HDR_STRUCT, HEADER_PLAIN_SIZE, HEADER_ENC_SIZE,
  25. pack_frame, unpack_stream,
  26. MSG_CS_LOGIN, MSG_CS_CREATE_ROLE, MSG_CS_SELECT_ROLE,
  27. MSG_SC_ROLE_LIST, MSG_SC_CREATE_ROLE_OK, MSG_SC_CREATE_ROLE_FAIL,
  28. MSG_SC_GAME_SERVER_ADDR, MSG_CS_HEARTBEAT,
  29. MSG_SC_DISCONNECTED, MSG_ILLEGAL, MSG_BATCH,
  30. )
  31. from xxtea import ( # type: ignore
  32. encrypt as xxtea_encrypt, decrypt as xxtea_decrypt,
  33. encrypt_padded, decrypt_padded,
  34. SCRIPT_KEY_RAW, SCRIPT_SIGN,
  35. )
  36. from rsa import encrypt_pub, decrypt_pri, PUB_KEY_N, PUB_KEY_E, PRI_KEY_D # type: ignore
  37. __all__ = [
  38. # 头编解码 (24B 内部表示)
  39. "encode_header", "decode_header", "parse_header", "ENC_TABLE",
  40. # 完整帧
  41. "Frame", "pack_frame", "unpack_stream", "send", "receive",
  42. # XXTEA
  43. "xxtea_encrypt", "xxtea_decrypt", "encrypt_padded", "decrypt_padded",
  44. "SCRIPT_KEY_RAW", "SCRIPT_SIGN",
  45. # RSA
  46. "encrypt_pub", "decrypt_pri",
  47. # 常用消息号
  48. "MSG_CS_LOGIN", "MSG_CS_CREATE_ROLE", "MSG_CS_SELECT_ROLE",
  49. "MSG_SC_ROLE_LIST", "MSG_SC_CREATE_ROLE_OK", "MSG_SC_CREATE_ROLE_FAIL",
  50. "MSG_SC_GAME_SERVER_ADDR", "MSG_CS_HEARTBEAT",
  51. "MSG_SC_DISCONNECTED", "MSG_ILLEGAL", "MSG_BATCH",
  52. # 头结构常量
  53. "HDR_STRUCT", "HEADER_PLAIN_SIZE", "HEADER_ENC_SIZE",
  54. ]
  55. # ============== 高层 API ==============
  56. def send(msg_id: int, recog: int, p1: int, p2: int, p3: int,
  57. body: bytes = b"", tail: int = 0) -> bytes:
  58. """
  59. 构造一条要发送的线协议消息
  60. :return: 48B 编码头 + body, 可直接 sendall
  61. """
  62. return pack_frame(msg_id, recog, p1, p2, p3, tail, body)
  63. def receive(buffer: bytes) -> tuple[list[Frame], bytes]:
  64. """
  65. 从累积的字节流中切出所有完整帧
  66. :return: (frames, rest) — frames 是完整帧, rest 是不足一帧的尾部残留 (供下次 recv 拼)
  67. """
  68. return unpack_stream(buffer)
  69. def frame_to_dict(f: Frame) -> dict:
  70. """Frame → 人类可读的 dict (供日志/调试)"""
  71. return {
  72. "msg_id": f.msg_id,
  73. "recog": f.recog,
  74. "p1": f.p1,
  75. "p2": f.p2,
  76. "p3": f.p3,
  77. "tail": f.tail,
  78. "body_len": len(f.body),
  79. "body_preview": f.body[:32].hex() + ("..." if len(f.body) > 32 else ""),
  80. }
  81. def decode_captured(enc_bytes: bytes) -> dict:
  82. """
  83. 抓包分析: 编码字节 → 明文头 dict (供 Wireshark 抓 TCP 流后 1-行解析)
  84. 接受两种输入:
  85. - 32B (旧 24B 头, 用 hdr_codec 解析)
  86. - 48B (新 36B 头, 用 frame 解析, 包含 body_len / tail)
  87. 自动识别: 长度 >= 48 → 走 wire 格式; 否则按 hdr_codec 24B 格式
  88. """
  89. if len(enc_bytes) >= HEADER_ENC_SIZE:
  90. # wire 格式 36B 头: 0, msg_id, recog, p1, p2, p3, 0, body_len, tail
  91. plain36 = _decode_full_to_36(enc_bytes[:HEADER_ENC_SIZE])
  92. zero, msg_id, recog, p1, p2, p3, zero2, body_len, tail = HDR_STRUCT.unpack(plain36)
  93. return {
  94. "msgId": msg_id, "recog": recog,
  95. "p1": p1, "p2": p2, "p3": p3,
  96. "body_len": body_len, "tail": tail,
  97. }
  98. elif len(enc_bytes) >= 32:
  99. # 24B 旧格式 (hdr_codec)
  100. plain24 = decode_header(bytes(enc_bytes[:32]))
  101. return parse_header(plain24)
  102. else:
  103. raise ValueError(f"至少需要 32B 编码头, 实际={len(enc_bytes)}")
  104. def _decode_full_to_36(enc48: bytes) -> bytes:
  105. """48B 编码头 → 36B 明文头 (与 frame._decode_full 相同, 此处独立以避免循环引用)"""
  106. from frame import _decode_full
  107. return _decode_full(enc48, HEADER_PLAIN_SIZE)
  108. # ============== 反汇编地址速查 (供"我想精读某函数实现"用) ==============
  109. ADDR = {
  110. "encode_header": "0x10004b4e4", # 头编码 (24B→32B)
  111. "decode_header": "0x10004b594", # 头解码 (32B→24B)
  112. "SendNetMessage": "0x10003d408", # 加密+入队
  113. "LuaNetworkController_SendMsg": "0x10005f8cc",
  114. "tolua_SendMsg_binding": "0x10007cd54",
  115. "netClient_recv_loop": "0x100023df8",
  116. "netClient_recv_dispatch": "0x10003cd30",
  117. "LuaBridgeCtl_SetXXTEAKeyGM": "0x100044b88",
  118. "init_keys_and_sign": "0x100043bdc",
  119. "xor_deobfuscate": "0x100043b34",
  120. }
  121. if __name__ == "__main__":
  122. import sys
  123. import os
  124. _PARENT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  125. if _PARENT not in sys.path:
  126. sys.path.insert(0, _PARENT)
  127. from protocol_bot.encode6bitbuf_final import build_frame, encode6
  128. from frame import HDR_STRUCT
  129. print("msg_codec.py — 996传奇客户端 协议编解码 统一 API")
  130. print("=" * 60)
  131. print()
  132. # 测试 1: send → receive roundtrip
  133. body = b'{"chr":"\xe8\x90\x8c\xe6\x96\xb0","job":0,"sex":1}'
  134. sent = send(MSG_CS_CREATE_ROLE, 900000, 12345, 37504, 0, body)
  135. print(f"[1] send(): msg_id={MSG_CS_CREATE_ROLE}, body={len(body)}B")
  136. print(f" wire: {len(sent)}B = 48 header + {len(body)} body")
  137. print(f" hex: {sent.hex()}")
  138. print(f" ascii: {''.join(chr(b) if 0x3C<=b<=0x7B else '.' for b in sent[:48])}")
  139. print()
  140. frames, rest = receive(sent)
  141. assert rest == b""
  142. assert len(frames) == 1
  143. f = frames[0]
  144. print(f"[2] receive(): {len(frames)} frame, rest={rest!r}")
  145. print(f" decoded: {frame_to_dict(f)}")
  146. assert f.body == body
  147. print()
  148. # 测试 3: 编码头与 protocol_bot 字节级一致
  149. sent_no_body = send(MSG_CS_CREATE_ROLE, 900000, 12345, 37504, 0, b"")
  150. other = build_frame(MSG_CS_CREATE_ROLE, 900000, 12345, 37504, 0)
  151. assert sent_no_body == other, "与 protocol_bot 不一致"
  152. print(f"[3] wire format 与 protocol_bot 字节级一致 ✓")
  153. print()
  154. # 测试 4: parse_header 抓包分析
  155. enc = sent[:48] # 前 48B 编码头 (wire 格式)
  156. fields = decode_captured(enc)
  157. print(f"[4] decode_captured(48B wire): {fields}")
  158. assert fields["msgId"] == MSG_CS_CREATE_ROLE
  159. assert fields["recog"] == 900000
  160. assert fields["p1"] == 12345
  161. print()
  162. # 测试 4b: decode_captured 也支持 24B 旧格式
  163. legacy24 = encode_header(b"\x00" * 24) # 占位
  164. legacy_dict = decode_captured(legacy24)
  165. print(f"[4b] decode_captured(32B legacy): keys={sorted(legacy_dict.keys())} ✓")
  166. print()
  167. # 测试 5: XXTEA 自洽 (脚本密文场景)
  168. from xxtea import encrypt as xe, decrypt as xd
  169. test_key = b"0123456789abcdef"
  170. test_plain = b"Hello XXTEA!!! " # 16 字节 (8 倍数)
  171. enc = xe(test_plain, test_key)
  172. dec = xd(enc, test_key)
  173. assert dec == test_plain
  174. print(f"[5] XXTEA roundtrip: '{test_plain.decode()}' → {enc.hex()} → '{dec.decode()}' ✓")
  175. print()
  176. # 测试 6: 多包粘包
  177. f1 = send(MSG_CS_LOGIN, 0, 0, 0, 0, b"*#acct/role/12345/9/1/0", 9)
  178. f2 = send(MSG_SC_ROLE_LIST, 0, 0, 0, 0, b"key32/session123", 0)
  179. multi = f1 + f2
  180. frames, rest = receive(multi)
  181. assert rest == b""
  182. assert len(frames) == 2
  183. assert frames[0].msg_id == MSG_CS_LOGIN
  184. assert frames[1].msg_id == MSG_SC_ROLE_LIST
  185. print(f"[6] 多包粘包: 2 帧 [{frames[0].msg_id}, {frames[1].msg_id}] ✓")
  186. print()
  187. # 测试 7: RSA 接口(应抛 NotImplementedError, 因密钥未提取)
  188. try:
  189. encrypt_pub(b"test")
  190. print("[7] FAIL: 应抛 NotImplementedError")
  191. except NotImplementedError as e:
  192. print(f"[7] RSA encrypt_pub 接口: NotImplementedError (密钥待提取) ✓")
  193. print()
  194. print("=" * 60)
  195. print("All msg_codec self-tests passed.")
  196. print()
  197. print("可用入口:")
  198. print(" send(msg_id, recog, p1, p2, p3, body) → 48B 头 + body")
  199. print(" receive(buffer) → (frames, rest)")
  200. print(" frame_to_dict(f) → 调试 dict")
  201. print(" decode_captured(32B 编码头) → 抓包解析 dict")