protocol.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
  1. """
  2. Trixy Protocol Implementation
  3. This module implements the custom Trixy binary streaming protocol as specified
  4. in CLAUDE.md. It provides:
  5. - Binary message structure with magic number "TRXI"
  6. - Version fields, datetime, 32-bit option flags, MD5 checksum
  7. - Support for serialized Python classes and dictionaries
  8. - Compression, encryption, and encoding options
  9. - Hard-coded performance commands
  10. - Thread-safe serialization/deserialization
  11. Message Structure:
  12. [4 bytes] Magic Number: "TRXI"
  13. [4 bytes] Version Major (int)
  14. [4 bytes] Version Minor (int)
  15. [4 bytes] Version Revision (int)
  16. [8 bytes] DateTime (binary)
  17. [4 bytes] Options (32-bit flags)
  18. [16 bytes] MD5 Checksum
  19. [4 bytes] Class Name Length
  20. [variable] Class Name
  21. [4 bytes] Serialized Data Length
  22. [variable] Serialized Class Instance
  23. Option Flags (32-bit):
  24. Bit 1: GZ compressed
  25. Bit 2: Encrypted
  26. Bit 3: JSON format (else binary)
  27. Bit 4: Requires "received" response
  28. Bit 5: Base64 encoded
  29. Bit 6: Multi-part message
  30. Bit 7: Dictionary instead of class
  31. Bit 8: Silent (no debug logging)
  32. """
  33. import struct
  34. import time
  35. import hashlib
  36. import gzip
  37. import json
  38. import pickle
  39. import base64
  40. import threading
  41. from typing import Union, Dict, Any, Optional, Tuple, Type
  42. from dataclasses import dataclass
  43. from enum import IntFlag, Enum
  44. from datetime import datetime
  45. import io
  46. # Import encryption if available
  47. try:
  48. from cryptography.fernet import Fernet
  49. ENCRYPTION_AVAILABLE = True
  50. except ImportError:
  51. ENCRYPTION_AVAILABLE = False
  52. def pprint(message: str) -> None:
  53. """Protocol logging function."""
  54. print(f"[PROTOCOL] {message}")
  55. # Protocol constants
  56. MAGIC_NUMBER = b"TRXI"
  57. HEADER_SIZE = 48 # Fixed header size in bytes
  58. class MessageFlags(IntFlag):
  59. """32-bit option flags for Trixy protocol messages."""
  60. NONE = 0
  61. GZ_COMPRESSED = 1 << 0 # Bit 1: GZ compressed
  62. ENCRYPTED = 1 << 1 # Bit 2: Encrypted
  63. JSON_FORMAT = 1 << 2 # Bit 3: JSON format (else binary)
  64. REQUIRES_ACK = 1 << 3 # Bit 4: Requires "received" response
  65. BASE64_ENCODED = 1 << 4 # Bit 5: Base64 encoded
  66. MULTI_PART = 1 << 5 # Bit 6: Multi-part message
  67. DICTIONARY = 1 << 6 # Bit 7: Dictionary instead of class
  68. SILENT = 1 << 7 # Bit 8: Silent (no debug logging)
  69. @dataclass
  70. class ProtocolVersion:
  71. """Protocol version information."""
  72. major: int
  73. minor: int
  74. revision: int
  75. def __str__(self) -> str:
  76. return f"{self.major}.{self.minor}.{self.revision}"
  77. def to_bytes(self) -> bytes:
  78. """Convert version to bytes for protocol header."""
  79. return struct.pack(">III", self.major, self.minor, self.revision)
  80. @classmethod
  81. def from_bytes(cls, data: bytes) -> 'ProtocolVersion':
  82. """Create version from bytes."""
  83. major, minor, revision = struct.unpack(">III", data)
  84. return cls(major, minor, revision)
  85. @dataclass
  86. class MessageOptions:
  87. """Message options and metadata."""
  88. flags: MessageFlags = MessageFlags.NONE
  89. timestamp: Optional[datetime] = None
  90. encryption_key: Optional[bytes] = None
  91. part_number: int = 0
  92. total_parts: int = 1
  93. def __post_init__(self):
  94. if self.timestamp is None:
  95. self.timestamp = datetime.now()
  96. class ProtocolError(Exception):
  97. """Base exception for protocol errors."""
  98. pass
  99. class InvalidMessageError(ProtocolError):
  100. """Raised when message format is invalid."""
  101. pass
  102. class ChecksumMismatchError(ProtocolError):
  103. """Raised when MD5 checksum doesn't match."""
  104. pass
  105. class UnsupportedVersionError(ProtocolError):
  106. """Raised when protocol version is unsupported."""
  107. pass
  108. class CompressionError(ProtocolError):
  109. """Raised when compression/decompression fails."""
  110. pass
  111. class EncryptionError(ProtocolError):
  112. """Raised when encryption/decryption fails."""
  113. pass
  114. class TrixyMessage:
  115. """
  116. Represents a complete Trixy protocol message.
  117. This class handles the binary message format including all headers,
  118. options, and payload data according to the protocol specification.
  119. """
  120. def __init__(
  121. self,
  122. class_name: str,
  123. data: Any,
  124. version: Optional[ProtocolVersion] = None,
  125. options: Optional[MessageOptions] = None
  126. ):
  127. """
  128. Initialize a Trixy message.
  129. Args:
  130. class_name: Name of the command class or "HARDCODED" for hard-coded commands
  131. data: The data to serialize (class instance, dict, or string)
  132. version: Protocol version (uses current if None)
  133. options: Message options (uses defaults if None)
  134. """
  135. self.class_name = class_name
  136. self.data = data
  137. self.version = version or ProtocolVersion(1, 0, 0)
  138. self.options = options or MessageOptions()
  139. # Calculated fields
  140. self.checksum: Optional[bytes] = None
  141. self.serialized_data: Optional[bytes] = None
  142. self._lock = threading.RLock()
  143. def serialize(self) -> bytes:
  144. """
  145. Serialize the message to binary format according to protocol specification.
  146. Returns:
  147. bytes: Complete binary message
  148. Raises:
  149. ProtocolError: If serialization fails
  150. """
  151. with self._lock:
  152. try:
  153. # Serialize the payload data
  154. if self.class_name == "HARDCODED":
  155. # Hard-coded commands are sent as UTF-8 strings
  156. payload_data = str(self.data).encode('utf-8')
  157. elif self.options.flags & MessageFlags.DICTIONARY:
  158. # Serialize as dictionary
  159. if self.options.flags & MessageFlags.JSON_FORMAT:
  160. payload_data = json.dumps(self.data).encode('utf-8')
  161. else:
  162. payload_data = pickle.dumps(self.data)
  163. else:
  164. # Serialize as class instance
  165. if self.options.flags & MessageFlags.JSON_FORMAT:
  166. # Convert class to dict then JSON
  167. if hasattr(self.data, '__dict__'):
  168. class_dict = self.data.__dict__.copy()
  169. class_dict['__class__'] = self.data.__class__.__name__
  170. payload_data = json.dumps(class_dict).encode('utf-8')
  171. else:
  172. payload_data = json.dumps(str(self.data)).encode('utf-8')
  173. else:
  174. payload_data = pickle.dumps(self.data)
  175. # Apply compression if requested
  176. if self.options.flags & MessageFlags.GZ_COMPRESSED:
  177. payload_data = compress_data(payload_data)
  178. # Apply encryption if requested
  179. if self.options.flags & MessageFlags.ENCRYPTED:
  180. if not self.options.encryption_key:
  181. raise EncryptionError("Encryption requested but no key provided")
  182. payload_data = encrypt_data(payload_data, self.options.encryption_key)
  183. # Apply base64 encoding if requested
  184. if self.options.flags & MessageFlags.BASE64_ENCODED:
  185. payload_data = base64.b64encode(payload_data)
  186. # Store serialized data for checksum calculation
  187. self.serialized_data = payload_data
  188. # Build the complete message
  189. message = self._build_message(payload_data)
  190. if not (self.options.flags & MessageFlags.SILENT):
  191. pprint(f"Serialized message: {self.class_name} ({len(message)} bytes)")
  192. return message
  193. except Exception as e:
  194. raise ProtocolError(f"Failed to serialize message: {e}") from e
  195. def _build_message(self, payload_data: bytes) -> bytes:
  196. """Build the complete binary message with headers."""
  197. # Encode class name
  198. class_name_bytes = self.class_name.encode('utf-8')
  199. # Build header components
  200. magic = MAGIC_NUMBER
  201. version_bytes = self.version.to_bytes()
  202. timestamp_bytes = struct.pack(">d", self.options.timestamp.timestamp())
  203. flags_bytes = struct.pack(">I", int(self.options.flags))
  204. # Calculate MD5 checksum of class name + payload
  205. checksum_data = class_name_bytes + payload_data
  206. checksum = calculate_checksum(checksum_data)
  207. # Length fields
  208. class_name_length = struct.pack(">I", len(class_name_bytes))
  209. payload_length = struct.pack(">I", len(payload_data))
  210. # Assemble complete message
  211. message = (
  212. magic + # 4 bytes
  213. version_bytes + # 12 bytes (3 * 4)
  214. timestamp_bytes + # 8 bytes
  215. flags_bytes + # 4 bytes
  216. checksum + # 16 bytes
  217. class_name_length + # 4 bytes
  218. class_name_bytes + # variable
  219. payload_length + # 4 bytes
  220. payload_data # variable
  221. )
  222. return message
  223. @classmethod
  224. def deserialize(cls, data: bytes) -> 'TrixyMessage':
  225. """
  226. Deserialize binary data into a TrixyMessage.
  227. Args:
  228. data: Binary message data
  229. Returns:
  230. TrixyMessage: Deserialized message
  231. Raises:
  232. ProtocolError: If deserialization fails
  233. """
  234. try:
  235. if len(data) < HEADER_SIZE:
  236. raise InvalidMessageError(f"Message too short: {len(data)} bytes")
  237. # Parse header
  238. offset = 0
  239. # Magic number
  240. magic = data[offset:offset + 4]
  241. if magic != MAGIC_NUMBER:
  242. raise InvalidMessageError(f"Invalid magic number: {magic}")
  243. offset += 4
  244. # Version
  245. version = ProtocolVersion.from_bytes(data[offset:offset + 12])
  246. offset += 12
  247. # Timestamp
  248. timestamp_float = struct.unpack(">d", data[offset:offset + 8])[0]
  249. timestamp = datetime.fromtimestamp(timestamp_float)
  250. offset += 8
  251. # Flags
  252. flags = MessageFlags(struct.unpack(">I", data[offset:offset + 4])[0])
  253. offset += 4
  254. # Checksum
  255. received_checksum = data[offset:offset + 16]
  256. offset += 16
  257. # Class name length and data
  258. class_name_length = struct.unpack(">I", data[offset:offset + 4])[0]
  259. offset += 4
  260. class_name = data[offset:offset + class_name_length].decode('utf-8')
  261. offset += class_name_length
  262. # Payload length and data
  263. payload_length = struct.unpack(">I", data[offset:offset + 4])[0]
  264. offset += 4
  265. payload_data = data[offset:offset + payload_length]
  266. # Verify checksum
  267. checksum_data = class_name.encode('utf-8') + payload_data
  268. calculated_checksum = calculate_checksum(checksum_data)
  269. if received_checksum != calculated_checksum:
  270. raise ChecksumMismatchError("MD5 checksum mismatch")
  271. # Process payload data
  272. processed_data = cls._process_payload(payload_data, flags, class_name)
  273. # Create message options
  274. options = MessageOptions(flags=flags, timestamp=timestamp)
  275. # Create message instance
  276. message = cls(class_name, processed_data, version, options)
  277. message.checksum = received_checksum
  278. message.serialized_data = payload_data
  279. if not (flags & MessageFlags.SILENT):
  280. pprint(f"Deserialized message: {class_name} ({len(data)} bytes)")
  281. return message
  282. except Exception as e:
  283. if isinstance(e, ProtocolError):
  284. raise
  285. raise ProtocolError(f"Failed to deserialize message: {e}") from e
  286. @classmethod
  287. def _process_payload(cls, payload_data: bytes, flags: MessageFlags, class_name: str) -> Any:
  288. """Process payload data according to flags."""
  289. data = payload_data
  290. # Reverse base64 encoding
  291. if flags & MessageFlags.BASE64_ENCODED:
  292. data = base64.b64decode(data)
  293. # Reverse encryption (would need key - placeholder for now)
  294. if flags & MessageFlags.ENCRYPTED:
  295. raise EncryptionError("Cannot decrypt without key")
  296. # Reverse compression
  297. if flags & MessageFlags.GZ_COMPRESSED:
  298. data = decompress_data(data)
  299. # Special handling for hard-coded commands
  300. if class_name == "HARDCODED":
  301. return data.decode('utf-8')
  302. # Deserialize based on format
  303. if flags & MessageFlags.DICTIONARY:
  304. if flags & MessageFlags.JSON_FORMAT:
  305. return json.loads(data.decode('utf-8'))
  306. else:
  307. return pickle.loads(data)
  308. else:
  309. if flags & MessageFlags.JSON_FORMAT:
  310. json_data = json.loads(data.decode('utf-8'))
  311. # If it's a class dict, we'd need to reconstruct the class
  312. # For now just return the dict
  313. return json_data
  314. else:
  315. return pickle.loads(data)
  316. class TrixyProtocol:
  317. """
  318. Main protocol handler for Trixy network communication.
  319. This class provides high-level methods for creating, sending, and receiving
  320. Trixy protocol messages. It handles all serialization, compression, and
  321. other protocol features.
  322. """
  323. def __init__(self, version: Optional[ProtocolVersion] = None):
  324. """
  325. Initialize the protocol handler.
  326. Args:
  327. version: Protocol version to use (defaults to 1.0.0)
  328. """
  329. self.version = version or ProtocolVersion(1, 0, 0)
  330. self._lock = threading.RLock()
  331. self._encryption_keys: Dict[str, bytes] = {}
  332. pprint(f"Trixy protocol initialized (version {self.version})")
  333. def create_message(
  334. self,
  335. class_name: str,
  336. data: Any,
  337. options: Optional[MessageOptions] = None
  338. ) -> TrixyMessage:
  339. """
  340. Create a new protocol message.
  341. Args:
  342. class_name: Name of the command class
  343. data: Data to serialize
  344. options: Message options
  345. Returns:
  346. TrixyMessage: Created message
  347. """
  348. with self._lock:
  349. return TrixyMessage(class_name, data, self.version, options)
  350. def create_hardcoded_message(self, command: str) -> TrixyMessage:
  351. """
  352. Create a hard-coded command message for performance.
  353. Args:
  354. command: Hard-coded command string (TRXINOOP, TRXIPING, etc.)
  355. Returns:
  356. TrixyMessage: Created message
  357. """
  358. options = MessageOptions(flags=MessageFlags.SILENT) # Hard-coded commands are silent
  359. return self.create_message("HARDCODED", command, options)
  360. def serialize_message(self, message: TrixyMessage) -> bytes:
  361. """
  362. Serialize a message to binary format.
  363. Args:
  364. message: Message to serialize
  365. Returns:
  366. bytes: Serialized message
  367. """
  368. return message.serialize()
  369. def deserialize_message(self, data: bytes) -> TrixyMessage:
  370. """
  371. Deserialize binary data to a message.
  372. Args:
  373. data: Binary message data
  374. Returns:
  375. TrixyMessage: Deserialized message
  376. """
  377. return TrixyMessage.deserialize(data)
  378. def is_hardcoded_command(self, data: bytes) -> bool:
  379. """
  380. Check if data starts with a hard-coded command.
  381. Args:
  382. data: Raw data from socket
  383. Returns:
  384. bool: True if it's a hard-coded command
  385. """
  386. try:
  387. # Hard-coded commands are simple text strings
  388. text = data.decode('utf-8', errors='ignore')
  389. return any(text.startswith(cmd) for cmd in ["TRXI"])
  390. except:
  391. return False
  392. def parse_hardcoded_command(self, data: bytes) -> Tuple[str, str]:
  393. """
  394. Parse a hard-coded command from raw data.
  395. Args:
  396. data: Raw command data
  397. Returns:
  398. Tuple[str, str]: (command, arguments)
  399. """
  400. try:
  401. text = data.decode('utf-8').strip()
  402. parts = text.split(' ', 1)
  403. command = parts[0]
  404. args = parts[1] if len(parts) > 1 else ""
  405. return command, args
  406. except Exception as e:
  407. raise ProtocolError(f"Failed to parse hard-coded command: {e}") from e
  408. def set_encryption_key(self, key_id: str, key: bytes) -> None:
  409. """
  410. Set an encryption key for encrypted messages.
  411. Args:
  412. key_id: Identifier for the key
  413. key: Encryption key bytes
  414. """
  415. with self._lock:
  416. self._encryption_keys[key_id] = key
  417. pprint(f"Encryption key set for: {key_id}")
  418. def get_encryption_key(self, key_id: str) -> Optional[bytes]:
  419. """
  420. Get an encryption key by ID.
  421. Args:
  422. key_id: Key identifier
  423. Returns:
  424. Optional[bytes]: Encryption key or None if not found
  425. """
  426. with self._lock:
  427. return self._encryption_keys.get(key_id)
  428. # Utility functions
  429. def calculate_checksum(data: bytes) -> bytes:
  430. """
  431. Calculate MD5 checksum for data.
  432. Args:
  433. data: Data to checksum
  434. Returns:
  435. bytes: 16-byte MD5 hash
  436. """
  437. return hashlib.md5(data).digest()
  438. def compress_data(data: bytes) -> bytes:
  439. """
  440. Compress data using gzip.
  441. Args:
  442. data: Data to compress
  443. Returns:
  444. bytes: Compressed data
  445. Raises:
  446. CompressionError: If compression fails
  447. """
  448. try:
  449. return gzip.compress(data)
  450. except Exception as e:
  451. raise CompressionError(f"Failed to compress data: {e}") from e
  452. def decompress_data(data: bytes) -> bytes:
  453. """
  454. Decompress gzip data.
  455. Args:
  456. data: Compressed data
  457. Returns:
  458. bytes: Decompressed data
  459. Raises:
  460. CompressionError: If decompression fails
  461. """
  462. try:
  463. return gzip.decompress(data)
  464. except Exception as e:
  465. raise CompressionError(f"Failed to decompress data: {e}") from e
  466. def encrypt_data(data: bytes, key: bytes) -> bytes:
  467. """
  468. Encrypt data using Fernet symmetric encryption.
  469. Args:
  470. data: Data to encrypt
  471. key: Encryption key
  472. Returns:
  473. bytes: Encrypted data
  474. Raises:
  475. EncryptionError: If encryption fails or not available
  476. """
  477. if not ENCRYPTION_AVAILABLE:
  478. raise EncryptionError("Cryptography library not available")
  479. try:
  480. fernet = Fernet(key)
  481. return fernet.encrypt(data)
  482. except Exception as e:
  483. raise EncryptionError(f"Failed to encrypt data: {e}") from e
  484. def decrypt_data(data: bytes, key: bytes) -> bytes:
  485. """
  486. Decrypt data using Fernet symmetric encryption.
  487. Args:
  488. data: Encrypted data
  489. key: Decryption key
  490. Returns:
  491. bytes: Decrypted data
  492. Raises:
  493. EncryptionError: If decryption fails or not available
  494. """
  495. if not ENCRYPTION_AVAILABLE:
  496. raise EncryptionError("Cryptography library not available")
  497. try:
  498. fernet = Fernet(key)
  499. return fernet.decrypt(data)
  500. except Exception as e:
  501. raise EncryptionError(f"Failed to decrypt data: {e}") from e
  502. def serialize_object(obj: Any, use_json: bool = False) -> bytes:
  503. """
  504. Serialize an object to bytes.
  505. Args:
  506. obj: Object to serialize
  507. use_json: Use JSON instead of pickle
  508. Returns:
  509. bytes: Serialized object
  510. """
  511. try:
  512. if use_json:
  513. if hasattr(obj, '__dict__'):
  514. obj_dict = obj.__dict__.copy()
  515. obj_dict['__class__'] = obj.__class__.__name__
  516. return json.dumps(obj_dict).encode('utf-8')
  517. else:
  518. return json.dumps(obj).encode('utf-8')
  519. else:
  520. return pickle.dumps(obj)
  521. except Exception as e:
  522. raise ProtocolError(f"Failed to serialize object: {e}") from e
  523. def deserialize_object(data: bytes, use_json: bool = False) -> Any:
  524. """
  525. Deserialize bytes to an object.
  526. Args:
  527. data: Serialized data
  528. use_json: Expect JSON format
  529. Returns:
  530. Any: Deserialized object
  531. """
  532. try:
  533. if use_json:
  534. return json.loads(data.decode('utf-8'))
  535. else:
  536. return pickle.loads(data)
  537. except Exception as e:
  538. raise ProtocolError(f"Failed to deserialize object: {e}") from e
  539. def generate_encryption_key() -> bytes:
  540. """
  541. Generate a new Fernet encryption key.
  542. Returns:
  543. bytes: Generated key
  544. Raises:
  545. EncryptionError: If cryptography library not available
  546. """
  547. if not ENCRYPTION_AVAILABLE:
  548. raise EncryptionError("Cryptography library not available")
  549. return Fernet.generate_key()
  550. # Module exports
  551. __all__ = [
  552. "TrixyProtocol",
  553. "TrixyMessage",
  554. "MessageOptions",
  555. "MessageFlags",
  556. "ProtocolVersion",
  557. "ProtocolError",
  558. "InvalidMessageError",
  559. "ChecksumMismatchError",
  560. "UnsupportedVersionError",
  561. "CompressionError",
  562. "EncryptionError",
  563. "serialize_object",
  564. "deserialize_object",
  565. "calculate_checksum",
  566. "compress_data",
  567. "decompress_data",
  568. "encrypt_data",
  569. "decrypt_data",
  570. "generate_encryption_key",
  571. "MAGIC_NUMBER",
  572. "HEADER_SIZE",
  573. ]