protocol.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. # -*- coding: utf-8 -*-
  2. """
  3. Trixy-Protokoll für die Netzwerkkommunikation.
  4. Binäres Protokoll mit:
  5. - Magic Number 'TRXI'
  6. - Versionierung
  7. - 32-bit Optionsflags
  8. - MD5 Prüfsumme
  9. - Serialisierte Daten
  10. """
  11. import hashlib
  12. import json
  13. import pickle
  14. import struct
  15. import base64
  16. from dataclasses import dataclass, field
  17. from datetime import datetime
  18. from enum import IntFlag
  19. from typing import Any, Type
  20. from trixy_core.utils.version import PROTOCOL_MAJOR, PROTOCOL_MINOR
  21. from trixy_core.utils.debug import pdebug, perror
  22. # Protokoll-Magic-Number
  23. MAGIC = b"TRXI"
  24. MAGIC_LENGTH = 4
  25. # Hard-coded Befehle für Effizienz
  26. COMMAND_NOOP = b"TRXINOOP" # Keine Operation
  27. COMMAND_PING = b"TRXIPING" # Ping-Anfrage
  28. COMMAND_PONG = b"TRXIPONG" # Ping-Antwort
  29. COMMAND_PRINT = b"TRXIPRNT" # Debug-Ausgabe
  30. COMMAND_HELLO = b"TRXYHELO" # Handshake
  31. HARD_COMMANDS = {COMMAND_NOOP, COMMAND_PING, COMMAND_PONG, COMMAND_PRINT, COMMAND_HELLO}
  32. class ProtocolFlags(IntFlag):
  33. """32-bit Protokoll-Optionen."""
  34. NONE = 0
  35. COMPRESSED = 1 << 0 # Daten sind komprimiert (zlib)
  36. ENCRYPTED = 1 << 1 # Daten sind verschlüsselt (AES-256-GCM)
  37. JSON = 1 << 2 # Daten sind JSON
  38. ACK_REQUIRED = 1 << 3 # Bestätigung erforderlich
  39. BASE64 = 1 << 4 # Daten sind Base64-kodiert
  40. MULTIPART = 1 << 5 # Teil einer Mehrteiligen Nachricht
  41. DICT = 1 << 6 # Daten sind ein Dictionary
  42. SILENT = 1 << 7 # Keine Logging-Ausgabe
  43. PICKLE = 1 << 8 # Daten sind Pickle-serialisiert
  44. RESPONSE = 1 << 9 # Ist eine Antwort auf eine Anfrage
  45. ERROR = 1 << 10 # Enthält Fehlerdaten
  46. BINARY = 1 << 11 # Rohe Binärdaten
  47. @dataclass
  48. class ProtocolMessage:
  49. """
  50. Repräsentiert eine Protokoll-Nachricht.
  51. Struktur:
  52. - Magic (4 bytes): 'TRXI'
  53. - Version (2 bytes): Major.Minor
  54. - Timestamp (8 bytes): Unix-Timestamp in Millisekunden
  55. - Flags (4 bytes): 32-bit Optionsflags
  56. - Checksum (16 bytes): MD5 der Daten
  57. - Class Name Length (2 bytes): Länge des Klassennamens
  58. - Class Name (variable): Name der Nachrichtenklasse
  59. - Data Length (4 bytes): Länge der Daten
  60. - Data (variable): Serialisierte Daten
  61. """
  62. class_name: str = ""
  63. data: Any = None
  64. flags: ProtocolFlags = ProtocolFlags.NONE
  65. timestamp: datetime = field(default_factory=datetime.now)
  66. checksum: bytes = b""
  67. raw_data: bytes = field(default=b"", repr=False)
  68. # Header-Größe ohne variable Teile
  69. HEADER_SIZE = MAGIC_LENGTH + 2 + 8 + 4 + 16 + 2
  70. class TrixyProtocol:
  71. """
  72. Protokoll-Handler für Serialisierung und Deserialisierung.
  73. """
  74. def __init__(self, encryption: Any = None) -> None:
  75. """
  76. Initialisiert das Protokoll.
  77. Args:
  78. encryption: Optionale TrixyEncryption-Instanz
  79. """
  80. self._encryption = encryption
  81. self._registered_classes: dict[str, Type] = {}
  82. def register_class(self, cls: Type) -> None:
  83. """
  84. Registriert eine Klasse für die Deserialisierung.
  85. Args:
  86. cls: Zu registrierende Klasse
  87. """
  88. self._registered_classes[cls.__name__] = cls
  89. def set_encryption(self, encryption: Any) -> None:
  90. """Setzt die Verschlüsselung."""
  91. self._encryption = encryption
  92. def serialize(
  93. self,
  94. message: Any,
  95. flags: ProtocolFlags = ProtocolFlags.NONE
  96. ) -> bytes:
  97. """
  98. Serialisiert eine Nachricht ins Protokoll-Format.
  99. Args:
  100. message: Zu serialisierende Nachricht
  101. flags: Protokoll-Flags
  102. Returns:
  103. Serialisierte Bytes
  104. """
  105. class_name = type(message).__name__
  106. timestamp = datetime.now()
  107. timestamp_ms = int(timestamp.timestamp() * 1000)
  108. # Daten serialisieren
  109. if flags & ProtocolFlags.JSON:
  110. if hasattr(message, "__dict__"):
  111. data = json.dumps(message.__dict__).encode("utf-8")
  112. else:
  113. data = json.dumps(message).encode("utf-8")
  114. elif flags & ProtocolFlags.PICKLE:
  115. data = pickle.dumps(message)
  116. elif flags & ProtocolFlags.BINARY:
  117. data = message if isinstance(message, bytes) else bytes(message)
  118. else:
  119. # Standard: Pickle
  120. flags |= ProtocolFlags.PICKLE
  121. data = pickle.dumps(message)
  122. # Base64-Kodierung
  123. if flags & ProtocolFlags.BASE64:
  124. data = base64.b64encode(data)
  125. # Verschlüsselung
  126. if flags & ProtocolFlags.ENCRYPTED:
  127. if self._encryption is None:
  128. perror("Verschlüsselung angefordert, aber nicht konfiguriert")
  129. flags &= ~ProtocolFlags.ENCRYPTED
  130. else:
  131. data = self._encryption.encrypt(data)
  132. # MD5 Prüfsumme
  133. checksum = hashlib.md5(data).digest()
  134. # Klassenname kodieren
  135. class_name_bytes = class_name.encode("utf-8")
  136. # Nachricht zusammenbauen
  137. header = struct.pack(
  138. ">4sBBQI16sH",
  139. MAGIC,
  140. PROTOCOL_MAJOR,
  141. PROTOCOL_MINOR,
  142. timestamp_ms,
  143. int(flags),
  144. checksum,
  145. len(class_name_bytes)
  146. )
  147. data_length = struct.pack(">I", len(data))
  148. return header + class_name_bytes + data_length + data
  149. def deserialize(self, raw_bytes: bytes) -> ProtocolMessage:
  150. """
  151. Deserialisiert Bytes in eine Protokoll-Nachricht.
  152. Args:
  153. raw_bytes: Rohe Bytes
  154. Returns:
  155. Deserialisierte Nachricht
  156. Raises:
  157. ValueError: Bei ungültigen Daten
  158. """
  159. # Prüfe zuerst auf Hard-coded Befehle (8 Bytes)
  160. # Diese haben eine andere Struktur als normale Nachrichten
  161. if len(raw_bytes) >= 8:
  162. cmd = raw_bytes[:8]
  163. if cmd in HARD_COMMANDS:
  164. return ProtocolMessage(
  165. class_name=cmd.decode("utf-8"),
  166. data=cmd,
  167. flags=ProtocolFlags.BINARY
  168. )
  169. # Für normale Nachrichten: Header-Größe prüfen
  170. if len(raw_bytes) < ProtocolMessage.HEADER_SIZE:
  171. raise ValueError("Nachricht zu kurz für Header")
  172. # Header parsen
  173. offset = 0
  174. magic = raw_bytes[offset:offset + MAGIC_LENGTH]
  175. offset += MAGIC_LENGTH
  176. if magic != MAGIC:
  177. raise ValueError(f"Ungültige Magic Number: {magic}")
  178. version_major, version_minor = struct.unpack(">BB", raw_bytes[offset:offset + 2])
  179. offset += 2
  180. timestamp_ms = struct.unpack(">Q", raw_bytes[offset:offset + 8])[0]
  181. offset += 8
  182. timestamp = datetime.fromtimestamp(timestamp_ms / 1000)
  183. flags = ProtocolFlags(struct.unpack(">I", raw_bytes[offset:offset + 4])[0])
  184. offset += 4
  185. checksum = raw_bytes[offset:offset + 16]
  186. offset += 16
  187. class_name_length = struct.unpack(">H", raw_bytes[offset:offset + 2])[0]
  188. offset += 2
  189. class_name = raw_bytes[offset:offset + class_name_length].decode("utf-8")
  190. offset += class_name_length
  191. data_length = struct.unpack(">I", raw_bytes[offset:offset + 4])[0]
  192. offset += 4
  193. data = raw_bytes[offset:offset + data_length]
  194. # Prüfsumme validieren
  195. calculated_checksum = hashlib.md5(data).digest()
  196. if checksum != calculated_checksum:
  197. perror("Prüfsummenfehler bei Protokoll-Nachricht")
  198. raise ValueError("Prüfsummenfehler")
  199. # Entschlüsselung
  200. if flags & ProtocolFlags.ENCRYPTED:
  201. if self._encryption is None:
  202. raise ValueError("Verschlüsselte Nachricht, aber keine Verschlüsselung konfiguriert")
  203. data = self._encryption.decrypt(data)
  204. # Base64-Dekodierung
  205. if flags & ProtocolFlags.BASE64:
  206. data = base64.b64decode(data)
  207. # Deserialisierung
  208. deserialized_data: Any = None
  209. if flags & ProtocolFlags.JSON:
  210. deserialized_data = json.loads(data.decode("utf-8"))
  211. elif flags & ProtocolFlags.PICKLE:
  212. deserialized_data = pickle.loads(data)
  213. elif flags & ProtocolFlags.BINARY:
  214. deserialized_data = data
  215. else:
  216. deserialized_data = data
  217. # Klasseninstanz erstellen wenn registriert
  218. if class_name in self._registered_classes and isinstance(deserialized_data, dict):
  219. cls = self._registered_classes[class_name]
  220. try:
  221. deserialized_data = cls(**deserialized_data)
  222. except Exception:
  223. pass # Behalte Dictionary
  224. return ProtocolMessage(
  225. class_name=class_name,
  226. data=deserialized_data,
  227. flags=flags,
  228. timestamp=timestamp,
  229. checksum=checksum,
  230. raw_data=raw_bytes
  231. )
  232. def create_ping(self) -> bytes:
  233. """Erstellt eine PING-Nachricht."""
  234. return COMMAND_PING
  235. def create_pong(self) -> bytes:
  236. """Erstellt eine PONG-Nachricht."""
  237. return COMMAND_PONG
  238. def create_noop(self) -> bytes:
  239. """Erstellt eine NOOP-Nachricht."""
  240. return COMMAND_NOOP
  241. def create_hello(self) -> bytes:
  242. """Erstellt eine HELLO-Nachricht."""
  243. return COMMAND_HELLO
  244. def is_hard_command(self, data: bytes) -> bool:
  245. """Prüft, ob die Daten ein Hard-coded Befehl sind."""
  246. return data[:8] in HARD_COMMANDS if len(data) >= 8 else False
  247. def get_hard_command(self, data: bytes) -> str | None:
  248. """Gibt den Hard-coded Befehl zurück oder None."""
  249. if len(data) >= 8:
  250. cmd = data[:8]
  251. if cmd in HARD_COMMANDS:
  252. return cmd.decode("utf-8")
  253. return None