Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
36a1a799
Commit
36a1a799
authored
Feb 19, 2026
by
yangjianbo
Browse files
feat(sora): 强制Sora走curl_cffi sidecar并完善校验测试
parent
40498aac
Changes
6
Hide whitespace changes
Inline
Side-by-side
backend/internal/config/config.go
View file @
36a1a799
...
@@ -271,18 +271,27 @@ type SoraConfig struct {
...
@@ -271,18 +271,27 @@ type SoraConfig struct {
// SoraClientConfig 直连 Sora 客户端配置
// SoraClientConfig 直连 Sora 客户端配置
type
SoraClientConfig
struct
{
type
SoraClientConfig
struct
{
BaseURL
string
`mapstructure:"base_url"`
BaseURL
string
`mapstructure:"base_url"`
TimeoutSeconds
int
`mapstructure:"timeout_seconds"`
TimeoutSeconds
int
`mapstructure:"timeout_seconds"`
MaxRetries
int
`mapstructure:"max_retries"`
MaxRetries
int
`mapstructure:"max_retries"`
PollIntervalSeconds
int
`mapstructure:"poll_interval_seconds"`
PollIntervalSeconds
int
`mapstructure:"poll_interval_seconds"`
MaxPollAttempts
int
`mapstructure:"max_poll_attempts"`
MaxPollAttempts
int
`mapstructure:"max_poll_attempts"`
RecentTaskLimit
int
`mapstructure:"recent_task_limit"`
RecentTaskLimit
int
`mapstructure:"recent_task_limit"`
RecentTaskLimitMax
int
`mapstructure:"recent_task_limit_max"`
RecentTaskLimitMax
int
`mapstructure:"recent_task_limit_max"`
Debug
bool
`mapstructure:"debug"`
Debug
bool
`mapstructure:"debug"`
UseOpenAITokenProvider
bool
`mapstructure:"use_openai_token_provider"`
UseOpenAITokenProvider
bool
`mapstructure:"use_openai_token_provider"`
Headers
map
[
string
]
string
`mapstructure:"headers"`
Headers
map
[
string
]
string
`mapstructure:"headers"`
UserAgent
string
`mapstructure:"user_agent"`
UserAgent
string
`mapstructure:"user_agent"`
DisableTLSFingerprint
bool
`mapstructure:"disable_tls_fingerprint"`
DisableTLSFingerprint
bool
`mapstructure:"disable_tls_fingerprint"`
CurlCFFISidecar
SoraCurlCFFISidecarConfig
`mapstructure:"curl_cffi_sidecar"`
}
// SoraCurlCFFISidecarConfig Sora 专用 curl_cffi sidecar 配置
type
SoraCurlCFFISidecarConfig
struct
{
Enabled
bool
`mapstructure:"enabled"`
BaseURL
string
`mapstructure:"base_url"`
Impersonate
string
`mapstructure:"impersonate"`
TimeoutSeconds
int
`mapstructure:"timeout_seconds"`
}
}
// SoraStorageConfig 媒体存储配置
// SoraStorageConfig 媒体存储配置
...
@@ -1123,6 +1132,10 @@ func setDefaults() {
...
@@ -1123,6 +1132,10 @@ func setDefaults() {
viper
.
SetDefault
(
"sora.client.headers"
,
map
[
string
]
string
{})
viper
.
SetDefault
(
"sora.client.headers"
,
map
[
string
]
string
{})
viper
.
SetDefault
(
"sora.client.user_agent"
,
"Sora/1.2026.007 (Android 15; 24122RKC7C; build 2600700)"
)
viper
.
SetDefault
(
"sora.client.user_agent"
,
"Sora/1.2026.007 (Android 15; 24122RKC7C; build 2600700)"
)
viper
.
SetDefault
(
"sora.client.disable_tls_fingerprint"
,
false
)
viper
.
SetDefault
(
"sora.client.disable_tls_fingerprint"
,
false
)
viper
.
SetDefault
(
"sora.client.curl_cffi_sidecar.enabled"
,
true
)
viper
.
SetDefault
(
"sora.client.curl_cffi_sidecar.base_url"
,
"http://sora-curl-cffi-sidecar:8080"
)
viper
.
SetDefault
(
"sora.client.curl_cffi_sidecar.impersonate"
,
"chrome131"
)
viper
.
SetDefault
(
"sora.client.curl_cffi_sidecar.timeout_seconds"
,
60
)
viper
.
SetDefault
(
"sora.storage.type"
,
"local"
)
viper
.
SetDefault
(
"sora.storage.type"
,
"local"
)
viper
.
SetDefault
(
"sora.storage.local_path"
,
""
)
viper
.
SetDefault
(
"sora.storage.local_path"
,
""
)
...
@@ -1526,6 +1539,15 @@ func (c *Config) Validate() error {
...
@@ -1526,6 +1539,15 @@ func (c *Config) Validate() error {
c
.
Sora
.
Client
.
RecentTaskLimitMax
<
c
.
Sora
.
Client
.
RecentTaskLimit
{
c
.
Sora
.
Client
.
RecentTaskLimitMax
<
c
.
Sora
.
Client
.
RecentTaskLimit
{
c
.
Sora
.
Client
.
RecentTaskLimitMax
=
c
.
Sora
.
Client
.
RecentTaskLimit
c
.
Sora
.
Client
.
RecentTaskLimitMax
=
c
.
Sora
.
Client
.
RecentTaskLimit
}
}
if
c
.
Sora
.
Client
.
CurlCFFISidecar
.
TimeoutSeconds
<
0
{
return
fmt
.
Errorf
(
"sora.client.curl_cffi_sidecar.timeout_seconds must be non-negative"
)
}
if
!
c
.
Sora
.
Client
.
CurlCFFISidecar
.
Enabled
{
return
fmt
.
Errorf
(
"sora.client.curl_cffi_sidecar.enabled must be true"
)
}
if
strings
.
TrimSpace
(
c
.
Sora
.
Client
.
CurlCFFISidecar
.
BaseURL
)
==
""
{
return
fmt
.
Errorf
(
"sora.client.curl_cffi_sidecar.base_url is required"
)
}
if
c
.
Sora
.
Storage
.
MaxConcurrentDownloads
<
0
{
if
c
.
Sora
.
Storage
.
MaxConcurrentDownloads
<
0
{
return
fmt
.
Errorf
(
"sora.storage.max_concurrent_downloads must be non-negative"
)
return
fmt
.
Errorf
(
"sora.storage.max_concurrent_downloads must be non-negative"
)
}
}
...
...
backend/internal/config/config_test.go
View file @
36a1a799
...
@@ -1024,3 +1024,52 @@ func TestValidateConfigErrors(t *testing.T) {
...
@@ -1024,3 +1024,52 @@ func TestValidateConfigErrors(t *testing.T) {
})
})
}
}
}
}
func
TestSoraCurlCFFISidecarDefaults
(
t
*
testing
.
T
)
{
resetViperWithJWTSecret
(
t
)
cfg
,
err
:=
Load
()
if
err
!=
nil
{
t
.
Fatalf
(
"Load() error: %v"
,
err
)
}
if
!
cfg
.
Sora
.
Client
.
CurlCFFISidecar
.
Enabled
{
t
.
Fatalf
(
"Sora curl_cffi sidecar should be enabled by default"
)
}
if
cfg
.
Sora
.
Client
.
CurlCFFISidecar
.
BaseURL
==
""
{
t
.
Fatalf
(
"Sora curl_cffi sidecar base_url should not be empty by default"
)
}
if
cfg
.
Sora
.
Client
.
CurlCFFISidecar
.
Impersonate
==
""
{
t
.
Fatalf
(
"Sora curl_cffi sidecar impersonate should not be empty by default"
)
}
}
func
TestValidateSoraCurlCFFISidecarRequired
(
t
*
testing
.
T
)
{
resetViperWithJWTSecret
(
t
)
cfg
,
err
:=
Load
()
if
err
!=
nil
{
t
.
Fatalf
(
"Load() error: %v"
,
err
)
}
cfg
.
Sora
.
Client
.
CurlCFFISidecar
.
Enabled
=
false
err
=
cfg
.
Validate
()
if
err
==
nil
||
!
strings
.
Contains
(
err
.
Error
(),
"sora.client.curl_cffi_sidecar.enabled must be true"
)
{
t
.
Fatalf
(
"Validate() error = %v, want sidecar enabled error"
,
err
)
}
}
func
TestValidateSoraCurlCFFISidecarBaseURLRequired
(
t
*
testing
.
T
)
{
resetViperWithJWTSecret
(
t
)
cfg
,
err
:=
Load
()
if
err
!=
nil
{
t
.
Fatalf
(
"Load() error: %v"
,
err
)
}
cfg
.
Sora
.
Client
.
CurlCFFISidecar
.
BaseURL
=
" "
err
=
cfg
.
Validate
()
if
err
==
nil
||
!
strings
.
Contains
(
err
.
Error
(),
"sora.client.curl_cffi_sidecar.base_url is required"
)
{
t
.
Fatalf
(
"Validate() error = %v, want sidecar base_url required error"
,
err
)
}
}
backend/internal/service/sora_client.go
View file @
36a1a799
...
@@ -1630,6 +1630,14 @@ func shouldAttemptSoraTokenRecover(statusCode int, rawURL string) bool {
...
@@ -1630,6 +1630,14 @@ func shouldAttemptSoraTokenRecover(statusCode int, rawURL string) bool {
}
}
func
(
c
*
SoraDirectClient
)
doHTTP
(
req
*
http
.
Request
,
proxyURL
string
,
account
*
Account
)
(
*
http
.
Response
,
error
)
{
func
(
c
*
SoraDirectClient
)
doHTTP
(
req
*
http
.
Request
,
proxyURL
string
,
account
*
Account
)
(
*
http
.
Response
,
error
)
{
if
c
!=
nil
&&
c
.
cfg
!=
nil
&&
c
.
cfg
.
Sora
.
Client
.
CurlCFFISidecar
.
Enabled
{
resp
,
err
:=
c
.
doHTTPViaCurlCFFISidecar
(
req
,
proxyURL
)
if
err
!=
nil
{
return
nil
,
err
}
return
resp
,
nil
}
enableTLS
:=
c
==
nil
||
c
.
cfg
==
nil
||
!
c
.
cfg
.
Sora
.
Client
.
DisableTLSFingerprint
enableTLS
:=
c
==
nil
||
c
.
cfg
==
nil
||
!
c
.
cfg
.
Sora
.
Client
.
DisableTLSFingerprint
if
c
.
httpUpstream
!=
nil
{
if
c
.
httpUpstream
!=
nil
{
accountID
:=
int64
(
0
)
accountID
:=
int64
(
0
)
...
...
backend/internal/service/sora_client_test.go
View file @
36a1a799
...
@@ -4,6 +4,7 @@ package service
...
@@ -4,6 +4,7 @@ package service
import
(
import
(
"context"
"context"
"encoding/base64"
"encoding/json"
"encoding/json"
"errors"
"errors"
"io"
"io"
...
@@ -639,3 +640,144 @@ func TestSoraDirectClient_PostVideoForWatermarkFree(t *testing.T) {
...
@@ -639,3 +640,144 @@ func TestSoraDirectClient_PostVideoForWatermarkFree(t *testing.T) {
require
.
Equal
(
t
,
"/backend-api/sentinel/req"
,
upstream
.
calls
[
0
]
.
Path
)
require
.
Equal
(
t
,
"/backend-api/sentinel/req"
,
upstream
.
calls
[
0
]
.
Path
)
require
.
Equal
(
t
,
"/backend/project_y/post"
,
upstream
.
calls
[
1
]
.
Path
)
require
.
Equal
(
t
,
"/backend/project_y/post"
,
upstream
.
calls
[
1
]
.
Path
)
}
}
type
soraClientFallbackUpstream
struct
{
doWithTLSCalls
int32
respBody
string
respStatusCode
int
err
error
}
func
(
u
*
soraClientFallbackUpstream
)
Do
(
_
*
http
.
Request
,
_
string
,
_
int64
,
_
int
)
(
*
http
.
Response
,
error
)
{
return
nil
,
errors
.
New
(
"unexpected Do call"
)
}
func
(
u
*
soraClientFallbackUpstream
)
DoWithTLS
(
_
*
http
.
Request
,
_
string
,
_
int64
,
_
int
,
_
bool
)
(
*
http
.
Response
,
error
)
{
atomic
.
AddInt32
(
&
u
.
doWithTLSCalls
,
1
)
if
u
.
err
!=
nil
{
return
nil
,
u
.
err
}
statusCode
:=
u
.
respStatusCode
if
statusCode
<=
0
{
statusCode
=
http
.
StatusOK
}
body
:=
u
.
respBody
if
body
==
""
{
body
=
`{"ok":true}`
}
return
newSoraClientMockResponse
(
statusCode
,
body
),
nil
}
func
TestSoraDirectClient_DoHTTP_UsesCurlCFFISidecarWhenEnabled
(
t
*
testing
.
T
)
{
var
captured
soraCurlCFFISidecarRequest
sidecar
:=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
require
.
Equal
(
t
,
http
.
MethodPost
,
r
.
Method
)
require
.
Equal
(
t
,
"/request"
,
r
.
URL
.
Path
)
raw
,
err
:=
io
.
ReadAll
(
r
.
Body
)
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
json
.
Unmarshal
(
raw
,
&
captured
))
_
=
json
.
NewEncoder
(
w
)
.
Encode
(
map
[
string
]
any
{
"status_code"
:
http
.
StatusOK
,
"headers"
:
map
[
string
]
any
{
"Content-Type"
:
"application/json"
,
"X-Sidecar"
:
[]
string
{
"yes"
},
},
"body_base64"
:
base64
.
StdEncoding
.
EncodeToString
([]
byte
(
`{"ok":true}`
)),
})
}))
defer
sidecar
.
Close
()
upstream
:=
&
soraClientFallbackUpstream
{}
cfg
:=
&
config
.
Config
{
Sora
:
config
.
SoraConfig
{
Client
:
config
.
SoraClientConfig
{
BaseURL
:
"https://sora.chatgpt.com/backend"
,
CurlCFFISidecar
:
config
.
SoraCurlCFFISidecarConfig
{
Enabled
:
true
,
BaseURL
:
sidecar
.
URL
,
Impersonate
:
"chrome131"
,
TimeoutSeconds
:
15
,
},
},
},
}
client
:=
NewSoraDirectClient
(
cfg
,
upstream
,
nil
)
req
,
err
:=
http
.
NewRequest
(
http
.
MethodPost
,
"https://sora.chatgpt.com/backend/me"
,
strings
.
NewReader
(
"hello-sidecar"
))
require
.
NoError
(
t
,
err
)
req
.
Header
.
Set
(
"User-Agent"
,
"test-ua"
)
resp
,
err
:=
client
.
doHTTP
(
req
,
"http://127.0.0.1:18080"
,
&
Account
{
ID
:
1
})
require
.
NoError
(
t
,
err
)
defer
resp
.
Body
.
Close
()
body
,
err
:=
io
.
ReadAll
(
resp
.
Body
)
require
.
NoError
(
t
,
err
)
require
.
JSONEq
(
t
,
`{"ok":true}`
,
string
(
body
))
require
.
Equal
(
t
,
int32
(
0
),
atomic
.
LoadInt32
(
&
upstream
.
doWithTLSCalls
))
require
.
Equal
(
t
,
"http://127.0.0.1:18080"
,
captured
.
ProxyURL
)
require
.
Equal
(
t
,
"chrome131"
,
captured
.
Impersonate
)
require
.
Equal
(
t
,
"https://sora.chatgpt.com/backend/me"
,
captured
.
URL
)
decodedReqBody
,
err
:=
base64
.
StdEncoding
.
DecodeString
(
captured
.
BodyBase64
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
"hello-sidecar"
,
string
(
decodedReqBody
))
}
func
TestSoraDirectClient_DoHTTP_CurlCFFISidecarFailureReturnsError
(
t
*
testing
.
T
)
{
sidecar
:=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
w
.
WriteHeader
(
http
.
StatusBadGateway
)
_
,
_
=
w
.
Write
([]
byte
(
`{"error":"boom"}`
))
}))
defer
sidecar
.
Close
()
upstream
:=
&
soraClientFallbackUpstream
{
respBody
:
`{"fallback":true}`
}
cfg
:=
&
config
.
Config
{
Sora
:
config
.
SoraConfig
{
Client
:
config
.
SoraClientConfig
{
BaseURL
:
"https://sora.chatgpt.com/backend"
,
CurlCFFISidecar
:
config
.
SoraCurlCFFISidecarConfig
{
Enabled
:
true
,
BaseURL
:
sidecar
.
URL
,
},
},
},
}
client
:=
NewSoraDirectClient
(
cfg
,
upstream
,
nil
)
req
,
err
:=
http
.
NewRequest
(
http
.
MethodGet
,
"https://sora.chatgpt.com/backend/me"
,
nil
)
require
.
NoError
(
t
,
err
)
_
,
err
=
client
.
doHTTP
(
req
,
""
,
&
Account
{
ID
:
2
})
require
.
Error
(
t
,
err
)
require
.
Contains
(
t
,
err
.
Error
(),
"sora curl_cffi sidecar"
)
require
.
Equal
(
t
,
int32
(
0
),
atomic
.
LoadInt32
(
&
upstream
.
doWithTLSCalls
))
}
func
TestSoraDirectClient_DoHTTP_CurlCFFISidecarDisabledUsesLegacyStack
(
t
*
testing
.
T
)
{
upstream
:=
&
soraClientFallbackUpstream
{
respBody
:
`{"legacy":true}`
}
cfg
:=
&
config
.
Config
{
Sora
:
config
.
SoraConfig
{
Client
:
config
.
SoraClientConfig
{
BaseURL
:
"https://sora.chatgpt.com/backend"
,
CurlCFFISidecar
:
config
.
SoraCurlCFFISidecarConfig
{
Enabled
:
false
,
BaseURL
:
"http://127.0.0.1:18080"
,
},
},
},
}
client
:=
NewSoraDirectClient
(
cfg
,
upstream
,
nil
)
req
,
err
:=
http
.
NewRequest
(
http
.
MethodGet
,
"https://sora.chatgpt.com/backend/me"
,
nil
)
require
.
NoError
(
t
,
err
)
resp
,
err
:=
client
.
doHTTP
(
req
,
""
,
&
Account
{
ID
:
3
})
require
.
NoError
(
t
,
err
)
defer
resp
.
Body
.
Close
()
body
,
err
:=
io
.
ReadAll
(
resp
.
Body
)
require
.
NoError
(
t
,
err
)
require
.
JSONEq
(
t
,
`{"legacy":true}`
,
string
(
body
))
require
.
Equal
(
t
,
int32
(
1
),
atomic
.
LoadInt32
(
&
upstream
.
doWithTLSCalls
))
}
func
TestConvertSidecarHeaderValue_NilAndSlice
(
t
*
testing
.
T
)
{
require
.
Nil
(
t
,
convertSidecarHeaderValue
(
nil
))
require
.
Equal
(
t
,
[]
string
{
"a"
,
"b"
},
convertSidecarHeaderValue
([]
any
{
"a"
,
" "
,
"b"
}))
}
backend/internal/service/sora_curl_cffi_sidecar.go
0 → 100644
View file @
36a1a799
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"`
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
)
(
*
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
),
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
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
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
}
}
}
deploy/config.example.yaml
View file @
36a1a799
...
@@ -402,6 +402,21 @@ sora:
...
@@ -402,6 +402,21 @@ sora:
# Disable TLS fingerprint for Sora upstream
# Disable TLS fingerprint for Sora upstream
# 关闭 Sora 上游 TLS 指纹伪装
# 关闭 Sora 上游 TLS 指纹伪装
disable_tls_fingerprint
:
false
disable_tls_fingerprint
:
false
# curl_cffi sidecar for Sora only (required)
# 仅 Sora 链路使用的 curl_cffi sidecar(必需)
curl_cffi_sidecar
:
# Sora 强制通过 sidecar 请求,必须启用
# Sora is forced to use sidecar only; keep enabled=true
enabled
:
true
# Sidecar base URL (default endpoint: /request)
# sidecar 基础地址(默认请求端点:/request)
base_url
:
"
http://sora-curl-cffi-sidecar:8080"
# curl_cffi impersonate profile, e.g. chrome131/chrome124/safari18_0
# curl_cffi 指纹伪装 profile,例如 chrome131/chrome124/safari18_0
impersonate
:
"
chrome131"
# Sidecar request timeout (seconds)
# sidecar 请求超时(秒)
timeout_seconds
:
60
storage
:
storage
:
# Storage type (local only for now)
# Storage type (local only for now)
# 存储类型(首发仅支持 local)
# 存储类型(首发仅支持 local)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment