A Python port of the Invisible Internet Project (I2P)
at main 122 lines 3.9 kB view raw
1"""Tests for SOCKSProxy server — _handle_client and start/stop.""" 2 3import asyncio 4from unittest.mock import AsyncMock, MagicMock 5 6import pytest 7 8from i2p_apps.i2ptunnel.socks_proxy import ( 9 SOCKSProxy, parse_socks_greeting, parse_socks_request, 10 build_socks_greeting_reply, build_socks_reply, 11 SOCKS_VERSION, AUTH_NONE, REPLY_HOST_UNREACHABLE, 12) 13 14 15def _mock_writer(): 16 writer = MagicMock() 17 writer.write = MagicMock() 18 writer.drain = AsyncMock() 19 writer.close = MagicMock() 20 writer.wait_closed = AsyncMock() 21 return writer 22 23 24class TestSOCKSProxyProperties: 25 def test_defaults(self): 26 proxy = SOCKSProxy() 27 assert proxy.listen_address == ("127.0.0.1", 4445) 28 assert proxy.is_running is False 29 30 def test_custom(self): 31 proxy = SOCKSProxy("0.0.0.0", 9050) 32 assert proxy.listen_address == ("0.0.0.0", 9050) 33 34 @pytest.mark.asyncio 35 async def test_start_stop(self): 36 proxy = SOCKSProxy("127.0.0.1", 0) 37 await proxy.start() 38 assert proxy.is_running is True 39 await proxy.stop() 40 assert proxy.is_running is False 41 42 @pytest.mark.asyncio 43 async def test_stop_without_start(self): 44 proxy = SOCKSProxy() 45 await proxy.stop() 46 assert proxy.is_running is False 47 48 49class TestSOCKSProxyHandleClient: 50 @pytest.mark.asyncio 51 async def test_valid_connect(self): 52 """Full SOCKS5 handshake: greeting → reply → request → host unreachable.""" 53 proxy = SOCKSProxy() 54 55 # Build greeting: VER=5, NMETHODS=1, METHODS=[0x00] 56 greeting = bytes([SOCKS_VERSION, 1, AUTH_NONE]) 57 # Build request: VER=5, CMD=1 (CONNECT), RSV=0, ATYP=3 (domain) 58 domain = b"example.i2p" 59 request = bytes([SOCKS_VERSION, 1, 0, 3, len(domain)]) + domain + b"\x01\xbb" # port 443 60 61 read_count = 0 62 async def mock_read(n): 63 nonlocal read_count 64 read_count += 1 65 if read_count == 1: 66 return greeting 67 return request 68 69 reader = AsyncMock() 70 reader.read = mock_read 71 writer = _mock_writer() 72 73 await proxy._handle_client(reader, writer) 74 75 # Should have sent greeting reply then SOCKS reply 76 calls = writer.write.call_args_list 77 assert len(calls) == 2 78 # First call: greeting reply 79 assert calls[0][0][0] == build_socks_greeting_reply(AUTH_NONE) 80 # Second call: host unreachable reply 81 assert calls[1][0][0] == build_socks_reply(REPLY_HOST_UNREACHABLE) 82 83 @pytest.mark.asyncio 84 async def test_no_acceptable_auth(self): 85 """Client offers only auth methods we don't support → 0xFF reply.""" 86 proxy = SOCKSProxy() 87 88 greeting = bytes([SOCKS_VERSION, 1, 0x02]) # only USERNAME/PASSWORD 89 90 reader = AsyncMock() 91 reader.read = AsyncMock(return_value=greeting) 92 writer = _mock_writer() 93 94 await proxy._handle_client(reader, writer) 95 96 calls = writer.write.call_args_list 97 assert len(calls) == 1 98 assert calls[0][0][0] == build_socks_greeting_reply(0xFF) 99 writer.close.assert_called() 100 101 @pytest.mark.asyncio 102 async def test_exception_closes_writer(self): 103 proxy = SOCKSProxy() 104 105 reader = AsyncMock() 106 reader.read = AsyncMock(side_effect=ConnectionResetError("reset")) 107 writer = _mock_writer() 108 109 await proxy._handle_client(reader, writer) 110 writer.close.assert_called() 111 112 @pytest.mark.asyncio 113 async def test_invalid_greeting(self): 114 """Too-short greeting → ValueError → exception path.""" 115 proxy = SOCKSProxy() 116 117 reader = AsyncMock() 118 reader.read = AsyncMock(return_value=b"\x04") # SOCKS4, not 5 119 writer = _mock_writer() 120 121 await proxy._handle_client(reader, writer) 122 writer.close.assert_called()