"""CONNECT-only proxy tunnel — dedicated HTTPS tunneling via I2P. Simpler than HTTPClientTask: only accepts CONNECT method, strips ALL headers, no keepalive, no address helpers. Ported from net.i2p.i2ptunnel.I2PTunnelConnectClient. """ from __future__ import annotations import logging from i2p_apps.i2ptunnel.forwarder import bridge from i2p_apps.i2ptunnel.http_proxy import parse_http_request, extract_i2p_destination from i2p_apps.i2ptunnel.tasks import ClientTunnelTask logger = logging.getLogger(__name__) class ConnectClientTask(ClientTunnelTask): """CONNECT-only proxy for HTTPS tunneling through I2P. Only accepts CONNECT method (returns 405 for GET/POST). ALL headers are discarded before tunneling for privacy. """ @property def _is_connect_only(self) -> bool: return True @staticmethod def _strip_all_headers(headers: dict[str, str]) -> dict[str, str]: """Strip all headers — returns empty dict.""" return {} async def _consume_headers(self, reader) -> dict[str, str]: """Read and discard all headers.""" headers: dict[str, str] = {} while True: line = await reader.readline() if not line or line == b"\r\n" or line == b"\n": break text = line.decode("utf-8", errors="replace").strip() if ":" in text: key, _, value = text.partition(":") headers[key.strip()] = value.strip() return headers async def _resolve(self, hostname: str) -> str | None: """Resolve an I2P hostname.""" if hostname.endswith(".b32.i2p"): return hostname if hostname.endswith(".i2p"): return await self._session.lookup(hostname) return hostname async def handle_client(self, reader, writer) -> None: try: first_line_bytes = await reader.readline() if not first_line_bytes: return first_line = first_line_bytes.decode("utf-8", errors="replace").strip() try: request = parse_http_request(first_line) except ValueError: writer.write(b"HTTP/1.1 400 Bad Request\r\n\r\n") await writer.drain() return # Consume and discard all headers await self._consume_headers(reader) if not request.is_connect: writer.write(b"HTTP/1.1 405 Method Not Allowed\r\n\r\n") await writer.drain() return dest_host = extract_i2p_destination(request.host) if dest_host: resolved = await self._resolve(dest_host) if resolved is None: writer.write(b"HTTP/1.1 503 Destination Not Found\r\n\r\n") await writer.drain() return try: remote_reader, remote_writer = await self._session.connect(resolved) except Exception: writer.write(b"HTTP/1.1 504 Connection Timeout\r\n\r\n") await writer.drain() return writer.write(b"HTTP/1.1 200 Connection Established\r\n\r\n") await writer.drain() await bridge(reader, writer, remote_reader, remote_writer) else: writer.write(b"HTTP/1.1 503 No Outproxy\r\n\r\n") await writer.drain() except Exception: logger.exception("Error in CONNECT proxy handler")