Spaces:
Paused
Paused
| """ | |
| WARP SOCKS5 Proxy Space - Cloudflare WARP VPN in Docker | |
| This Space runs Cloudflare WARP as a SOCKS5 proxy on port 40000. | |
| It also provides an HTTP forwarding endpoint so other HF Spaces can | |
| route requests through WARP without needing SOCKS5 support. | |
| Endpoints: | |
| GET / - Health check + WARP status | |
| GET /ping - Simple health check | |
| GET /status - WARP connection status | |
| GET /proxy - HTTP proxy: ?url=<target_url> (forwards request through WARP) | |
| POST /proxy - HTTP proxy: body={"url": "...", "method": "GET", "headers": {}} | |
| GET /download - Download a file through WARP: ?url=<file_url>&output=json | |
| """ | |
| import asyncio | |
| import os | |
| import subprocess | |
| import time | |
| from pathlib import Path | |
| import httpx | |
| from fastapi import FastAPI, Query, Request | |
| from fastapi.responses import JSONResponse, Response, StreamingResponse | |
| app = FastAPI(title="WARP SOCKS5 Proxy", version="2.0") | |
| WARP_SOCKS5 = "socks5://127.0.0.1:40000" | |
| _warp_ready = False | |
| _warp_start_time = 0 | |
| def is_warp_connected() -> bool: | |
| """Check if WARP is connected.""" | |
| try: | |
| result = subprocess.run( | |
| ["warp-cli", "--accept-tos", "status"], | |
| capture_output=True, text=True, timeout=5 | |
| ) | |
| return "Connected" in result.stdout | |
| except Exception: | |
| return False | |
| async def wait_for_warp(max_seconds: int = 30): | |
| """Wait for WARP to connect.""" | |
| global _warp_ready | |
| for i in range(max_seconds): | |
| if is_warp_connected(): | |
| _warp_ready = True | |
| print(f"[WARP] Connected after {i+1}s") | |
| return True | |
| await asyncio.sleep(1) | |
| print(f"[WARP] Failed to connect after {max_seconds}s") | |
| return False | |
| async def startup(): | |
| """Start WARP daemon and connect.""" | |
| global _warp_start_time | |
| _warp_start_time = time.time() | |
| print("[STARTUP] WARP Proxy Space starting...") | |
| # warp-svc should already be running from entrypoint.sh | |
| # Just need to register and connect | |
| print("[STARTUP] Registering WARP...") | |
| subprocess.run(["warp-cli", "--accept-tos", "registration", "new"], | |
| capture_output=True, timeout=10) | |
| print("[STARTUP] Setting proxy mode...") | |
| subprocess.run(["warp-cli", "--accept-tos", "mode", "proxy"], | |
| capture_output=True, timeout=10) | |
| print("[STARTUP] Connecting WARP...") | |
| subprocess.run(["warp-cli", "--accept-tos", "connect"], | |
| capture_output=True, timeout=10) | |
| # Wait for connection in background | |
| asyncio.create_task(wait_for_warp(60)) | |
| print("[STARTUP] WARP Proxy Space ready!") | |
| async def root(): | |
| """Health check with WARP status.""" | |
| connected = is_warp_connected() | |
| uptime = int(time.time() - _warp_start_time) if _warp_start_time else 0 | |
| return { | |
| "status": "ok" if connected else "starting", | |
| "warp_connected": connected, | |
| "socks5_proxy": WARP_SOCKS5 if connected else None, | |
| "uptime_seconds": uptime, | |
| "usage": { | |
| "http_proxy": "/proxy?url=<target_url>", | |
| "download": "/download?url=<file_url>", | |
| "socks5": WARP_SOCKS5, | |
| } | |
| } | |
| async def ping(): | |
| """Simple health check.""" | |
| connected = is_warp_connected() | |
| return {"status": "alive", "warp_connected": connected} | |
| async def status(): | |
| """Get WARP connection status.""" | |
| try: | |
| result = subprocess.run( | |
| ["warp-cli", "--accept-tos", "status"], | |
| capture_output=True, text=True, timeout=5 | |
| ) | |
| return {"raw": result.stdout.strip(), "connected": "Connected" in result.stdout} | |
| except Exception as e: | |
| return {"raw": str(e), "connected": False} | |
| async def proxy_get(url: str = Query(..., description="Target URL to proxy")): | |
| """Forward a GET request through WARP SOCKS5 proxy.""" | |
| return await _forward_request(url, "GET") | |
| async def proxy_post(request: Request): | |
| """Forward a POST request through WARP SOCKS5 proxy. | |
| Body: {"url": "...", "method": "GET", "headers": {}, "body": "..."} | |
| """ | |
| try: | |
| body = await request.json() | |
| target_url = body.get("url", "") | |
| method = body.get("method", "GET").upper() | |
| headers = body.get("headers", {}) | |
| req_body = body.get("body") | |
| return await _forward_request(target_url, method, headers, req_body) | |
| except Exception as e: | |
| return JSONResponse({"error": str(e)}, status_code=400) | |
| async def _forward_request(url: str, method: str = "GET", | |
| headers: dict = None, body=None, | |
| follow_redirects: bool = True, | |
| timeout: float = 60.0): | |
| """Forward a request through the WARP SOCKS5 proxy.""" | |
| if not _warp_ready and not is_warp_connected(): | |
| # Try to wait a bit | |
| connected = await wait_for_warp(10) | |
| if not connected: | |
| return JSONResponse( | |
| {"error": "WARP not connected", "status": "starting"}, | |
| status_code=503 | |
| ) | |
| try: | |
| async with httpx.AsyncClient( | |
| proxy=WARP_SOCKS5, | |
| follow_redirects=follow_redirects, | |
| timeout=timeout, | |
| ) as client: | |
| # Filter out hop-by-hop headers | |
| safe_headers = {} | |
| if headers: | |
| skip = {"host", "connection", "keep-alive", "transfer-encoding", | |
| "te", "trailer", "upgrade", "proxy-authorization"} | |
| for k, v in headers.items(): | |
| if k.lower() not in skip: | |
| safe_headers[k] = v | |
| resp = await client.request( | |
| method=method, | |
| url=url, | |
| headers=safe_headers, | |
| content=body if isinstance(body, bytes) else (body.encode() if isinstance(body, str) else None), | |
| ) | |
| # Return the response | |
| return Response( | |
| content=resp.content, | |
| status_code=resp.status_code, | |
| headers=dict(resp.headers), | |
| ) | |
| except httpx.ConnectError as e: | |
| return JSONResponse( | |
| {"error": f"WARP proxy connection failed: {e}"}, | |
| status_code=502 | |
| ) | |
| except httpx.TimeoutException: | |
| return JSONResponse( | |
| {"error": "Request timed out through WARP proxy"}, | |
| status_code=504 | |
| ) | |
| except Exception as e: | |
| return JSONResponse( | |
| {"error": f"Proxy error: {e}"}, | |
| status_code=500 | |
| ) | |
| async def download_file(url: str = Query(..., description="File URL to download through WARP")): | |
| """Download a file through WARP and stream it back. | |
| Query params: | |
| url: The file URL to download | |
| output: "stream" (default) or "json" (returns base64) | |
| """ | |
| if not _warp_ready and not is_warp_connected(): | |
| connected = await wait_for_warp(10) | |
| if not connected: | |
| return JSONResponse( | |
| {"error": "WARP not connected"}, | |
| status_code=503 | |
| ) | |
| try: | |
| async with httpx.AsyncClient( | |
| proxy=WARP_SOCKS5, | |
| follow_redirects=True, | |
| timeout=120.0, | |
| ) as client: | |
| async with client.stream("GET", url) as resp: | |
| if resp.status_code != 200: | |
| return JSONResponse( | |
| {"error": f"Upstream returned {resp.status_code}"}, | |
| status_code=resp.status_code | |
| ) | |
| content_type = resp.headers.get("content-type", "application/octet-stream") | |
| return StreamingResponse( | |
| resp.aiter_bytes(chunk_size=65536), | |
| media_type=content_type, | |
| headers={ | |
| "Content-Disposition": f'attachment; filename="{url.split("/")[-1].split("?")[0]}"', | |
| } | |
| ) | |
| except Exception as e: | |
| return JSONResponse( | |
| {"error": f"Download failed: {e}"}, | |
| status_code=500 | |
| ) | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) | |