fix: 双模型审查 Critical 修复

1. Sora session_key 按 accountID 隔离(消除跨账号指纹关联)
2. 有 per-account 代理的 Sora 账号跳过 sidecar(保持代理 IP)
3. 请求体用 base64 编码传输(防止二进制数据损坏)
4. Node.js 代理 Body 用 GetBody 安全复制(修复重试时 Body 枯竭)
This commit is contained in:
win 2026-03-22 12:04:31 +08:00
parent 4a92f1903f
commit 3c8ffd3efc
3 changed files with 38 additions and 11 deletions

View File

@ -286,7 +286,12 @@ func (s *httpUpstreamService) doViaNodeTLSProxy(req *http.Request, proxyURL stri
// 克隆请求,避免修改原始 req重试时需要原始 URL // 克隆请求,避免修改原始 req重试时需要原始 URL
proxyReq := req.Clone(req.Context()) proxyReq := req.Clone(req.Context())
proxyReq.Body = req.Body // Clone 不复制 Body // 安全复制 Body优先用 GetBody 工厂方法
if req.GetBody != nil {
proxyReq.Body, _ = req.GetBody()
} else {
proxyReq.Body = req.Body
}
// 保存原始目标主机,通过自定义头传给 Node.js 代理 // 保存原始目标主机,通过自定义头传给 Node.js 代理
originalHost := req.URL.Host originalHost := req.URL.Host

View File

@ -3,6 +3,7 @@ package service
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -1026,11 +1027,19 @@ func (c *SoraSDKClient) debugLogf(format string, args ...any) {
} }
// doSoraHTTP 执行 Sora HTTP 请求,优先走 curl_cffi sidecarChrome TLS 指纹绕过 Cloudflare // doSoraHTTP 执行 Sora HTTP 请求,优先走 curl_cffi sidecarChrome TLS 指纹绕过 Cloudflare
// 如果 sidecar 未启用或不可用,回退到 httpUpstream.Do() / http.DefaultClient // 如果 sidecar 未启用、账号有独立代理、 sidecar 不可用,回退到 httpUpstream.Do()
func (c *SoraSDKClient) doSoraHTTP(req *http.Request, proxyURL string, accountID int64, accountConcurrency int) (*http.Response, error) { func (c *SoraSDKClient) doSoraHTTP(req *http.Request, proxyURL string, accountID int64, accountConcurrency int) (*http.Response, error) {
// 如果账号配置了独立代理,跳过 sidecarsidecar 不支持 per-account 代理)
if proxyURL != "" {
if c.httpUpstream != nil {
return c.httpUpstream.Do(req, proxyURL, accountID, accountConcurrency)
}
return http.DefaultClient.Do(req)
}
// 检查 sidecar 是否启用 // 检查 sidecar 是否启用
if c.cfg != nil && c.cfg.Sora.Client.CurlCFFISidecar.Enabled && c.cfg.Sora.Client.CurlCFFISidecar.BaseURL != "" { if c.cfg != nil && c.cfg.Sora.Client.CurlCFFISidecar.Enabled && c.cfg.Sora.Client.CurlCFFISidecar.BaseURL != "" {
resp, err := c.doViaSidecar(req) resp, err := c.doViaSidecar(req, accountID)
if err == nil { if err == nil {
return resp, nil return resp, nil
} }
@ -1046,17 +1055,17 @@ func (c *SoraSDKClient) doSoraHTTP(req *http.Request, proxyURL string, accountID
} }
// doViaSidecar 通过 curl_cffi sidecar 发送请求Chrome TLS 指纹) // doViaSidecar 通过 curl_cffi sidecar 发送请求Chrome TLS 指纹)
func (c *SoraSDKClient) doViaSidecar(originalReq *http.Request) (*http.Response, error) { func (c *SoraSDKClient) doViaSidecar(originalReq *http.Request, accountID int64) (*http.Response, error) {
sidecarURL := strings.TrimRight(c.cfg.Sora.Client.CurlCFFISidecar.BaseURL, "/") + "/proxy" sidecarURL := strings.TrimRight(c.cfg.Sora.Client.CurlCFFISidecar.BaseURL, "/") + "/proxy"
// 读取原始请求体 // 读取原始请求体,用 base64 编码(安全传输二进制数据)
var bodyStr string var bodyB64 string
if originalReq.Body != nil { if originalReq.Body != nil {
bodyBytes, err := io.ReadAll(originalReq.Body) bodyBytes, err := io.ReadAll(originalReq.Body)
if err != nil { if err != nil {
return nil, fmt.Errorf("read request body: %w", err) return nil, fmt.Errorf("read request body: %w", err)
} }
bodyStr = string(bodyBytes) bodyB64 = base64.StdEncoding.EncodeToString(bodyBytes)
// 恢复 body 以备回退 // 恢复 body 以备回退
originalReq.Body = io.NopCloser(bytes.NewReader(bodyBytes)) originalReq.Body = io.NopCloser(bytes.NewReader(bodyBytes))
} }
@ -1073,8 +1082,9 @@ func (c *SoraSDKClient) doViaSidecar(originalReq *http.Request) (*http.Response,
"url": originalReq.URL.String(), "url": originalReq.URL.String(),
"method": originalReq.Method, "method": originalReq.Method,
"headers": headers, "headers": headers,
"body": bodyStr, "body": bodyB64,
"session_key": fmt.Sprintf("account_%d", 0), // 简单的 session key "is_base64": true,
"session_key": fmt.Sprintf("account_%d", accountID), // 每账号独立会话
} }
payloadBytes, err := json.Marshal(payload) payloadBytes, err := json.Marshal(payload)

View File

@ -4,7 +4,7 @@ sub2api 通过 HTTP 调用此服务转发 Sora 请求
""" """
from flask import Flask, request, Response from flask import Flask, request, Response
from curl_cffi import requests as cffi_requests from curl_cffi import requests as cffi_requests
import json, os, time, threading import json, os, time, threading, base64
app = Flask(__name__) app = Flask(__name__)
@ -54,18 +54,30 @@ def proxy():
method = data.get("method", "GET").upper() method = data.get("method", "GET").upper()
headers = data.get("headers", {}) headers = data.get("headers", {})
body = data.get("body") body = data.get("body")
is_base64 = data.get("is_base64", False)
session_key = data.get("session_key", "default") session_key = data.get("session_key", "default")
if not url: if not url:
return json.dumps({"error": "url required"}), 400 return json.dumps({"error": "url required"}), 400
# 解码 base64 编码的请求体(安全传输二进制数据)
req_data = None
if body:
if is_base64:
try:
req_data = base64.b64decode(body)
except Exception:
req_data = body.encode("utf-8") if isinstance(body, str) else body
else:
req_data = body.encode("utf-8") if isinstance(body, str) else body
try: try:
sess = get_session(session_key) sess = get_session(session_key)
resp = sess.request( resp = sess.request(
method=method, method=method,
url=url, url=url,
headers=headers, headers=headers,
data=body.encode("utf-8") if isinstance(body, str) else body, data=req_data,
timeout=TIMEOUT, timeout=TIMEOUT,
allow_redirects=True, allow_redirects=True,
) )