mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-08 19:06:22 +00:00
246 lines
6.4 KiB
Go
246 lines
6.4 KiB
Go
package middleware
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/QuantumNous/new-api/service"
|
|
"github.com/QuantumNous/new-api/setting/system_setting"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
const TaskArtifactAccessContextKey = "task_artifact_access"
|
|
|
|
const (
|
|
taskArtifactAccessRawContextKey = "task_artifact_access_raw"
|
|
taskArtifactAccessPresentContextKey = "task_artifact_access_present"
|
|
taskArtifactAccessInvalidContextKey = "task_artifact_access_invalid"
|
|
taskArtifactAccessRateWindow = time.Minute
|
|
taskArtifactAccessCleanupInterval = time.Minute
|
|
maxEncodedTaskArtifactAccessQuerySize = 128
|
|
)
|
|
|
|
type taskArtifactRateEntry struct {
|
|
windowStart time.Time
|
|
count int
|
|
}
|
|
|
|
type taskArtifactAccessLimiter struct {
|
|
mutex sync.Mutex
|
|
global int
|
|
byIP map[string]int
|
|
byObject map[string]int
|
|
rates map[string]taskArtifactRateEntry
|
|
nextCleanup time.Time
|
|
limits system_setting.TaskArtifactAccessLimits
|
|
}
|
|
|
|
var taskArtifactAnonymousLimiter = newTaskArtifactAccessLimiter(
|
|
system_setting.LoadTaskArtifactAccessLimits(),
|
|
)
|
|
|
|
func newTaskArtifactAccessLimiter(limits system_setting.TaskArtifactAccessLimits) *taskArtifactAccessLimiter {
|
|
return &taskArtifactAccessLimiter{
|
|
byIP: make(map[string]int),
|
|
byObject: make(map[string]int),
|
|
rates: make(map[string]taskArtifactRateEntry),
|
|
limits: limits,
|
|
}
|
|
}
|
|
|
|
func (l *taskArtifactAccessLimiter) invalidAttempt(now time.Time, ip string) bool {
|
|
l.mutex.Lock()
|
|
defer l.mutex.Unlock()
|
|
|
|
if l.nextCleanup.IsZero() || !now.Before(l.nextCleanup) {
|
|
for key, entry := range l.rates {
|
|
if now.Sub(entry.windowStart) >= taskArtifactAccessRateWindow {
|
|
delete(l.rates, key)
|
|
}
|
|
}
|
|
l.nextCleanup = now.Add(taskArtifactAccessCleanupInterval)
|
|
}
|
|
|
|
rate := l.rates[ip]
|
|
if rate.windowStart.IsZero() || now.Sub(rate.windowStart) >= taskArtifactAccessRateWindow {
|
|
rate = taskArtifactRateEntry{windowStart: now}
|
|
}
|
|
if rate.count >= l.limits.InvalidRatePerMinute {
|
|
return false
|
|
}
|
|
rate.count++
|
|
l.rates[ip] = rate
|
|
return true
|
|
}
|
|
|
|
func (l *taskArtifactAccessLimiter) acquire(ip, taskID, artifactKey string) (func(), bool) {
|
|
l.mutex.Lock()
|
|
defer l.mutex.Unlock()
|
|
objectKey := taskID + "\x00" + artifactKey
|
|
if l.global >= l.limits.GlobalConcurrency ||
|
|
l.byIP[ip] >= l.limits.IPConcurrency ||
|
|
l.byObject[objectKey] >= l.limits.ObjectConcurrency {
|
|
return nil, false
|
|
}
|
|
|
|
l.global++
|
|
l.byIP[ip]++
|
|
l.byObject[objectKey]++
|
|
|
|
var once sync.Once
|
|
return func() {
|
|
once.Do(func() {
|
|
l.mutex.Lock()
|
|
defer l.mutex.Unlock()
|
|
l.global--
|
|
l.byIP[ip]--
|
|
l.byObject[objectKey]--
|
|
if l.byIP[ip] == 0 {
|
|
delete(l.byIP, ip)
|
|
}
|
|
if l.byObject[objectKey] == 0 {
|
|
delete(l.byObject, objectKey)
|
|
}
|
|
})
|
|
}, true
|
|
}
|
|
|
|
func redactTaskArtifactAccessQuery() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
path := c.Request.URL.Path
|
|
isArtifactContent := strings.HasPrefix(path, "/v1/tasks/") &&
|
|
strings.Contains(path, "/artifacts/") &&
|
|
strings.HasSuffix(path, "/content")
|
|
isLegacyVideoContent := strings.HasPrefix(path, "/v1/videos/") &&
|
|
strings.HasSuffix(path, "/content")
|
|
if !isArtifactContent && !isLegacyVideoContent {
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
rawAccess, present, invalid := popTaskArtifactAccessQuery(c.Request)
|
|
if present {
|
|
c.Set(taskArtifactAccessRawContextKey, rawAccess)
|
|
c.Set(taskArtifactAccessPresentContextKey, true)
|
|
c.Set(taskArtifactAccessInvalidContextKey, invalid)
|
|
}
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
func popTaskArtifactAccessQuery(request *http.Request) (string, bool, bool) {
|
|
if request == nil || request.URL == nil {
|
|
return "", false, false
|
|
}
|
|
rawAccess := ""
|
|
count := 0
|
|
invalid := false
|
|
kept := make([]string, 0)
|
|
for _, part := range strings.Split(request.URL.RawQuery, "&") {
|
|
rawKey, rawValue, _ := strings.Cut(part, "=")
|
|
key, err := url.QueryUnescape(rawKey)
|
|
if err != nil || key != service.TaskArtifactAccessQueryParameter {
|
|
kept = append(kept, part)
|
|
continue
|
|
}
|
|
count++
|
|
if len(rawValue) > maxEncodedTaskArtifactAccessQuerySize {
|
|
invalid = true
|
|
continue
|
|
}
|
|
if count == 1 {
|
|
value, decodeErr := url.QueryUnescape(rawValue)
|
|
if decodeErr != nil {
|
|
invalid = true
|
|
} else {
|
|
rawAccess = value
|
|
}
|
|
}
|
|
}
|
|
if count == 0 {
|
|
return "", false, false
|
|
}
|
|
invalid = invalid || count != 1
|
|
request.URL.RawQuery = strings.Join(kept, "&")
|
|
request.RequestURI = request.URL.RequestURI()
|
|
return rawAccess, true, invalid
|
|
}
|
|
|
|
// TokenOrTaskArtifactAccessAuth accepts the normal relay API Bearer token or a
|
|
// route-bound capability. Capabilities are verified before any database read.
|
|
func TokenOrTaskArtifactAccessAuth(taskParam, artifactParam string) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
c.Header("Cache-Control", "private, no-store")
|
|
|
|
rawAccess := c.GetString(taskArtifactAccessRawContextKey)
|
|
present := c.GetBool(taskArtifactAccessPresentContextKey)
|
|
invalid := c.GetBool(taskArtifactAccessInvalidContextKey)
|
|
if queryAccess, queryPresent, queryInvalid := popTaskArtifactAccessQuery(c.Request); queryPresent {
|
|
present = true
|
|
if rawAccess == "" {
|
|
rawAccess = queryAccess
|
|
}
|
|
invalid = invalid || queryInvalid
|
|
}
|
|
if !present {
|
|
TokenAuth()(c)
|
|
return
|
|
}
|
|
|
|
taskID := c.Param(taskParam)
|
|
artifactKey := c.Param(artifactParam)
|
|
ip := c.ClientIP()
|
|
if ip == "" {
|
|
ip = "unknown"
|
|
}
|
|
if invalid || !service.VerifyTaskArtifactAccess(rawAccess, taskID, artifactKey) {
|
|
if !taskArtifactAnonymousLimiter.invalidAttempt(time.Now(), ip) {
|
|
writeTaskArtifactAccessLimited(c)
|
|
return
|
|
}
|
|
writeTaskArtifactAccessNotFound(c)
|
|
return
|
|
}
|
|
|
|
release, ok := taskArtifactAnonymousLimiter.acquire(ip, taskID, artifactKey)
|
|
if !ok {
|
|
writeTaskArtifactAccessLimited(c)
|
|
return
|
|
}
|
|
defer release()
|
|
|
|
c.Set(TaskArtifactAccessContextKey, true)
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
func IsTaskArtifactAccess(c *gin.Context) bool {
|
|
return c != nil && c.GetBool(TaskArtifactAccessContextKey)
|
|
}
|
|
|
|
func writeTaskArtifactAccessNotFound(c *gin.Context) {
|
|
c.Header("Cache-Control", "private, no-store")
|
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
|
"error": gin.H{
|
|
"message": "Task or artifact not found",
|
|
"type": "artifact_not_found",
|
|
"code": "artifact_not_found",
|
|
},
|
|
})
|
|
}
|
|
|
|
func writeTaskArtifactAccessLimited(c *gin.Context) {
|
|
c.Header("Cache-Control", "private, no-store")
|
|
c.Header("Retry-After", "60")
|
|
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
|
|
"error": gin.H{
|
|
"message": "Artifact access limit exceeded",
|
|
"type": "rate_limit_error",
|
|
"code": "artifact_access_limited",
|
|
},
|
|
})
|
|
}
|