A Python port of the Invisible Internet Project (I2P)
at main 204 lines 6.9 kB view raw
1"""I2NP message types — header, base class, and concrete messages.""" 2 3import hashlib 4import struct 5from abc import ABC, abstractmethod 6 7 8class I2NPHeader: 9 """I2NP message header: type(1) + msg_id(4) + expiration(8) + size(2) + checksum(1) = 16 bytes.""" 10 11 SIZE = 16 12 13 def __init__(self, msg_type: int, msg_id: int, expiration: int, size: int, checksum: int): 14 self.msg_type = msg_type 15 self.msg_id = msg_id 16 self.expiration = expiration 17 self.size = size 18 self.checksum = checksum 19 20 def to_bytes(self) -> bytes: 21 return struct.pack("!BIQHB", self.msg_type, self.msg_id, 22 self.expiration, self.size, self.checksum) 23 24 @classmethod 25 def from_bytes(cls, data: bytes) -> "I2NPHeader": 26 if len(data) < cls.SIZE: 27 raise ValueError(f"I2NPHeader requires {cls.SIZE} bytes, got {len(data)}") 28 msg_type, msg_id, expiration, size, checksum = struct.unpack("!BIQHB", data[:cls.SIZE]) 29 return cls(msg_type, msg_id, expiration, size, checksum) 30 31 @classmethod 32 def from_stream(cls, stream) -> "I2NPHeader": 33 data = stream.read(cls.SIZE) 34 if len(data) < cls.SIZE: 35 raise ValueError(f"I2NPHeader requires {cls.SIZE} bytes") 36 return cls.from_bytes(data) 37 38 39class I2NPMessage(ABC): 40 """Base class for I2NP messages with type registry.""" 41 42 TYPE: int = -1 43 _registry: dict[int, type["I2NPMessage"]] = {} 44 _header_msg_id: int = 0 45 _header_expiration: int = 0 46 47 @classmethod 48 def __init_subclass__(cls, **kwargs): 49 super().__init_subclass__(**kwargs) 50 if hasattr(cls, 'TYPE') and cls.TYPE >= 0: 51 I2NPMessage._registry[cls.TYPE] = cls 52 53 @abstractmethod 54 def body_bytes(self) -> bytes: 55 """Serialize the message body (without header).""" 56 57 @staticmethod 58 def calculate_checksum(body: bytes) -> int: 59 return hashlib.sha256(body).digest()[0] 60 61 def to_bytes(self) -> bytes: 62 body = self.body_bytes() 63 checksum = self.calculate_checksum(body) 64 header = I2NPHeader( 65 msg_type=self.TYPE, 66 msg_id=getattr(self, '_header_msg_id', 0), 67 expiration=getattr(self, '_header_expiration', 0), 68 size=len(body), 69 checksum=checksum, 70 ) 71 return header.to_bytes() + body 72 73 @classmethod 74 def from_bytes(cls, data: bytes) -> "I2NPMessage": 75 if len(data) < I2NPHeader.SIZE: 76 raise ValueError(f"Need at least {I2NPHeader.SIZE} bytes for I2NP message") 77 header = I2NPHeader.from_bytes(data) 78 body_start = I2NPHeader.SIZE 79 body_end = body_start + header.size 80 if len(data) < body_end: 81 raise ValueError(f"Message body truncated: need {header.size} bytes, got {len(data) - body_start}") 82 body = data[body_start:body_end] 83 expected_checksum = I2NPMessage.calculate_checksum(body) 84 if header.checksum != expected_checksum: 85 raise ValueError(f"Checksum mismatch: expected {expected_checksum}, got {header.checksum}") 86 msg_cls = cls._registry.get(header.msg_type) 87 if msg_cls is None: 88 raise ValueError(f"Unknown I2NP message type: {header.msg_type}") 89 msg = msg_cls._from_body(body) 90 msg._header_msg_id = header.msg_id 91 msg._header_expiration = header.expiration 92 return msg 93 94 @classmethod 95 @abstractmethod 96 def _from_body(cls, body: bytes) -> "I2NPMessage": 97 """Deserialize from body bytes.""" 98 99 100class DeliveryStatusMessage(I2NPMessage): 101 """Type 10: msg_id(4) + arrival_time(8).""" 102 103 TYPE = 10 104 105 def __init__(self, msg_id: int, arrival_time: int): 106 self.msg_id = msg_id 107 self.arrival_time = arrival_time 108 self._header_msg_id = msg_id 109 self._header_expiration = 0 110 111 def body_bytes(self) -> bytes: 112 return struct.pack("!IQ", self.msg_id, self.arrival_time) 113 114 @classmethod 115 def _from_body(cls, body: bytes) -> "DeliveryStatusMessage": 116 msg_id, arrival_time = struct.unpack("!IQ", body[:12]) 117 return cls(msg_id, arrival_time) 118 119 120class DataMessage(I2NPMessage): 121 """Type 20: length(4) + payload.""" 122 123 TYPE = 20 124 125 def __init__(self, payload: bytes): 126 self.payload = payload 127 self._header_msg_id = 0 128 self._header_expiration = 0 129 130 def body_bytes(self) -> bytes: 131 return struct.pack("!I", len(self.payload)) + self.payload 132 133 @classmethod 134 def _from_body(cls, body: bytes) -> "DataMessage": 135 length = struct.unpack("!I", body[:4])[0] 136 return cls(body[4:4 + length]) 137 138 139class DatabaseStoreMessage(I2NPMessage): 140 """Type 1: key(32) + type(1) + reply_token(4) + data.""" 141 142 TYPE = 1 143 144 def __init__(self, key: bytes, ds_type: int, reply_token: int, data: bytes): 145 self.key = key 146 self.ds_type = ds_type 147 self.reply_token = reply_token 148 self.data = data 149 self._header_msg_id = 0 150 self._header_expiration = 0 151 152 def body_bytes(self) -> bytes: 153 return self.key + struct.pack("!BI", self.ds_type, self.reply_token) + self.data 154 155 @classmethod 156 def _from_body(cls, body: bytes) -> "DatabaseStoreMessage": 157 key = body[:32] 158 ds_type = body[32] 159 reply_token = struct.unpack("!I", body[33:37])[0] 160 data = body[37:] 161 return cls(key, ds_type, reply_token, data) 162 163 164class DatabaseLookupMessage(I2NPMessage): 165 """Type 2: key(32) + from_hash(32) + flags(1) + [reply_tunnel_id(4)] + size(1) + exclude_list.""" 166 167 TYPE = 2 168 169 def __init__(self, key: bytes, from_hash: bytes, flags: int, 170 reply_tunnel_id: int = 0, exclude_list: list[bytes] | None = None): 171 self.key = key 172 self.from_hash = from_hash 173 self.flags = flags 174 self.reply_tunnel_id = reply_tunnel_id 175 self.exclude_list = exclude_list or [] 176 self._header_msg_id = 0 177 self._header_expiration = 0 178 179 def body_bytes(self) -> bytes: 180 parts = [self.key, self.from_hash, struct.pack("!B", self.flags)] 181 if self.flags & 0x01: 182 parts.append(struct.pack("!I", self.reply_tunnel_id)) 183 parts.append(struct.pack("!B", len(self.exclude_list))) 184 for ex in self.exclude_list: 185 parts.append(ex) 186 return b"".join(parts) 187 188 @classmethod 189 def _from_body(cls, body: bytes) -> "DatabaseLookupMessage": 190 key = body[:32] 191 from_hash = body[32:64] 192 flags = body[64] 193 offset = 65 194 reply_tunnel_id = 0 195 if flags & 0x01: 196 reply_tunnel_id = struct.unpack("!I", body[offset:offset + 4])[0] 197 offset += 4 198 num_excludes = body[offset] 199 offset += 1 200 exclude_list = [] 201 for _ in range(num_excludes): 202 exclude_list.append(body[offset:offset + 32]) 203 offset += 32 204 return cls(key, from_hash, flags, reply_tunnel_id, exclude_list)