feat: Sora curl_cffi sidecar — Chrome TLS 指纹绕过 Cloudflare
- 新增 sora-curl-cffi-sidecar 容器(Python + curl_cffi + chrome131) - docker-compose.tls-proxy.yml 集成 sidecar,sub2api 自动连接 - 会话池复用,避免重复 TLS 握手 - 镜像 zfc931912343/sora-curl-cffi-sidecar:latest (amd64+arm64)
This commit is contained in:
parent
7185630ba1
commit
0bfd6edde6
@ -1,41 +1,40 @@
|
||||
# =============================================================================
|
||||
# Node.js TLS Proxy Overlay
|
||||
# Node.js TLS Proxy + Sora Sidecar Overlay
|
||||
# =============================================================================
|
||||
# 在现有 docker-compose.yml 基础上增加 Node.js TLS 代理。
|
||||
#
|
||||
# 用法:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.tls-proxy.yml up -d
|
||||
#
|
||||
# 架构:
|
||||
# sub2api (Go) → HTTP 明文 → node-tls-proxy → HTTPS (原生 TLS) → api.anthropic.com
|
||||
#
|
||||
# 网络隔离:
|
||||
# - sub2api 仅连接 internal + sub2api-network(访问 pg/redis,但无外网)
|
||||
# - node-tls-proxy 双栈网络(internal + external),唯一的出站通道
|
||||
# - IPv6 内核级禁用
|
||||
# Anthropic: sub2api → node-tls-proxy (Node.js TLS) → api.anthropic.com
|
||||
# Sora: sub2api → sora-curl-cffi-sidecar (Chrome TLS) → sora.chatgpt.com
|
||||
# =============================================================================
|
||||
|
||||
services:
|
||||
# ===========================================================================
|
||||
# 覆盖 sub2api:加入 internal 网络 + 启用 Node.js TLS 代理
|
||||
# 覆盖 sub2api:加入 internal 网络 + 启用代理
|
||||
# ===========================================================================
|
||||
sub2api:
|
||||
networks:
|
||||
- sub2api-internal
|
||||
- sub2api-network # 保留:访问 postgres/redis
|
||||
environment:
|
||||
# 启用 Node.js TLS 代理
|
||||
# Node.js TLS 代理(Anthropic)
|
||||
- GATEWAY_NODE_TLS_PROXY_ENABLED=true
|
||||
- GATEWAY_NODE_TLS_PROXY_LISTEN_PORT=3456
|
||||
- GATEWAY_NODE_TLS_PROXY_LISTEN_HOST=node-tls-proxy
|
||||
- GATEWAY_NODE_TLS_PROXY_UPSTREAM_HOST=api.anthropic.com
|
||||
# Sora curl_cffi sidecar(Chrome 指纹绕过 Cloudflare)
|
||||
- SORA_CLIENT_CURL_CFFI_SIDECAR_ENABLED=true
|
||||
- SORA_CLIENT_CURL_CFFI_SIDECAR_BASE_URL=http://sora-curl-cffi-sidecar:8080
|
||||
- SORA_CLIENT_CURL_CFFI_SIDECAR_IMPERSONATE=chrome131
|
||||
depends_on:
|
||||
node-tls-proxy:
|
||||
condition: service_healthy
|
||||
sora-curl-cffi-sidecar:
|
||||
condition: service_healthy
|
||||
|
||||
# ===========================================================================
|
||||
# Node.js TLS Forward Proxy
|
||||
# 直接拉取预构建镜像,支持 amd64/arm64
|
||||
# Node.js TLS Forward Proxy (Anthropic)
|
||||
# ===========================================================================
|
||||
node-tls-proxy:
|
||||
image: zfc931912343/sub2api-tls-proxy:latest
|
||||
@ -49,14 +48,12 @@ services:
|
||||
- PROXY_PORT=3456
|
||||
- PROXY_HOST=0.0.0.0
|
||||
- UPSTREAM_HOST=api.anthropic.com
|
||||
# 可选:经过外部代理出站(HTTP CONNECT 隧道)
|
||||
- UPSTREAM_PROXY=${TLS_PROXY_UPSTREAM_PROXY:-}
|
||||
- TZ=${TZ:-Asia/Shanghai}
|
||||
networks:
|
||||
- sub2api-internal # sub2api 可以访问
|
||||
- sub2api-external # 可以访问外网
|
||||
- sub2api-internal
|
||||
- sub2api-external
|
||||
sysctls:
|
||||
# 内核级禁用 IPv6(防 IPv6 泄露)
|
||||
- net.ipv6.conf.all.disable_ipv6=1
|
||||
- net.ipv6.conf.default.disable_ipv6=1
|
||||
healthcheck:
|
||||
@ -71,12 +68,40 @@ services:
|
||||
memory: 256M
|
||||
cpus: "1.0"
|
||||
|
||||
# ===========================================================================
|
||||
# Sora curl_cffi Sidecar (Chrome TLS fingerprint for Cloudflare bypass)
|
||||
# ===========================================================================
|
||||
sora-curl-cffi-sidecar:
|
||||
image: zfc931912343/sora-curl-cffi-sidecar:latest
|
||||
container_name: sub2api-sora-sidecar
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PORT=8080
|
||||
- IMPERSONATE=chrome131
|
||||
- TIMEOUT_SECONDS=60
|
||||
- SESSION_TTL_SECONDS=3600
|
||||
- TZ=${TZ:-Asia/Shanghai}
|
||||
networks:
|
||||
- sub2api-internal
|
||||
- sub2api-external
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health')"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
cpus: "1.0"
|
||||
|
||||
# =============================================================================
|
||||
# Networks
|
||||
# =============================================================================
|
||||
networks:
|
||||
sub2api-internal:
|
||||
internal: true # 关键:无外网访问
|
||||
internal: true
|
||||
driver: bridge
|
||||
sub2api-external:
|
||||
driver: bridge
|
||||
|
||||
27
tools/sora-curl-cffi-sidecar/Dockerfile
Normal file
27
tools/sora-curl-cffi-sidecar/Dockerfile
Normal file
@ -0,0 +1,27 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
LABEL description="Sora curl_cffi sidecar - Chrome TLS fingerprint for Cloudflare bypass"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 安装依赖(curl_cffi 需要编译环境)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc libffi-dev && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY app.py .
|
||||
|
||||
ENV PORT=8080
|
||||
ENV IMPERSONATE=chrome131
|
||||
ENV TIMEOUT_SECONDS=60
|
||||
ENV SESSION_TTL_SECONDS=3600
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=10s \
|
||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health')" || exit 1
|
||||
|
||||
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8080", "--timeout", "120", "app:app"]
|
||||
91
tools/sora-curl-cffi-sidecar/app.py
Normal file
91
tools/sora-curl-cffi-sidecar/app.py
Normal file
@ -0,0 +1,91 @@
|
||||
"""
|
||||
Sora curl_cffi sidecar — 用 Chrome 131 TLS 指纹绕过 Cloudflare
|
||||
sub2api 通过 HTTP 调用此服务转发 Sora 请求
|
||||
"""
|
||||
from flask import Flask, request, Response
|
||||
from curl_cffi import requests as cffi_requests
|
||||
import json, os, time, threading
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
IMPERSONATE = os.environ.get("IMPERSONATE", "chrome131")
|
||||
TIMEOUT = int(os.environ.get("TIMEOUT_SECONDS", "60"))
|
||||
|
||||
# 会话池:按 session_key 复用,避免每次请求都做 TLS 握手
|
||||
sessions = {}
|
||||
sessions_lock = threading.Lock()
|
||||
SESSION_TTL = int(os.environ.get("SESSION_TTL_SECONDS", "3600"))
|
||||
|
||||
|
||||
def get_session(session_key="default"):
|
||||
with sessions_lock:
|
||||
entry = sessions.get(session_key)
|
||||
if entry and (time.time() - entry["created"]) < SESSION_TTL:
|
||||
return entry["session"]
|
||||
s = cffi_requests.Session(impersonate=IMPERSONATE)
|
||||
sessions[session_key] = {"session": s, "created": time.time()}
|
||||
return s
|
||||
|
||||
|
||||
@app.route("/health", methods=["GET"])
|
||||
def health():
|
||||
return json.dumps({"status": "ok", "impersonate": IMPERSONATE}), 200
|
||||
|
||||
|
||||
@app.route("/proxy", methods=["POST"])
|
||||
def proxy():
|
||||
"""
|
||||
接收 sub2api 的代理请求,用 curl_cffi + Chrome 指纹转发到目标 URL
|
||||
请求体 JSON:
|
||||
{
|
||||
"url": "https://sora.chatgpt.com/backend/me",
|
||||
"method": "GET",
|
||||
"headers": {"Authorization": "Bearer xxx", ...},
|
||||
"body": "...",
|
||||
"session_key": "account_123" // 可选
|
||||
}
|
||||
"""
|
||||
try:
|
||||
data = request.get_json(force=True)
|
||||
except Exception:
|
||||
return json.dumps({"error": "invalid json"}), 400
|
||||
|
||||
url = data.get("url", "")
|
||||
method = data.get("method", "GET").upper()
|
||||
headers = data.get("headers", {})
|
||||
body = data.get("body")
|
||||
session_key = data.get("session_key", "default")
|
||||
|
||||
if not url:
|
||||
return json.dumps({"error": "url required"}), 400
|
||||
|
||||
try:
|
||||
sess = get_session(session_key)
|
||||
resp = sess.request(
|
||||
method=method,
|
||||
url=url,
|
||||
headers=headers,
|
||||
data=body.encode("utf-8") if isinstance(body, str) else body,
|
||||
timeout=TIMEOUT,
|
||||
allow_redirects=True,
|
||||
)
|
||||
|
||||
# 透传响应
|
||||
excluded_headers = {"transfer-encoding", "content-encoding", "connection"}
|
||||
resp_headers = {
|
||||
k: v for k, v in resp.headers.items()
|
||||
if k.lower() not in excluded_headers
|
||||
}
|
||||
|
||||
return Response(
|
||||
response=resp.content,
|
||||
status=resp.status_code,
|
||||
headers=resp_headers,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)}), 502
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
port = int(os.environ.get("PORT", "8080"))
|
||||
app.run(host="0.0.0.0", port=port)
|
||||
3
tools/sora-curl-cffi-sidecar/requirements.txt
Normal file
3
tools/sora-curl-cffi-sidecar/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
flask>=3.0
|
||||
curl_cffi>=0.7
|
||||
gunicorn>=22.0
|
||||
Loading…
x
Reference in New Issue
Block a user