| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727 |
- """
- Trixy Protocol Implementation
- This module implements the custom Trixy binary streaming protocol as specified
- in CLAUDE.md. It provides:
- - Binary message structure with magic number "TRXI"
- - Version fields, datetime, 32-bit option flags, MD5 checksum
- - Support for serialized Python classes and dictionaries
- - Compression, encryption, and encoding options
- - Hard-coded performance commands
- - Thread-safe serialization/deserialization
- Message Structure:
- [4 bytes] Magic Number: "TRXI"
- [4 bytes] Version Major (int)
- [4 bytes] Version Minor (int)
- [4 bytes] Version Revision (int)
- [8 bytes] DateTime (binary)
- [4 bytes] Options (32-bit flags)
- [16 bytes] MD5 Checksum
- [4 bytes] Class Name Length
- [variable] Class Name
- [4 bytes] Serialized Data Length
- [variable] Serialized Class Instance
- Option Flags (32-bit):
- Bit 1: GZ compressed
- Bit 2: Encrypted
- Bit 3: JSON format (else binary)
- Bit 4: Requires "received" response
- Bit 5: Base64 encoded
- Bit 6: Multi-part message
- Bit 7: Dictionary instead of class
- Bit 8: Silent (no debug logging)
- """
- import struct
- import time
- import hashlib
- import gzip
- import json
- import pickle
- import base64
- import threading
- from typing import Union, Dict, Any, Optional, Tuple, Type
- from dataclasses import dataclass
- from enum import IntFlag, Enum
- from datetime import datetime
- import io
- # Import encryption if available
- try:
- from cryptography.fernet import Fernet
- ENCRYPTION_AVAILABLE = True
- except ImportError:
- ENCRYPTION_AVAILABLE = False
- def pprint(message: str) -> None:
- """Protocol logging function."""
- print(f"[PROTOCOL] {message}")
- # Protocol constants
- MAGIC_NUMBER = b"TRXI"
- HEADER_SIZE = 48 # Fixed header size in bytes
- class MessageFlags(IntFlag):
- """32-bit option flags for Trixy protocol messages."""
- NONE = 0
- GZ_COMPRESSED = 1 << 0 # Bit 1: GZ compressed
- ENCRYPTED = 1 << 1 # Bit 2: Encrypted
- JSON_FORMAT = 1 << 2 # Bit 3: JSON format (else binary)
- REQUIRES_ACK = 1 << 3 # Bit 4: Requires "received" response
- BASE64_ENCODED = 1 << 4 # Bit 5: Base64 encoded
- MULTI_PART = 1 << 5 # Bit 6: Multi-part message
- DICTIONARY = 1 << 6 # Bit 7: Dictionary instead of class
- SILENT = 1 << 7 # Bit 8: Silent (no debug logging)
- @dataclass
- class ProtocolVersion:
- """Protocol version information."""
- major: int
- minor: int
- revision: int
-
- def __str__(self) -> str:
- return f"{self.major}.{self.minor}.{self.revision}"
-
- def to_bytes(self) -> bytes:
- """Convert version to bytes for protocol header."""
- return struct.pack(">III", self.major, self.minor, self.revision)
-
- @classmethod
- def from_bytes(cls, data: bytes) -> 'ProtocolVersion':
- """Create version from bytes."""
- major, minor, revision = struct.unpack(">III", data)
- return cls(major, minor, revision)
- @dataclass
- class MessageOptions:
- """Message options and metadata."""
- flags: MessageFlags = MessageFlags.NONE
- timestamp: Optional[datetime] = None
- encryption_key: Optional[bytes] = None
- part_number: int = 0
- total_parts: int = 1
-
- def __post_init__(self):
- if self.timestamp is None:
- self.timestamp = datetime.now()
- class ProtocolError(Exception):
- """Base exception for protocol errors."""
- pass
- class InvalidMessageError(ProtocolError):
- """Raised when message format is invalid."""
- pass
- class ChecksumMismatchError(ProtocolError):
- """Raised when MD5 checksum doesn't match."""
- pass
- class UnsupportedVersionError(ProtocolError):
- """Raised when protocol version is unsupported."""
- pass
- class CompressionError(ProtocolError):
- """Raised when compression/decompression fails."""
- pass
- class EncryptionError(ProtocolError):
- """Raised when encryption/decryption fails."""
- pass
- class TrixyMessage:
- """
- Represents a complete Trixy protocol message.
-
- This class handles the binary message format including all headers,
- options, and payload data according to the protocol specification.
- """
-
- def __init__(
- self,
- class_name: str,
- data: Any,
- version: Optional[ProtocolVersion] = None,
- options: Optional[MessageOptions] = None
- ):
- """
- Initialize a Trixy message.
-
- Args:
- class_name: Name of the command class or "HARDCODED" for hard-coded commands
- data: The data to serialize (class instance, dict, or string)
- version: Protocol version (uses current if None)
- options: Message options (uses defaults if None)
- """
- self.class_name = class_name
- self.data = data
- self.version = version or ProtocolVersion(1, 0, 0)
- self.options = options or MessageOptions()
-
- # Calculated fields
- self.checksum: Optional[bytes] = None
- self.serialized_data: Optional[bytes] = None
- self._lock = threading.RLock()
-
- def serialize(self) -> bytes:
- """
- Serialize the message to binary format according to protocol specification.
-
- Returns:
- bytes: Complete binary message
-
- Raises:
- ProtocolError: If serialization fails
- """
- with self._lock:
- try:
- # Serialize the payload data
- if self.class_name == "HARDCODED":
- # Hard-coded commands are sent as UTF-8 strings
- payload_data = str(self.data).encode('utf-8')
- elif self.options.flags & MessageFlags.DICTIONARY:
- # Serialize as dictionary
- if self.options.flags & MessageFlags.JSON_FORMAT:
- payload_data = json.dumps(self.data).encode('utf-8')
- else:
- payload_data = pickle.dumps(self.data)
- else:
- # Serialize as class instance
- if self.options.flags & MessageFlags.JSON_FORMAT:
- # Convert class to dict then JSON
- if hasattr(self.data, '__dict__'):
- class_dict = self.data.__dict__.copy()
- class_dict['__class__'] = self.data.__class__.__name__
- payload_data = json.dumps(class_dict).encode('utf-8')
- else:
- payload_data = json.dumps(str(self.data)).encode('utf-8')
- else:
- payload_data = pickle.dumps(self.data)
-
- # Apply compression if requested
- if self.options.flags & MessageFlags.GZ_COMPRESSED:
- payload_data = compress_data(payload_data)
-
- # Apply encryption if requested
- if self.options.flags & MessageFlags.ENCRYPTED:
- if not self.options.encryption_key:
- raise EncryptionError("Encryption requested but no key provided")
- payload_data = encrypt_data(payload_data, self.options.encryption_key)
-
- # Apply base64 encoding if requested
- if self.options.flags & MessageFlags.BASE64_ENCODED:
- payload_data = base64.b64encode(payload_data)
-
- # Store serialized data for checksum calculation
- self.serialized_data = payload_data
-
- # Build the complete message
- message = self._build_message(payload_data)
-
- if not (self.options.flags & MessageFlags.SILENT):
- pprint(f"Serialized message: {self.class_name} ({len(message)} bytes)")
-
- return message
-
- except Exception as e:
- raise ProtocolError(f"Failed to serialize message: {e}") from e
-
- def _build_message(self, payload_data: bytes) -> bytes:
- """Build the complete binary message with headers."""
- # Encode class name
- class_name_bytes = self.class_name.encode('utf-8')
-
- # Build header components
- magic = MAGIC_NUMBER
- version_bytes = self.version.to_bytes()
- timestamp_bytes = struct.pack(">d", self.options.timestamp.timestamp())
- flags_bytes = struct.pack(">I", int(self.options.flags))
-
- # Calculate MD5 checksum of class name + payload
- checksum_data = class_name_bytes + payload_data
- checksum = calculate_checksum(checksum_data)
-
- # Length fields
- class_name_length = struct.pack(">I", len(class_name_bytes))
- payload_length = struct.pack(">I", len(payload_data))
-
- # Assemble complete message
- message = (
- magic + # 4 bytes
- version_bytes + # 12 bytes (3 * 4)
- timestamp_bytes + # 8 bytes
- flags_bytes + # 4 bytes
- checksum + # 16 bytes
- class_name_length + # 4 bytes
- class_name_bytes + # variable
- payload_length + # 4 bytes
- payload_data # variable
- )
-
- return message
-
- @classmethod
- def deserialize(cls, data: bytes) -> 'TrixyMessage':
- """
- Deserialize binary data into a TrixyMessage.
-
- Args:
- data: Binary message data
-
- Returns:
- TrixyMessage: Deserialized message
-
- Raises:
- ProtocolError: If deserialization fails
- """
- try:
- if len(data) < HEADER_SIZE:
- raise InvalidMessageError(f"Message too short: {len(data)} bytes")
-
- # Parse header
- offset = 0
-
- # Magic number
- magic = data[offset:offset + 4]
- if magic != MAGIC_NUMBER:
- raise InvalidMessageError(f"Invalid magic number: {magic}")
- offset += 4
-
- # Version
- version = ProtocolVersion.from_bytes(data[offset:offset + 12])
- offset += 12
-
- # Timestamp
- timestamp_float = struct.unpack(">d", data[offset:offset + 8])[0]
- timestamp = datetime.fromtimestamp(timestamp_float)
- offset += 8
-
- # Flags
- flags = MessageFlags(struct.unpack(">I", data[offset:offset + 4])[0])
- offset += 4
-
- # Checksum
- received_checksum = data[offset:offset + 16]
- offset += 16
-
- # Class name length and data
- class_name_length = struct.unpack(">I", data[offset:offset + 4])[0]
- offset += 4
-
- class_name = data[offset:offset + class_name_length].decode('utf-8')
- offset += class_name_length
-
- # Payload length and data
- payload_length = struct.unpack(">I", data[offset:offset + 4])[0]
- offset += 4
-
- payload_data = data[offset:offset + payload_length]
-
- # Verify checksum
- checksum_data = class_name.encode('utf-8') + payload_data
- calculated_checksum = calculate_checksum(checksum_data)
-
- if received_checksum != calculated_checksum:
- raise ChecksumMismatchError("MD5 checksum mismatch")
-
- # Process payload data
- processed_data = cls._process_payload(payload_data, flags, class_name)
-
- # Create message options
- options = MessageOptions(flags=flags, timestamp=timestamp)
-
- # Create message instance
- message = cls(class_name, processed_data, version, options)
- message.checksum = received_checksum
- message.serialized_data = payload_data
-
- if not (flags & MessageFlags.SILENT):
- pprint(f"Deserialized message: {class_name} ({len(data)} bytes)")
-
- return message
-
- except Exception as e:
- if isinstance(e, ProtocolError):
- raise
- raise ProtocolError(f"Failed to deserialize message: {e}") from e
-
- @classmethod
- def _process_payload(cls, payload_data: bytes, flags: MessageFlags, class_name: str) -> Any:
- """Process payload data according to flags."""
- data = payload_data
-
- # Reverse base64 encoding
- if flags & MessageFlags.BASE64_ENCODED:
- data = base64.b64decode(data)
-
- # Reverse encryption (would need key - placeholder for now)
- if flags & MessageFlags.ENCRYPTED:
- raise EncryptionError("Cannot decrypt without key")
-
- # Reverse compression
- if flags & MessageFlags.GZ_COMPRESSED:
- data = decompress_data(data)
-
- # Special handling for hard-coded commands
- if class_name == "HARDCODED":
- return data.decode('utf-8')
-
- # Deserialize based on format
- if flags & MessageFlags.DICTIONARY:
- if flags & MessageFlags.JSON_FORMAT:
- return json.loads(data.decode('utf-8'))
- else:
- return pickle.loads(data)
- else:
- if flags & MessageFlags.JSON_FORMAT:
- json_data = json.loads(data.decode('utf-8'))
- # If it's a class dict, we'd need to reconstruct the class
- # For now just return the dict
- return json_data
- else:
- return pickle.loads(data)
- class TrixyProtocol:
- """
- Main protocol handler for Trixy network communication.
-
- This class provides high-level methods for creating, sending, and receiving
- Trixy protocol messages. It handles all serialization, compression, and
- other protocol features.
- """
-
- def __init__(self, version: Optional[ProtocolVersion] = None):
- """
- Initialize the protocol handler.
-
- Args:
- version: Protocol version to use (defaults to 1.0.0)
- """
- self.version = version or ProtocolVersion(1, 0, 0)
- self._lock = threading.RLock()
- self._encryption_keys: Dict[str, bytes] = {}
-
- pprint(f"Trixy protocol initialized (version {self.version})")
-
- def create_message(
- self,
- class_name: str,
- data: Any,
- options: Optional[MessageOptions] = None
- ) -> TrixyMessage:
- """
- Create a new protocol message.
-
- Args:
- class_name: Name of the command class
- data: Data to serialize
- options: Message options
-
- Returns:
- TrixyMessage: Created message
- """
- with self._lock:
- return TrixyMessage(class_name, data, self.version, options)
-
- def create_hardcoded_message(self, command: str) -> TrixyMessage:
- """
- Create a hard-coded command message for performance.
-
- Args:
- command: Hard-coded command string (TRXINOOP, TRXIPING, etc.)
-
- Returns:
- TrixyMessage: Created message
- """
- options = MessageOptions(flags=MessageFlags.SILENT) # Hard-coded commands are silent
- return self.create_message("HARDCODED", command, options)
-
- def serialize_message(self, message: TrixyMessage) -> bytes:
- """
- Serialize a message to binary format.
-
- Args:
- message: Message to serialize
-
- Returns:
- bytes: Serialized message
- """
- return message.serialize()
-
- def deserialize_message(self, data: bytes) -> TrixyMessage:
- """
- Deserialize binary data to a message.
-
- Args:
- data: Binary message data
-
- Returns:
- TrixyMessage: Deserialized message
- """
- return TrixyMessage.deserialize(data)
-
- def is_hardcoded_command(self, data: bytes) -> bool:
- """
- Check if data starts with a hard-coded command.
-
- Args:
- data: Raw data from socket
-
- Returns:
- bool: True if it's a hard-coded command
- """
- try:
- # Hard-coded commands are simple text strings
- text = data.decode('utf-8', errors='ignore')
- return any(text.startswith(cmd) for cmd in ["TRXI"])
- except:
- return False
-
- def parse_hardcoded_command(self, data: bytes) -> Tuple[str, str]:
- """
- Parse a hard-coded command from raw data.
-
- Args:
- data: Raw command data
-
- Returns:
- Tuple[str, str]: (command, arguments)
- """
- try:
- text = data.decode('utf-8').strip()
- parts = text.split(' ', 1)
- command = parts[0]
- args = parts[1] if len(parts) > 1 else ""
- return command, args
- except Exception as e:
- raise ProtocolError(f"Failed to parse hard-coded command: {e}") from e
-
- def set_encryption_key(self, key_id: str, key: bytes) -> None:
- """
- Set an encryption key for encrypted messages.
-
- Args:
- key_id: Identifier for the key
- key: Encryption key bytes
- """
- with self._lock:
- self._encryption_keys[key_id] = key
- pprint(f"Encryption key set for: {key_id}")
-
- def get_encryption_key(self, key_id: str) -> Optional[bytes]:
- """
- Get an encryption key by ID.
-
- Args:
- key_id: Key identifier
-
- Returns:
- Optional[bytes]: Encryption key or None if not found
- """
- with self._lock:
- return self._encryption_keys.get(key_id)
- # Utility functions
- def calculate_checksum(data: bytes) -> bytes:
- """
- Calculate MD5 checksum for data.
-
- Args:
- data: Data to checksum
-
- Returns:
- bytes: 16-byte MD5 hash
- """
- return hashlib.md5(data).digest()
- def compress_data(data: bytes) -> bytes:
- """
- Compress data using gzip.
-
- Args:
- data: Data to compress
-
- Returns:
- bytes: Compressed data
-
- Raises:
- CompressionError: If compression fails
- """
- try:
- return gzip.compress(data)
- except Exception as e:
- raise CompressionError(f"Failed to compress data: {e}") from e
- def decompress_data(data: bytes) -> bytes:
- """
- Decompress gzip data.
-
- Args:
- data: Compressed data
-
- Returns:
- bytes: Decompressed data
-
- Raises:
- CompressionError: If decompression fails
- """
- try:
- return gzip.decompress(data)
- except Exception as e:
- raise CompressionError(f"Failed to decompress data: {e}") from e
- def encrypt_data(data: bytes, key: bytes) -> bytes:
- """
- Encrypt data using Fernet symmetric encryption.
-
- Args:
- data: Data to encrypt
- key: Encryption key
-
- Returns:
- bytes: Encrypted data
-
- Raises:
- EncryptionError: If encryption fails or not available
- """
- if not ENCRYPTION_AVAILABLE:
- raise EncryptionError("Cryptography library not available")
-
- try:
- fernet = Fernet(key)
- return fernet.encrypt(data)
- except Exception as e:
- raise EncryptionError(f"Failed to encrypt data: {e}") from e
- def decrypt_data(data: bytes, key: bytes) -> bytes:
- """
- Decrypt data using Fernet symmetric encryption.
-
- Args:
- data: Encrypted data
- key: Decryption key
-
- Returns:
- bytes: Decrypted data
-
- Raises:
- EncryptionError: If decryption fails or not available
- """
- if not ENCRYPTION_AVAILABLE:
- raise EncryptionError("Cryptography library not available")
-
- try:
- fernet = Fernet(key)
- return fernet.decrypt(data)
- except Exception as e:
- raise EncryptionError(f"Failed to decrypt data: {e}") from e
- def serialize_object(obj: Any, use_json: bool = False) -> bytes:
- """
- Serialize an object to bytes.
-
- Args:
- obj: Object to serialize
- use_json: Use JSON instead of pickle
-
- Returns:
- bytes: Serialized object
- """
- try:
- if use_json:
- if hasattr(obj, '__dict__'):
- obj_dict = obj.__dict__.copy()
- obj_dict['__class__'] = obj.__class__.__name__
- return json.dumps(obj_dict).encode('utf-8')
- else:
- return json.dumps(obj).encode('utf-8')
- else:
- return pickle.dumps(obj)
- except Exception as e:
- raise ProtocolError(f"Failed to serialize object: {e}") from e
- def deserialize_object(data: bytes, use_json: bool = False) -> Any:
- """
- Deserialize bytes to an object.
-
- Args:
- data: Serialized data
- use_json: Expect JSON format
-
- Returns:
- Any: Deserialized object
- """
- try:
- if use_json:
- return json.loads(data.decode('utf-8'))
- else:
- return pickle.loads(data)
- except Exception as e:
- raise ProtocolError(f"Failed to deserialize object: {e}") from e
- def generate_encryption_key() -> bytes:
- """
- Generate a new Fernet encryption key.
-
- Returns:
- bytes: Generated key
-
- Raises:
- EncryptionError: If cryptography library not available
- """
- if not ENCRYPTION_AVAILABLE:
- raise EncryptionError("Cryptography library not available")
-
- return Fernet.generate_key()
- # Module exports
- __all__ = [
- "TrixyProtocol",
- "TrixyMessage",
- "MessageOptions",
- "MessageFlags",
- "ProtocolVersion",
- "ProtocolError",
- "InvalidMessageError",
- "ChecksumMismatchError",
- "UnsupportedVersionError",
- "CompressionError",
- "EncryptionError",
- "serialize_object",
- "deserialize_object",
- "calculate_checksum",
- "compress_data",
- "decompress_data",
- "encrypt_data",
- "decrypt_data",
- "generate_encryption_key",
- "MAGIC_NUMBER",
- "HEADER_SIZE",
- ]
|