File size: 5,440 Bytes
7ca28aa
 
 
 
 
 
 
 
 
 
 
 
b2329f5
7ca28aa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
from __future__ import annotations
from fastapi import FastAPI, Request, Response, Depends
from fastapi.responses import StreamingResponse
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from contextlib import asynccontextmanager
from urllib.parse import quote
import httpx
import asyncio
import os
import secrets

# ============== 保活配置 ==============
SPACE_HOST = os.environ.get("KEEPALIVE_HOST", "doasyousay-googleapis.hf.space")
KEEP_ALIVE_INTERVAL = 300

async def keep_alive_task():
    """后台保活任务"""
    await asyncio.sleep(30)
    while True:
        try:
            if SPACE_HOST:
                async with httpx.AsyncClient() as client:
                    resp = await client.get("https://" + SPACE_HOST, timeout=30)
                    print(f"[Keep-Alive] Ping -> {resp.status_code}")
            else:
                print("[Keep-Alive] SPACE_HOST not set, skipping...")
        except Exception as e:
            print(f"[Keep-Alive] Error: {e}")
        await asyncio.sleep(KEEP_ALIVE_INTERVAL)

@asynccontextmanager
async def lifespan(app: FastAPI):
    task = asyncio.create_task(keep_alive_task())
    print("[Startup] Keep-alive task started")
    yield
    task.cancel()
    print("[Shutdown] Keep-alive task cancelled")

app = FastAPI(lifespan=lifespan)
security = HTTPBasic(auto_error=False)

USERNAME = os.environ.get("PROXY_USER", "IulHnU")
PASSWORD = os.environ.get("PROXY_PASS", "TtLOY2")

def parse_proxy_header(proxy_value: str) -> str:
    """
    解析 proxy header 并返回 httpx 可用的代理 URL。

    支持两种格式:
    1. 新格式 (SOCKS5 认证): host:port:username:password
       -> socks5://user:pass@host:port
    2. 旧格式 (HTTP 代理): host:port
       -> http://host:port
    """
    if not proxy_value:
        return None

    parts = proxy_value.split(":")

    # 至少需要 host:port
    if len(parts) < 2:
        print(f"[Proxy] Invalid format (need host:port): {proxy_value}")
        return None

    # 新格式: host:port:username:password (4+ 部分)
    if len(parts) >= 4:
        host = parts[0]
        port = parts[1]
        username = parts[2]
        password = ":".join(parts[3:])  # 密码可能包含冒号

        if not port.isdigit():
            print(f"[Proxy] Invalid port: {port}")
            return None

        encoded_user = quote(username, safe="")
        encoded_pass = quote(password, safe="")

        print(f"[Proxy] Using HTTP proxy: {host}:{port} (user={username})")
        return f"http://{encoded_user}:{encoded_pass}@{host}:{port}"

    # 旧格式: host:port
    host, port = parts[0], parts[1]
    if not port.isdigit():
        print(f"[Proxy] Invalid port: {port}")
        return None

    print(f"[Proxy] Using HTTP proxy: {host}:{port}")
    return f"http://{host}:{port}"

def verify_auth(credentials: HTTPBasicCredentials = Depends(security)):
    """可选认证:如果传递了凭据则验证,但不强制要求也不阻止请求"""
    if credentials:
        is_user = secrets.compare_digest(credentials.username, USERNAME)
        is_pass = secrets.compare_digest(credentials.password, PASSWORD)
        if not (is_user and is_pass):
            print(f"[Auth] Invalid credentials provided (user={credentials.username})")
    return None  # 始终放行

@app.api_route("/proxy", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"])
async def proxy(target_url: str, request: Request, _: None = Depends(verify_auth)):
    try:
        body = await request.body()

        # 提取 proxy 头并解析代理 URL
        proxy_header = request.headers.get("proxy")
        proxy_url = parse_proxy_header(proxy_header)

        # 过滤会泄露客户端真实 IP 的头,以及 host 和 proxy 头
        STRIP_HEADERS = {
            b"host", b"proxy",
            b"x-forwarded-for", b"x-forwarded-host", b"x-forwarded-proto",
            b"x-real-ip", b"forwarded", b"via",
            b"cf-connecting-ip", b"true-client-ip",
        }
        forward_headers = [
            (k, v) for k, v in request.headers.raw
            if k.lower() not in STRIP_HEADERS
        ]

        client = httpx.AsyncClient(
            proxy=proxy_url,
        )
        req = client.build_request(
            method=request.method,
            url=target_url,
            headers=forward_headers,
            content=body if body else None,
            timeout=30,
        )
        resp = await client.send(req, stream=True, follow_redirects=False)

        # 流式转发:边收边发
        async def stream_body():
            try:
                async for chunk in resp.aiter_raw():
                    yield chunk
            finally:
                await resp.aclose()
                await client.aclose()

        SKIP_RESP_HEADERS = {b"transfer-encoding", b"connection"}
        response = StreamingResponse(
            content=stream_body(),
            status_code=resp.status_code,
        )
        for k, v in resp.headers.raw:
            if k.lower() not in SKIP_RESP_HEADERS:
                response.headers.append(k.decode(), v.decode())
        return response

    except Exception as e:
        import traceback
        print(f"Proxy Error: {e}")
        traceback.print_exc()
        return Response(content=f"Proxy Error: {str(e)}", status_code=500)

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=7860)