261 lines
7.3 KiB
Go
261 lines
7.3 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/util/logredact"
|
|
)
|
|
|
|
const soraCurlCFFISidecarDefaultTimeoutSeconds = 60
|
|
|
|
type soraCurlCFFISidecarRequest struct {
|
|
Method string `json:"method"`
|
|
URL string `json:"url"`
|
|
Headers map[string][]string `json:"headers,omitempty"`
|
|
BodyBase64 string `json:"body_base64,omitempty"`
|
|
ProxyURL string `json:"proxy_url,omitempty"`
|
|
SessionKey string `json:"session_key,omitempty"`
|
|
Impersonate string `json:"impersonate,omitempty"`
|
|
TimeoutSeconds int `json:"timeout_seconds,omitempty"`
|
|
}
|
|
|
|
type soraCurlCFFISidecarResponse struct {
|
|
StatusCode int `json:"status_code"`
|
|
Status int `json:"status"`
|
|
Headers map[string]any `json:"headers"`
|
|
BodyBase64 string `json:"body_base64"`
|
|
Body string `json:"body"`
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
func (c *SoraDirectClient) doHTTPViaCurlCFFISidecar(req *http.Request, proxyURL string, account *Account) (*http.Response, error) {
|
|
if req == nil || req.URL == nil {
|
|
return nil, errors.New("request url is nil")
|
|
}
|
|
if c == nil || c.cfg == nil {
|
|
return nil, errors.New("sora curl_cffi sidecar config is nil")
|
|
}
|
|
if !c.cfg.Sora.Client.CurlCFFISidecar.Enabled {
|
|
return nil, errors.New("sora curl_cffi sidecar is disabled")
|
|
}
|
|
endpoint := c.curlCFFISidecarEndpoint()
|
|
if endpoint == "" {
|
|
return nil, errors.New("sora curl_cffi sidecar base_url is empty")
|
|
}
|
|
|
|
bodyBytes, err := readAndRestoreRequestBody(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sora curl_cffi sidecar read request body failed: %w", err)
|
|
}
|
|
|
|
headers := make(map[string][]string, len(req.Header)+1)
|
|
for key, vals := range req.Header {
|
|
copied := make([]string, len(vals))
|
|
copy(copied, vals)
|
|
headers[key] = copied
|
|
}
|
|
if strings.TrimSpace(req.Host) != "" {
|
|
if _, ok := headers["Host"]; !ok {
|
|
headers["Host"] = []string{req.Host}
|
|
}
|
|
}
|
|
|
|
payload := soraCurlCFFISidecarRequest{
|
|
Method: req.Method,
|
|
URL: req.URL.String(),
|
|
Headers: headers,
|
|
ProxyURL: strings.TrimSpace(proxyURL),
|
|
SessionKey: c.sidecarSessionKey(account, proxyURL),
|
|
Impersonate: c.curlCFFIImpersonate(),
|
|
TimeoutSeconds: c.curlCFFISidecarTimeoutSeconds(),
|
|
}
|
|
if len(bodyBytes) > 0 {
|
|
payload.BodyBase64 = base64.StdEncoding.EncodeToString(bodyBytes)
|
|
}
|
|
|
|
encoded, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sora curl_cffi sidecar marshal request failed: %w", err)
|
|
}
|
|
|
|
sidecarReq, err := http.NewRequestWithContext(req.Context(), http.MethodPost, endpoint, bytes.NewReader(encoded))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sora curl_cffi sidecar build request failed: %w", err)
|
|
}
|
|
sidecarReq.Header.Set("Content-Type", "application/json")
|
|
sidecarReq.Header.Set("Accept", "application/json")
|
|
|
|
httpClient := &http.Client{Timeout: time.Duration(payload.TimeoutSeconds) * time.Second}
|
|
sidecarResp, err := httpClient.Do(sidecarReq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sora curl_cffi sidecar request failed: %w", err)
|
|
}
|
|
defer func() {
|
|
_ = sidecarResp.Body.Close()
|
|
}()
|
|
|
|
sidecarRespBody, err := io.ReadAll(io.LimitReader(sidecarResp.Body, 8<<20))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sora curl_cffi sidecar read response failed: %w", err)
|
|
}
|
|
if sidecarResp.StatusCode != http.StatusOK {
|
|
redacted := truncateForLog([]byte(logredact.RedactText(string(sidecarRespBody))), 512)
|
|
return nil, fmt.Errorf("sora curl_cffi sidecar http status=%d body=%s", sidecarResp.StatusCode, redacted)
|
|
}
|
|
|
|
var payloadResp soraCurlCFFISidecarResponse
|
|
if err := json.Unmarshal(sidecarRespBody, &payloadResp); err != nil {
|
|
return nil, fmt.Errorf("sora curl_cffi sidecar parse response failed: %w", err)
|
|
}
|
|
if msg := strings.TrimSpace(payloadResp.Error); msg != "" {
|
|
return nil, fmt.Errorf("sora curl_cffi sidecar upstream error: %s", msg)
|
|
}
|
|
statusCode := payloadResp.StatusCode
|
|
if statusCode <= 0 {
|
|
statusCode = payloadResp.Status
|
|
}
|
|
if statusCode <= 0 {
|
|
return nil, errors.New("sora curl_cffi sidecar response missing status code")
|
|
}
|
|
|
|
responseBody := []byte(payloadResp.Body)
|
|
if strings.TrimSpace(payloadResp.BodyBase64) != "" {
|
|
decoded, err := base64.StdEncoding.DecodeString(payloadResp.BodyBase64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sora curl_cffi sidecar decode body failed: %w", err)
|
|
}
|
|
responseBody = decoded
|
|
}
|
|
|
|
respHeaders := make(http.Header)
|
|
for key, rawVal := range payloadResp.Headers {
|
|
for _, v := range convertSidecarHeaderValue(rawVal) {
|
|
respHeaders.Add(key, v)
|
|
}
|
|
}
|
|
|
|
return &http.Response{
|
|
StatusCode: statusCode,
|
|
Header: respHeaders,
|
|
Body: io.NopCloser(bytes.NewReader(responseBody)),
|
|
ContentLength: int64(len(responseBody)),
|
|
Request: req,
|
|
}, nil
|
|
}
|
|
|
|
func readAndRestoreRequestBody(req *http.Request) ([]byte, error) {
|
|
if req == nil || req.Body == nil {
|
|
return nil, nil
|
|
}
|
|
bodyBytes, err := io.ReadAll(req.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_ = req.Body.Close()
|
|
req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
|
req.ContentLength = int64(len(bodyBytes))
|
|
return bodyBytes, nil
|
|
}
|
|
|
|
func (c *SoraDirectClient) curlCFFISidecarEndpoint() string {
|
|
if c == nil || c.cfg == nil {
|
|
return ""
|
|
}
|
|
raw := strings.TrimSpace(c.cfg.Sora.Client.CurlCFFISidecar.BaseURL)
|
|
if raw == "" {
|
|
return ""
|
|
}
|
|
parsed, err := url.Parse(raw)
|
|
if err != nil || strings.TrimSpace(parsed.Scheme) == "" || strings.TrimSpace(parsed.Host) == "" {
|
|
return raw
|
|
}
|
|
if path := strings.TrimSpace(parsed.Path); path == "" || path == "/" {
|
|
parsed.Path = "/request"
|
|
}
|
|
return parsed.String()
|
|
}
|
|
|
|
func (c *SoraDirectClient) curlCFFISidecarTimeoutSeconds() int {
|
|
if c == nil || c.cfg == nil {
|
|
return soraCurlCFFISidecarDefaultTimeoutSeconds
|
|
}
|
|
timeoutSeconds := c.cfg.Sora.Client.CurlCFFISidecar.TimeoutSeconds
|
|
if timeoutSeconds <= 0 {
|
|
return soraCurlCFFISidecarDefaultTimeoutSeconds
|
|
}
|
|
return timeoutSeconds
|
|
}
|
|
|
|
func (c *SoraDirectClient) curlCFFIImpersonate() string {
|
|
if c == nil || c.cfg == nil {
|
|
return "chrome131"
|
|
}
|
|
impersonate := strings.TrimSpace(c.cfg.Sora.Client.CurlCFFISidecar.Impersonate)
|
|
if impersonate == "" {
|
|
return "chrome131"
|
|
}
|
|
return impersonate
|
|
}
|
|
|
|
func (c *SoraDirectClient) sidecarSessionReuseEnabled() bool {
|
|
if c == nil || c.cfg == nil {
|
|
return true
|
|
}
|
|
return c.cfg.Sora.Client.CurlCFFISidecar.SessionReuseEnabled
|
|
}
|
|
|
|
func (c *SoraDirectClient) sidecarSessionTTLSeconds() int {
|
|
if c == nil || c.cfg == nil {
|
|
return 3600
|
|
}
|
|
ttl := c.cfg.Sora.Client.CurlCFFISidecar.SessionTTLSeconds
|
|
if ttl < 0 {
|
|
return 3600
|
|
}
|
|
return ttl
|
|
}
|
|
|
|
func convertSidecarHeaderValue(raw any) []string {
|
|
switch val := raw.(type) {
|
|
case nil:
|
|
return nil
|
|
case string:
|
|
if strings.TrimSpace(val) == "" {
|
|
return nil
|
|
}
|
|
return []string{val}
|
|
case []any:
|
|
out := make([]string, 0, len(val))
|
|
for _, item := range val {
|
|
s := strings.TrimSpace(fmt.Sprint(item))
|
|
if s != "" {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
case []string:
|
|
out := make([]string, 0, len(val))
|
|
for _, item := range val {
|
|
if strings.TrimSpace(item) != "" {
|
|
out = append(out, item)
|
|
}
|
|
}
|
|
return out
|
|
default:
|
|
s := strings.TrimSpace(fmt.Sprint(val))
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
return []string{s}
|
|
}
|
|
}
|