fvps/loop
0
1import keep_alive
2from fastapi import FastAPI, HTTPException, status
3from fastapi.responses import JSONResponse, StreamingResponse, Response
4from curl_cffi.requests.exceptions import (
5 CurlError,
6 ContentDecodingError,
7 StreamConsumedError,
8)
9from schemas import ProxyRequest, BROWSER_TYPES, API_KEY_EXPECTED
10from utils import pick_impersonate, perform_final_hop, forward_to_next_hop, render_response
11
12app = FastAPI(title="Proxy Chain Service", version="1.1.0")
13
14
15def check_key(apikey: str | None):
16 if API_KEY_EXPECTED and apikey != API_KEY_EXPECTED:
17 raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
18 detail="Invalid API key")
19
20
21@app.post("/proxy", summary="单跳代理")
22async def proxy(req: ProxyRequest):
23 check_key(req.apikey)
24 imp = pick_impersonate(req.impersonate)
25 try:
26 resp = await perform_final_hop(req, imp)
27 except (CurlError, ContentDecodingError, StreamConsumedError) as e:
28 raise HTTPException(status_code=502, detail=str(e))
29 except Exception as e:
30 raise HTTPException(status_code=500, detail=str(e))
31
32 return render_response(resp, imp, req.return_data, req.stream)
33
34
35@app.post("/looproxy", summary="链式代理")
36async def looproxy(req: ProxyRequest):
37 """
38 * proxy_chain 为空 -> 等价 /proxy
39 * proxy_chain 非空 -> 取首元素作为下一跳 URL,递归 POST
40 """
41 check_key(req.apikey)
42 imp = pick_impersonate(req.impersonate)
43
44 # ---------- 无链路:落地 ----------
45 if not req.proxy_chain:
46 try:
47 resp = await perform_final_hop(req, imp)
48 except (CurlError, ContentDecodingError, StreamConsumedError) as e:
49 raise HTTPException(status_code=502, detail=str(e))
50 except Exception as e:
51 raise HTTPException(status_code=500, detail=str(e))
52 return render_response(resp, imp, req.return_data, req.stream)
53
54 # ---------- 有链路:转发 ----------
55 next_hop, *rest = req.proxy_chain
56 nested_req = req.copy(update={"proxy_chain": rest})
57
58 try:
59 resp = await forward_to_next_hop(next_hop, nested_req, imp)
60 except (CurlError, ContentDecodingError, StreamConsumedError) as e:
61 raise HTTPException(status_code=502, detail=str(e))
62 except Exception as e:
63 raise HTTPException(status_code=500, detail=str(e))
64
65 hop_by_hop = {"content-encoding", "transfer-encoding", "content-length", "connection"}
66 headers = {k: v for k, v in resp.headers.items() if k.lower() not in hop_by_hop}
67
68 if req.stream:
69 return StreamingResponse(resp.iter_content(), status_code=resp.status_code, media_type="application/octet-stream", headers=headers)
70
71 hop_by_hop = {"content-length", "transfer-encoding"}
72 headers = {k: v for k, v in resp.headers.items() if k.lower() not in hop_by_hop}
73 return Response(
74 content=resp.content,
75 status_code=resp.status_code,
76 headers=headers,
77 media_type=resp.headers.get("content-type")
78 )
79
80
81@app.get("/impersonate", summary="impersonate 可用列表")
82def impersonate():
83 return JSONResponse(content=BROWSER_TYPES)
84
85
86@app.get("/health", summary="健康状态")
87def health():
88 return JSONResponse(content={"status": "healthy"})
89
90
91@app.get("/")
92def index():
93 return Response(content="looproxy pro is running...")
94
95
96if __name__ == "__main__":
97 import os, platform, uvicorn
98
99 config = {
100 "app": "main:app",
101 "host": "0.0.0.0",
102 "port": int(os.getenv("PORT", "8000")),
103 "proxy_headers": True,
104 "forwarded_allow_ips": "*",
105 "access_log": False,
106 }
107 if platform.system().lower() != "windows":
108 config.update({"loop": "uvloop", "http": "httptools"})
109 uvicorn.run(**config)
110 