A Python port of the Invisible Internet Project (I2P)
at main 248 lines 9.0 kB view raw
1"""Tests for I2CP Tier 2: wire format fixes + new message types.""" 2 3import struct 4import os 5import pytest 6 7from i2p_client.i2cp_messages import ( 8 SendMessageMessage, 9 HostLookupMessage, 10 HostReplyMessage, 11 ReconfigureSessionMessage, 12 SendMessageExpiresMessage, 13 DateAndFlags, 14) 15from i2p_client.session_config import WireSessionConfig 16 17 18def _make_dest_data() -> bytes: 19 """Create valid-looking destination data (387 bytes with null cert).""" 20 # 256 bytes pub key area + 128 bytes sig key area + null cert (type=0, len=0) 21 return os.urandom(384) + b"\x00\x00\x00" 22 23 24# --- SendMessageMessage wire format fix --- 25 26class TestSendMessageMessageFixed: 27 def test_roundtrip_with_nonce(self): 28 dest = _make_dest_data() # typical destination size 29 payload = b"hello" 30 msg = SendMessageMessage(session_id=1, destination_data=dest, 31 payload=payload, nonce=12345) 32 wire = msg.payload_bytes() 33 msg2 = SendMessageMessage._from_payload(wire) 34 assert msg2.session_id == 1 35 assert msg2.destination_data == dest 36 assert msg2.payload == payload 37 assert msg2.nonce == 12345 38 39 def test_no_dest_len_prefix(self): 40 """Wire format should NOT have a 2-byte dest_len prefix.""" 41 dest = _make_dest_data() 42 msg = SendMessageMessage(session_id=5, destination_data=dest, 43 payload=b"x", nonce=0) 44 wire = msg.payload_bytes() 45 # First 2 bytes: session_id 46 sid = struct.unpack("!H", wire[:2])[0] 47 assert sid == 5 48 # Next 2 bytes should NOT be dest_len (390) — they should be part of dest 49 # In old format, bytes 2-4 would be dest_len=390 (0x0186) 50 # In new format, bytes 2+ are raw destination bytes 51 assert wire[2:2+len(dest)] == dest 52 53 def test_nonce_at_end(self): 54 dest = _make_dest_data() 55 msg = SendMessageMessage(session_id=1, destination_data=dest, 56 payload=b"test", nonce=42) 57 wire = msg.payload_bytes() 58 # Last 4 bytes should be nonce 59 nonce = struct.unpack("!I", wire[-4:])[0] 60 assert nonce == 42 61 62 def test_default_nonce_zero(self): 63 """Backward compat: nonce defaults to 0.""" 64 msg = SendMessageMessage(session_id=1, destination_data=_make_dest_data(), 65 payload=b"data") 66 assert msg.nonce == 0 67 68 69# --- SessionConfig --- 70 71class TestWireSessionConfig: 72 def test_roundtrip(self): 73 dest_data = _make_dest_data() 74 opts = {"inbound.length": "3", "outbound.length": "3"} 75 sc = WireSessionConfig(dest_data, options=opts) 76 wire = sc.to_bytes() 77 sc2 = WireSessionConfig.from_bytes(wire) 78 assert sc2.destination_data == dest_data 79 assert sc2.options == opts 80 81 def test_date_field(self): 82 dest_data = _make_dest_data() 83 sc = WireSessionConfig(dest_data, date_ms=1700000000000) 84 wire = sc.to_bytes() 85 sc2 = WireSessionConfig.from_bytes(wire) 86 assert sc2.date_ms == 1700000000000 87 88 def test_empty_options(self): 89 dest_data = _make_dest_data() 90 sc = WireSessionConfig(dest_data) 91 wire = sc.to_bytes() 92 sc2 = WireSessionConfig.from_bytes(wire) 93 assert sc2.options == {} 94 95 def test_signature_field(self): 96 dest_data = _make_dest_data() 97 sig = os.urandom(64) 98 sc = WireSessionConfig(dest_data, signature=sig) 99 wire = sc.to_bytes() 100 sc2 = WireSessionConfig.from_bytes(wire) 101 assert sc2.signature == sig 102 103 104# --- HostLookupMessage wire format fix --- 105 106class TestHostLookupMessageFixed: 107 def test_roundtrip_by_name(self): 108 msg = HostLookupMessage(session_id=1, request_id=100, 109 hostname="example.i2p", timeout=15000) 110 wire = msg.payload_bytes() 111 msg2 = HostLookupMessage._from_payload(wire) 112 assert msg2.session_id == 1 113 assert msg2.request_id == 100 114 assert msg2.hostname == "example.i2p" 115 assert msg2.timeout == 15000 116 117 def test_roundtrip_by_hash(self): 118 h = os.urandom(32) 119 msg = HostLookupMessage(session_id=2, request_id=200, 120 dest_hash=h, timeout=5000) 121 wire = msg.payload_bytes() 122 msg2 = HostLookupMessage._from_payload(wire) 123 assert msg2.dest_hash == h 124 assert msg2.timeout == 5000 125 126 def test_timeout_field_position(self): 127 """Timeout should be at offset 6 (after session_id+request_id).""" 128 msg = HostLookupMessage(session_id=0, request_id=0, 129 dest_hash=os.urandom(32), timeout=9999) 130 wire = msg.payload_bytes() 131 timeout = struct.unpack("!I", wire[6:10])[0] 132 assert timeout == 9999 133 134 def test_type_codes_match_java(self): 135 """Java: 0=hash lookup, 1=host lookup.""" 136 # Hash lookup 137 h = os.urandom(32) 138 msg = HostLookupMessage(session_id=0, request_id=0, 139 dest_hash=h, timeout=10000) 140 wire = msg.payload_bytes() 141 lookup_type = wire[10] # after session_id(2)+req_id(4)+timeout(4) 142 assert lookup_type == HostLookupMessage.LOOKUP_HASH # 0 143 144 # Host lookup 145 msg2 = HostLookupMessage(session_id=0, request_id=0, 146 hostname="test.i2p", timeout=10000) 147 wire2 = msg2.payload_bytes() 148 lookup_type2 = wire2[10] 149 assert lookup_type2 == HostLookupMessage.LOOKUP_HOST # 1 150 151 152# --- HostReplyMessage wire format fix --- 153 154class TestHostReplyMessageFixed: 155 def test_no_dest_len_in_wire(self): 156 """Success reply should NOT have 2-byte dest_len prefix.""" 157 dest = _make_dest_data() 158 msg = HostReplyMessage(session_id=1, request_id=100, 159 result_code=0, destination_data=dest) 160 wire = msg.payload_bytes() 161 # After header (2+4+1=7 bytes), dest data starts directly 162 assert wire[7:7+len(dest)] == dest 163 164 def test_all_result_codes(self): 165 for code in range(6): 166 msg = HostReplyMessage(session_id=1, request_id=1, result_code=code) 167 wire = msg.payload_bytes() 168 msg2 = HostReplyMessage._from_payload(wire) 169 assert msg2.result_code == code 170 171 def test_roundtrip_success_with_dest(self): 172 dest = _make_dest_data() 173 msg = HostReplyMessage(session_id=5, request_id=42, 174 result_code=0, destination_data=dest) 175 wire = msg.payload_bytes() 176 msg2 = HostReplyMessage._from_payload(wire) 177 assert msg2.destination_data == dest 178 assert msg2.result_code == 0 179 180 181# --- ReconfigureSessionMessage --- 182 183class TestReconfigureSessionMessage: 184 def test_roundtrip(self): 185 sc = WireSessionConfig(_make_dest_data(), options={"foo": "bar"}) 186 msg = ReconfigureSessionMessage(session_id=7, session_config=sc) 187 wire = msg.payload_bytes() 188 msg2 = ReconfigureSessionMessage._from_payload(wire) 189 assert msg2.session_id == 7 190 assert msg2.session_config.options == {"foo": "bar"} 191 192 def test_type_code(self): 193 assert ReconfigureSessionMessage.TYPE == 2 194 195 196# --- DateAndFlags --- 197 198class TestDateAndFlags: 199 def test_roundtrip(self): 200 df = DateAndFlags(date_ms=1700000000000, flags=0x0001) 201 raw = df.to_bytes() 202 assert len(raw) == 8 203 df2 = DateAndFlags.from_bytes(raw) 204 assert df2.date_ms == 1700000000000 205 assert df2.flags == 0x0001 206 207 def test_zero_flags(self): 208 df = DateAndFlags(date_ms=5000, flags=0) 209 raw = df.to_bytes() 210 df2 = DateAndFlags.from_bytes(raw) 211 assert df2.date_ms == 5000 212 assert df2.flags == 0 213 214 215# --- SendMessageExpiresMessage --- 216 217class TestSendMessageExpiresMessage: 218 def test_roundtrip(self): 219 dest = _make_dest_data() 220 msg = SendMessageExpiresMessage( 221 session_id=3, destination_data=dest, 222 payload=b"data", nonce=99, 223 expiration=DateAndFlags(date_ms=1700000000000, flags=0x0003), 224 ) 225 wire = msg.payload_bytes() 226 msg2 = SendMessageExpiresMessage._from_payload(wire) 227 assert msg2.session_id == 3 228 assert msg2.destination_data == dest 229 assert msg2.payload == b"data" 230 assert msg2.nonce == 99 231 assert msg2.expiration.date_ms == 1700000000000 232 assert msg2.expiration.flags == 0x0003 233 234 def test_type_code(self): 235 assert SendMessageExpiresMessage.TYPE == 36 236 237 def test_expiration_at_end(self): 238 dest = _make_dest_data() 239 msg = SendMessageExpiresMessage( 240 session_id=1, destination_data=dest, 241 payload=b"x", nonce=0, 242 expiration=DateAndFlags(date_ms=42, flags=0), 243 ) 244 wire = msg.payload_bytes() 245 # Last 8 bytes are expiration 246 exp_raw = wire[-8:] 247 df = DateAndFlags.from_bytes(exp_raw) 248 assert df.date_ms == 42