Spaces:
Sleeping
Sleeping
| import asyncio | |
| import os | |
| import signal | |
| import sys | |
| from urllib.parse import urlparse | |
| PROXY_PORT = 7860 | |
| async def health_check(writer): | |
| writer.write(b"HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nOK\n") | |
| await writer.drain() | |
| writer.close() | |
| async def error(writer, status, message): | |
| body = message.encode() | |
| writer.write(f"HTTP/1.1 {status} {message}\r\nContent-Length: {len(body)}\r\n\r\n{message}\n".encode()) | |
| await writer.drain() | |
| writer.close() | |
| async def forward(src, dst): | |
| try: | |
| while True: | |
| data = await src.read(65536) | |
| if not data: | |
| break | |
| dst.write(data) | |
| await dst.drain() | |
| except (ConnectionResetError, BrokenPipeError, OSError): | |
| pass | |
| finally: | |
| try: | |
| dst.close() | |
| except OSError: | |
| pass | |
| async def handle_tunnel(target_host, target_port, reader, writer): | |
| try: | |
| backend_reader, backend_writer = await asyncio.wait_for( | |
| asyncio.open_connection(target_host, target_port), timeout=15 | |
| ) | |
| except (OSError, asyncio.TimeoutError): | |
| await error(writer, 502, "Cannot connect to target") | |
| return | |
| writer.write(b"HTTP/1.1 200 Connection Established\r\n\r\n") | |
| await writer.drain() | |
| await asyncio.gather( | |
| forward(reader, backend_writer), | |
| forward(backend_reader, writer), | |
| ) | |
| async def forward_request(host, port, data, reader, writer): | |
| try: | |
| backend_reader, backend_writer = await asyncio.wait_for( | |
| asyncio.open_connection(host, port), timeout=15 | |
| ) | |
| except (OSError, asyncio.TimeoutError): | |
| await error(writer, 502, "Cannot connect to target") | |
| return | |
| backend_writer.write(data) | |
| await backend_writer.drain() | |
| await asyncio.gather( | |
| forward(reader, backend_writer), | |
| forward(backend_reader, writer), | |
| ) | |
| async def handle_client(reader, writer): | |
| try: | |
| data = await asyncio.wait_for(reader.read(4096), timeout=10) | |
| if not data: | |
| writer.close() | |
| return | |
| parts = data.split(b" ", 2) | |
| if len(parts) < 2: | |
| writer.close() | |
| return | |
| method = parts[0] | |
| raw_path = parts[1] | |
| if method == b"CONNECT": | |
| target = raw_path.split(b":") | |
| if len(target) == 2: | |
| try: | |
| await handle_tunnel(target[0].decode(), int(target[1]), reader, writer) | |
| return | |
| except (ValueError, UnicodeDecodeError): | |
| pass | |
| path = raw_path.split(b"?")[0] | |
| if path in (b"/", b"/health"): | |
| await health_check(writer) | |
| return | |
| if raw_path.startswith(b"http://") or raw_path.startswith(b"https://"): | |
| parsed = urlparse(raw_path.decode()) | |
| host = parsed.hostname | |
| port = parsed.port or (443 if parsed.scheme == "https" else 80) | |
| if host: | |
| await forward_request(host, port, data, reader, writer) | |
| return | |
| await error(writer, 400, "Bad Request") | |
| except (ConnectionResetError, BrokenPipeError, OSError, asyncio.TimeoutError): | |
| pass | |
| finally: | |
| try: | |
| writer.close() | |
| except OSError: | |
| pass | |
| async def main(): | |
| print(f"starting proxy on port {PROXY_PORT}...", flush=True) | |
| server = await asyncio.start_server(handle_client, "0.0.0.0", PROXY_PORT) | |
| async def shutdown(): | |
| server.close() | |
| loop = asyncio.get_event_loop() | |
| for sig in (signal.SIGTERM, signal.SIGINT): | |
| try: | |
| loop.add_signal_handler(sig, lambda: asyncio.create_task(shutdown())) | |
| except NotImplementedError: | |
| signal.signal(sig, lambda n, f: asyncio.run_coroutine_threadsafe(shutdown(), loop)) | |
| async with server: | |
| print(f"proxy server ready on port {PROXY_PORT}", flush=True) | |
| await server.serve_forever() | |
| if __name__ == "__main__": | |
| asyncio.run(main()) | |