feat: Sora curl_cffi sidecar — Chrome TLS 指纹绕过 Cloudflare
Some checks failed
CI / test (push) Failing after 3s
CI / golangci-lint (push) Failing after 3s
Security Scan / backend-security (push) Failing after 3s
Security Scan / frontend-security (push) Failing after 3s

- 新增 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:
win 2026-03-22 03:31:49 +08:00
parent 7185630ba1
commit 0bfd6edde6
4 changed files with 164 additions and 18 deletions

View File

@ -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 sidecarChrome 指纹绕过 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

View 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"]

View 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)

View File

@ -0,0 +1,3 @@
flask>=3.0
curl_cffi>=0.7
gunicorn>=22.0