214 lines
5.1 KiB
Go
214 lines
5.1 KiB
Go
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
|
|
}
|