A Python port of the Invisible Internet Project (I2P)
at main 102 lines 3.6 kB view raw
1"""CONNECT-only proxy tunnel — dedicated HTTPS tunneling via I2P. 2 3Simpler than HTTPClientTask: only accepts CONNECT method, strips ALL headers, 4no keepalive, no address helpers. 5 6Ported from net.i2p.i2ptunnel.I2PTunnelConnectClient. 7""" 8 9from __future__ import annotations 10 11import logging 12 13from i2p_apps.i2ptunnel.forwarder import bridge 14from i2p_apps.i2ptunnel.http_proxy import parse_http_request, extract_i2p_destination 15from i2p_apps.i2ptunnel.tasks import ClientTunnelTask 16 17logger = logging.getLogger(__name__) 18 19 20class ConnectClientTask(ClientTunnelTask): 21 """CONNECT-only proxy for HTTPS tunneling through I2P. 22 23 Only accepts CONNECT method (returns 405 for GET/POST). 24 ALL headers are discarded before tunneling for privacy. 25 """ 26 27 @property 28 def _is_connect_only(self) -> bool: 29 return True 30 31 @staticmethod 32 def _strip_all_headers(headers: dict[str, str]) -> dict[str, str]: 33 """Strip all headers — returns empty dict.""" 34 return {} 35 36 async def _consume_headers(self, reader) -> dict[str, str]: 37 """Read and discard all headers.""" 38 headers: dict[str, str] = {} 39 while True: 40 line = await reader.readline() 41 if not line or line == b"\r\n" or line == b"\n": 42 break 43 text = line.decode("utf-8", errors="replace").strip() 44 if ":" in text: 45 key, _, value = text.partition(":") 46 headers[key.strip()] = value.strip() 47 return headers 48 49 async def _resolve(self, hostname: str) -> str | None: 50 """Resolve an I2P hostname.""" 51 if hostname.endswith(".b32.i2p"): 52 return hostname 53 if hostname.endswith(".i2p"): 54 return await self._session.lookup(hostname) 55 return hostname 56 57 async def handle_client(self, reader, writer) -> None: 58 try: 59 first_line_bytes = await reader.readline() 60 if not first_line_bytes: 61 return 62 63 first_line = first_line_bytes.decode("utf-8", errors="replace").strip() 64 try: 65 request = parse_http_request(first_line) 66 except ValueError: 67 writer.write(b"HTTP/1.1 400 Bad Request\r\n\r\n") 68 await writer.drain() 69 return 70 71 # Consume and discard all headers 72 await self._consume_headers(reader) 73 74 if not request.is_connect: 75 writer.write(b"HTTP/1.1 405 Method Not Allowed\r\n\r\n") 76 await writer.drain() 77 return 78 79 dest_host = extract_i2p_destination(request.host) 80 if dest_host: 81 resolved = await self._resolve(dest_host) 82 if resolved is None: 83 writer.write(b"HTTP/1.1 503 Destination Not Found\r\n\r\n") 84 await writer.drain() 85 return 86 87 try: 88 remote_reader, remote_writer = await self._session.connect(resolved) 89 except Exception: 90 writer.write(b"HTTP/1.1 504 Connection Timeout\r\n\r\n") 91 await writer.drain() 92 return 93 94 writer.write(b"HTTP/1.1 200 Connection Established\r\n\r\n") 95 await writer.drain() 96 await bridge(reader, writer, remote_reader, remote_writer) 97 else: 98 writer.write(b"HTTP/1.1 503 No Outproxy\r\n\r\n") 99 await writer.drain() 100 101 except Exception: 102 logger.exception("Error in CONNECT proxy handler")