TomatitoToho commited on
Commit
5d9c346
·
verified ·
1 Parent(s): 1b4dc0f

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +219 -67
app.py CHANGED
@@ -1,101 +1,253 @@
1
- """Cloudflare WARP SOCKS5 Proxy Server with HTTP CONNECT forwarding
 
 
 
 
 
2
 
3
- Exposes WARP as an HTTP proxy that other HF Spaces can use.
4
- Routes: /proxy?url=<url> - fetches URL through WARP VPN
5
- /connect - HTTP CONNECT tunnel (for YouTube API)
 
 
 
 
6
  """
 
 
7
  import os
8
  import subprocess
9
- import asyncio
 
 
10
  import httpx
11
- from fastapi import FastAPI, Request
12
  from fastapi.responses import JSONResponse, Response, StreamingResponse
13
 
14
  app = FastAPI(title="WARP SOCKS5 Proxy", version="2.0")
15
 
16
- WARP_CONNECTED = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  @app.on_event("startup")
19
  async def startup():
20
- global WARP_CONNECTED
21
- print("[WARP-PROXY] Starting WARP daemon...")
22
- subprocess.Popen(["warp-svc"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
23
- asyncio.create_task(_connect_warp())
24
-
25
- async def _connect_warp():
26
- global WARP_CONNECTED
27
- await asyncio.sleep(3)
28
- for attempt in range(5):
29
- try:
30
- subprocess.run(["warp-cli", "--accept-tos", "registration", "new"], capture_output=True, timeout=10)
31
- subprocess.run(["warp-cli", "--accept-tos", "mode", "proxy"], capture_output=True, timeout=10)
32
- subprocess.run(["warp-cli", "--accept-tos", "connect"], capture_output=True, timeout=10)
33
- for i in range(30):
34
- result = subprocess.run(["warp-cli", "--accept-tos", "status"], capture_output=True, text=True, timeout=5)
35
- if "connected" in result.stdout.lower():
36
- print(f"[WARP-PROXY] CONNECTED! VPN active.")
37
- WARP_CONNECTED = True
38
- return
39
- await asyncio.sleep(2)
40
- except Exception as e:
41
- print(f"[WARP-PROXY] Attempt {attempt+1} error: {e}")
42
- await asyncio.sleep(3)
43
- print("[WARP-PROXY] Failed - running without VPN")
 
44
 
45
  @app.get("/")
46
  async def root():
47
- return {"status": "ok" if WARP_CONNECTED else "connecting", "warp_connected": WARP_CONNECTED}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- @app.get("/health")
50
- async def health():
51
- return {"warp_connected": WARP_CONNECTED}
52
 
53
  @app.get("/proxy")
54
- async def proxy_get(url: str = ""):
55
- """Fetch a URL through the WARP VPN SOCKS5 proxy."""
56
- if not url:
57
- return JSONResponse({"error": "url parameter required"}, 400)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  try:
59
  async with httpx.AsyncClient(
60
- proxy="socks5://127.0.0.1:40000" if WARP_CONNECTED else None,
61
- timeout=30,
62
- follow_redirects=True
63
  ) as client:
64
- resp = await client.get(url)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  return Response(
66
  content=resp.content,
67
  status_code=resp.status_code,
68
- headers=dict(resp.headers)
69
  )
 
 
 
 
 
 
 
 
 
 
70
  except Exception as e:
71
- return JSONResponse({"error": str(e)}, 500)
 
 
 
72
 
73
- @app.post("/proxy")
74
- async def proxy_post(request: Request):
75
- """POST a request through the WARP VPN SOCKS5 proxy."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  try:
77
- body = await request.body()
78
- target_url = request.headers.get("X-Target-URL", "")
79
- if not target_url:
80
- return JSONResponse({"error": "X-Target-URL header required"}, 400)
81
-
82
- headers = {}
83
- for key, value in request.headers.items():
84
- if key.lower() not in ('host', 'x-target-url', 'content-length'):
85
- headers[key] = value
86
-
87
  async with httpx.AsyncClient(
88
- proxy="socks5://127.0.0.1:40000" if WARP_CONNECTED else None,
89
- timeout=60,
 
90
  ) as client:
91
- resp = await client.post(target_url, content=body, headers=headers)
92
- return Response(
93
- content=resp.content,
94
- status_code=resp.status_code,
95
- headers=dict(resp.headers)
96
- )
 
 
 
 
 
 
 
 
 
 
97
  except Exception as e:
98
- return JSONResponse({"error": str(e)}, 500)
 
 
 
 
99
 
100
  if __name__ == "__main__":
101
  import uvicorn
 
1
+ """
2
+ WARP SOCKS5 Proxy Space - Cloudflare WARP VPN in Docker
3
+
4
+ This Space runs Cloudflare WARP as a SOCKS5 proxy on port 40000.
5
+ It also provides an HTTP forwarding endpoint so other HF Spaces can
6
+ route requests through WARP without needing SOCKS5 support.
7
 
8
+ Endpoints:
9
+ GET / - Health check + WARP status
10
+ GET /ping - Simple health check
11
+ GET /status - WARP connection status
12
+ GET /proxy - HTTP proxy: ?url=<target_url> (forwards request through WARP)
13
+ POST /proxy - HTTP proxy: body={"url": "...", "method": "GET", "headers": {}}
14
+ GET /download - Download a file through WARP: ?url=<file_url>&output=json
15
  """
16
+
17
+ import asyncio
18
  import os
19
  import subprocess
20
+ import time
21
+ from pathlib import Path
22
+
23
  import httpx
24
+ from fastapi import FastAPI, Query, Request
25
  from fastapi.responses import JSONResponse, Response, StreamingResponse
26
 
27
  app = FastAPI(title="WARP SOCKS5 Proxy", version="2.0")
28
 
29
+ WARP_SOCKS5 = "socks5://127.0.0.1:40000"
30
+ _warp_ready = False
31
+ _warp_start_time = 0
32
+
33
+
34
+ def is_warp_connected() -> bool:
35
+ """Check if WARP is connected."""
36
+ try:
37
+ result = subprocess.run(
38
+ ["warp-cli", "--accept-tos", "status"],
39
+ capture_output=True, text=True, timeout=5
40
+ )
41
+ return "Connected" in result.stdout
42
+ except Exception:
43
+ return False
44
+
45
+
46
+ async def wait_for_warp(max_seconds: int = 30):
47
+ """Wait for WARP to connect."""
48
+ global _warp_ready
49
+ for i in range(max_seconds):
50
+ if is_warp_connected():
51
+ _warp_ready = True
52
+ print(f"[WARP] Connected after {i+1}s")
53
+ return True
54
+ await asyncio.sleep(1)
55
+ print(f"[WARP] Failed to connect after {max_seconds}s")
56
+ return False
57
+
58
 
59
  @app.on_event("startup")
60
  async def startup():
61
+ """Start WARP daemon and connect."""
62
+ global _warp_start_time
63
+ _warp_start_time = time.time()
64
+
65
+ print("[STARTUP] WARP Proxy Space starting...")
66
+
67
+ # warp-svc should already be running from entrypoint.sh
68
+ # Just need to register and connect
69
+ print("[STARTUP] Registering WARP...")
70
+ subprocess.run(["warp-cli", "--accept-tos", "registration", "new"],
71
+ capture_output=True, timeout=10)
72
+
73
+ print("[STARTUP] Setting proxy mode...")
74
+ subprocess.run(["warp-cli", "--accept-tos", "mode", "proxy"],
75
+ capture_output=True, timeout=10)
76
+
77
+ print("[STARTUP] Connecting WARP...")
78
+ subprocess.run(["warp-cli", "--accept-tos", "connect"],
79
+ capture_output=True, timeout=10)
80
+
81
+ # Wait for connection in background
82
+ asyncio.create_task(wait_for_warp(60))
83
+
84
+ print("[STARTUP] WARP Proxy Space ready!")
85
+
86
 
87
  @app.get("/")
88
  async def root():
89
+ """Health check with WARP status."""
90
+ connected = is_warp_connected()
91
+ uptime = int(time.time() - _warp_start_time) if _warp_start_time else 0
92
+ return {
93
+ "status": "ok" if connected else "starting",
94
+ "warp_connected": connected,
95
+ "socks5_proxy": WARP_SOCKS5 if connected else None,
96
+ "uptime_seconds": uptime,
97
+ "usage": {
98
+ "http_proxy": "/proxy?url=<target_url>",
99
+ "download": "/download?url=<file_url>",
100
+ "socks5": WARP_SOCKS5,
101
+ }
102
+ }
103
+
104
+
105
+ @app.get("/ping")
106
+ async def ping():
107
+ """Simple health check."""
108
+ connected = is_warp_connected()
109
+ return {"status": "alive", "warp_connected": connected}
110
+
111
+
112
+ @app.get("/status")
113
+ async def status():
114
+ """Get WARP connection status."""
115
+ try:
116
+ result = subprocess.run(
117
+ ["warp-cli", "--accept-tos", "status"],
118
+ capture_output=True, text=True, timeout=5
119
+ )
120
+ return {"raw": result.stdout.strip(), "connected": "Connected" in result.stdout}
121
+ except Exception as e:
122
+ return {"raw": str(e), "connected": False}
123
 
 
 
 
124
 
125
  @app.get("/proxy")
126
+ async def proxy_get(url: str = Query(..., description="Target URL to proxy")):
127
+ """Forward a GET request through WARP SOCKS5 proxy."""
128
+ return await _forward_request(url, "GET")
129
+
130
+
131
+ @app.post("/proxy")
132
+ async def proxy_post(request: Request):
133
+ """Forward a POST request through WARP SOCKS5 proxy.
134
+
135
+ Body: {"url": "...", "method": "GET", "headers": {}, "body": "..."}
136
+ """
137
+ try:
138
+ body = await request.json()
139
+ target_url = body.get("url", "")
140
+ method = body.get("method", "GET").upper()
141
+ headers = body.get("headers", {})
142
+ req_body = body.get("body")
143
+ return await _forward_request(target_url, method, headers, req_body)
144
+ except Exception as e:
145
+ return JSONResponse({"error": str(e)}, status_code=400)
146
+
147
+
148
+ async def _forward_request(url: str, method: str = "GET",
149
+ headers: dict = None, body=None,
150
+ follow_redirects: bool = True,
151
+ timeout: float = 60.0):
152
+ """Forward a request through the WARP SOCKS5 proxy."""
153
+ if not _warp_ready and not is_warp_connected():
154
+ # Try to wait a bit
155
+ connected = await wait_for_warp(10)
156
+ if not connected:
157
+ return JSONResponse(
158
+ {"error": "WARP not connected", "status": "starting"},
159
+ status_code=503
160
+ )
161
+
162
  try:
163
  async with httpx.AsyncClient(
164
+ proxy=WARP_SOCKS5,
165
+ follow_redirects=follow_redirects,
166
+ timeout=timeout,
167
  ) as client:
168
+ # Filter out hop-by-hop headers
169
+ safe_headers = {}
170
+ if headers:
171
+ skip = {"host", "connection", "keep-alive", "transfer-encoding",
172
+ "te", "trailer", "upgrade", "proxy-authorization"}
173
+ for k, v in headers.items():
174
+ if k.lower() not in skip:
175
+ safe_headers[k] = v
176
+
177
+ resp = await client.request(
178
+ method=method,
179
+ url=url,
180
+ headers=safe_headers,
181
+ content=body if isinstance(body, bytes) else (body.encode() if isinstance(body, str) else None),
182
+ )
183
+
184
+ # Return the response
185
  return Response(
186
  content=resp.content,
187
  status_code=resp.status_code,
188
+ headers=dict(resp.headers),
189
  )
190
+ except httpx.ConnectError as e:
191
+ return JSONResponse(
192
+ {"error": f"WARP proxy connection failed: {e}"},
193
+ status_code=502
194
+ )
195
+ except httpx.TimeoutException:
196
+ return JSONResponse(
197
+ {"error": "Request timed out through WARP proxy"},
198
+ status_code=504
199
+ )
200
  except Exception as e:
201
+ return JSONResponse(
202
+ {"error": f"Proxy error: {e}"},
203
+ status_code=500
204
+ )
205
 
206
+
207
+ @app.get("/download")
208
+ async def download_file(url: str = Query(..., description="File URL to download through WARP")):
209
+ """Download a file through WARP and stream it back.
210
+
211
+ Query params:
212
+ url: The file URL to download
213
+ output: "stream" (default) or "json" (returns base64)
214
+ """
215
+ if not _warp_ready and not is_warp_connected():
216
+ connected = await wait_for_warp(10)
217
+ if not connected:
218
+ return JSONResponse(
219
+ {"error": "WARP not connected"},
220
+ status_code=503
221
+ )
222
+
223
  try:
 
 
 
 
 
 
 
 
 
 
224
  async with httpx.AsyncClient(
225
+ proxy=WARP_SOCKS5,
226
+ follow_redirects=True,
227
+ timeout=120.0,
228
  ) as client:
229
+ async with client.stream("GET", url) as resp:
230
+ if resp.status_code != 200:
231
+ return JSONResponse(
232
+ {"error": f"Upstream returned {resp.status_code}"},
233
+ status_code=resp.status_code
234
+ )
235
+
236
+ content_type = resp.headers.get("content-type", "application/octet-stream")
237
+
238
+ return StreamingResponse(
239
+ resp.aiter_bytes(chunk_size=65536),
240
+ media_type=content_type,
241
+ headers={
242
+ "Content-Disposition": f'attachment; filename="{url.split("/")[-1].split("?")[0]}"',
243
+ }
244
+ )
245
  except Exception as e:
246
+ return JSONResponse(
247
+ {"error": f"Download failed: {e}"},
248
+ status_code=500
249
+ )
250
+
251
 
252
  if __name__ == "__main__":
253
  import uvicorn