Files
new-api/middleware/task_artifact_access.go
T

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",
},
})
}