CoolFace
Apppublic

fvps/loop

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
main.py110 linesDownload Raw Back to root
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