feat(proxy,sora): 增强代理质量检测与Sora稳定性并修复审查问题
This commit is contained in:
213
backend/internal/service/sora_request_guard.go
Normal file
213
backend/internal/service/sora_request_guard.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/util/soraerror"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type soraChallengeCooldownEntry struct {
|
||||
Until time.Time
|
||||
StatusCode int
|
||||
CFRay string
|
||||
}
|
||||
|
||||
type soraSidecarSessionEntry struct {
|
||||
SessionKey string
|
||||
ExpiresAt time.Time
|
||||
LastUsedAt time.Time
|
||||
}
|
||||
|
||||
func (c *SoraDirectClient) cloudflareChallengeCooldownSeconds() int {
|
||||
if c == nil || c.cfg == nil {
|
||||
return 900
|
||||
}
|
||||
cooldown := c.cfg.Sora.Client.CloudflareChallengeCooldownSeconds
|
||||
if cooldown <= 0 {
|
||||
return 0
|
||||
}
|
||||
return cooldown
|
||||
}
|
||||
|
||||
func (c *SoraDirectClient) checkCloudflareChallengeCooldown(account *Account, proxyURL string) error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
if account == nil || account.ID <= 0 {
|
||||
return nil
|
||||
}
|
||||
cooldownSeconds := c.cloudflareChallengeCooldownSeconds()
|
||||
if cooldownSeconds <= 0 {
|
||||
return nil
|
||||
}
|
||||
key := soraAccountProxyKey(account, proxyURL)
|
||||
now := time.Now()
|
||||
|
||||
c.challengeCooldownMu.RLock()
|
||||
entry, ok := c.challengeCooldowns[key]
|
||||
c.challengeCooldownMu.RUnlock()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if !entry.Until.After(now) {
|
||||
c.challengeCooldownMu.Lock()
|
||||
delete(c.challengeCooldowns, key)
|
||||
c.challengeCooldownMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
remaining := int(math.Ceil(entry.Until.Sub(now).Seconds()))
|
||||
if remaining < 1 {
|
||||
remaining = 1
|
||||
}
|
||||
message := fmt.Sprintf("Sora request cooling down due to recent Cloudflare challenge. Retry in %d seconds.", remaining)
|
||||
if entry.CFRay != "" {
|
||||
message = fmt.Sprintf("%s (last cf-ray: %s)", message, entry.CFRay)
|
||||
}
|
||||
return &SoraUpstreamError{
|
||||
StatusCode: http.StatusTooManyRequests,
|
||||
Message: message,
|
||||
Headers: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SoraDirectClient) recordCloudflareChallengeCooldown(account *Account, proxyURL string, statusCode int, headers http.Header, body []byte) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
if account == nil || account.ID <= 0 {
|
||||
return
|
||||
}
|
||||
cooldownSeconds := c.cloudflareChallengeCooldownSeconds()
|
||||
if cooldownSeconds <= 0 {
|
||||
return
|
||||
}
|
||||
key := soraAccountProxyKey(account, proxyURL)
|
||||
now := time.Now()
|
||||
until := now.Add(time.Duration(cooldownSeconds) * time.Second)
|
||||
cfRay := soraerror.ExtractCloudflareRayID(headers, body)
|
||||
|
||||
c.challengeCooldownMu.Lock()
|
||||
c.cleanupExpiredChallengeCooldownsLocked(now)
|
||||
existing, ok := c.challengeCooldowns[key]
|
||||
if ok && existing.Until.After(until) {
|
||||
until = existing.Until
|
||||
if cfRay == "" {
|
||||
cfRay = existing.CFRay
|
||||
}
|
||||
}
|
||||
c.challengeCooldowns[key] = soraChallengeCooldownEntry{
|
||||
Until: until,
|
||||
StatusCode: statusCode,
|
||||
CFRay: cfRay,
|
||||
}
|
||||
c.challengeCooldownMu.Unlock()
|
||||
|
||||
if c.debugEnabled() {
|
||||
remain := int(math.Ceil(until.Sub(now).Seconds()))
|
||||
if remain < 0 {
|
||||
remain = 0
|
||||
}
|
||||
c.debugLogf("cloudflare_challenge_cooldown_set key=%s status=%d remain_s=%d cf_ray=%s", key, statusCode, remain, cfRay)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SoraDirectClient) sidecarSessionKey(account *Account, proxyURL string) string {
|
||||
if c == nil || !c.sidecarSessionReuseEnabled() {
|
||||
return ""
|
||||
}
|
||||
if account == nil || account.ID <= 0 {
|
||||
return ""
|
||||
}
|
||||
key := soraAccountProxyKey(account, proxyURL)
|
||||
now := time.Now()
|
||||
ttlSeconds := c.sidecarSessionTTLSeconds()
|
||||
|
||||
c.sidecarSessionMu.Lock()
|
||||
defer c.sidecarSessionMu.Unlock()
|
||||
c.cleanupExpiredSidecarSessionsLocked(now)
|
||||
if existing, exists := c.sidecarSessions[key]; exists {
|
||||
existing.LastUsedAt = now
|
||||
c.sidecarSessions[key] = existing
|
||||
return existing.SessionKey
|
||||
}
|
||||
|
||||
expiresAt := now.Add(time.Duration(ttlSeconds) * time.Second)
|
||||
if ttlSeconds <= 0 {
|
||||
expiresAt = now.Add(365 * 24 * time.Hour)
|
||||
}
|
||||
newEntry := soraSidecarSessionEntry{
|
||||
SessionKey: "sora-" + uuid.NewString(),
|
||||
ExpiresAt: expiresAt,
|
||||
LastUsedAt: now,
|
||||
}
|
||||
c.sidecarSessions[key] = newEntry
|
||||
|
||||
if c.debugEnabled() {
|
||||
c.debugLogf("sidecar_session_created key=%s ttl_s=%d", key, ttlSeconds)
|
||||
}
|
||||
return newEntry.SessionKey
|
||||
}
|
||||
|
||||
func (c *SoraDirectClient) cleanupExpiredChallengeCooldownsLocked(now time.Time) {
|
||||
if c == nil || len(c.challengeCooldowns) == 0 {
|
||||
return
|
||||
}
|
||||
for key, entry := range c.challengeCooldowns {
|
||||
if !entry.Until.After(now) {
|
||||
delete(c.challengeCooldowns, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SoraDirectClient) cleanupExpiredSidecarSessionsLocked(now time.Time) {
|
||||
if c == nil || len(c.sidecarSessions) == 0 {
|
||||
return
|
||||
}
|
||||
for key, entry := range c.sidecarSessions {
|
||||
if !entry.ExpiresAt.After(now) {
|
||||
delete(c.sidecarSessions, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func soraAccountProxyKey(account *Account, proxyURL string) string {
|
||||
accountID := int64(0)
|
||||
if account != nil {
|
||||
accountID = account.ID
|
||||
}
|
||||
return fmt.Sprintf("account:%d|proxy:%s", accountID, normalizeSoraProxyKey(proxyURL))
|
||||
}
|
||||
|
||||
func normalizeSoraProxyKey(proxyURL string) string {
|
||||
raw := strings.TrimSpace(proxyURL)
|
||||
if raw == "" {
|
||||
return "direct"
|
||||
}
|
||||
parsed, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return strings.ToLower(raw)
|
||||
}
|
||||
scheme := strings.ToLower(strings.TrimSpace(parsed.Scheme))
|
||||
host := strings.ToLower(strings.TrimSpace(parsed.Hostname()))
|
||||
port := strings.TrimSpace(parsed.Port())
|
||||
if host == "" {
|
||||
return strings.ToLower(raw)
|
||||
}
|
||||
if (scheme == "http" && port == "80") || (scheme == "https" && port == "443") {
|
||||
port = ""
|
||||
}
|
||||
if port != "" {
|
||||
host = host + ":" + port
|
||||
}
|
||||
if scheme == "" {
|
||||
scheme = "proxy"
|
||||
}
|
||||
return scheme + "://" + host
|
||||
}
|
||||
Reference in New Issue
Block a user