From 7571d9fdabd2aaf92d9d2abf32ce9e55fc37486b Mon Sep 17 00:00:00 2001 From: Roberta001 <2596628651@qq.com> Date: Sat, 18 Apr 2026 08:00:30 +0800 Subject: [PATCH 1/2] feat: add proxy max concurrent requests per IP limit --- internal/bootstrap/data/setting.go | 1 + internal/conf/const.go | 1 + server/middlewares/ip_limit.go | 65 ++++++++++++++++++++++++++++++ server/router.go | 9 +++-- 4 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 server/middlewares/ip_limit.go diff --git a/internal/bootstrap/data/setting.go b/internal/bootstrap/data/setting.go index d7fd8ea47..f99ae26a1 100644 --- a/internal/bootstrap/data/setting.go +++ b/internal/bootstrap/data/setting.go @@ -238,6 +238,7 @@ func InitialSettings() []model.SettingItem { {Key: conf.TaskCopyThreadsNum, Value: strconv.Itoa(conf.Conf.Tasks.Copy.Workers), Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE}, {Key: conf.TaskDecompressDownloadThreadsNum, Value: strconv.Itoa(conf.Conf.Tasks.Decompress.Workers), Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE}, {Key: conf.TaskDecompressUploadThreadsNum, Value: strconv.Itoa(conf.Conf.Tasks.DecompressUpload.Workers), Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE}, + {Key: conf.ProxyMaxConcurrentRequestsPerIP, Value: "-1", Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE}, {Key: conf.StreamMaxClientDownloadSpeed, Value: "-1", Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE}, {Key: conf.StreamMaxClientUploadSpeed, Value: "-1", Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE}, {Key: conf.StreamMaxServerDownloadSpeed, Value: "-1", Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE}, diff --git a/internal/conf/const.go b/internal/conf/const.go index b99d8849c..23fce9fa0 100644 --- a/internal/conf/const.go +++ b/internal/conf/const.go @@ -157,6 +157,7 @@ const ( TaskMoveThreadsNum = "move_task_threads_num" TaskDecompressDownloadThreadsNum = "decompress_download_task_threads_num" TaskDecompressUploadThreadsNum = "decompress_upload_task_threads_num" + ProxyMaxConcurrentRequestsPerIP = "proxy_max_concurrent_requests_per_ip" StreamMaxClientDownloadSpeed = "max_client_download_speed" StreamMaxClientUploadSpeed = "max_client_upload_speed" StreamMaxServerDownloadSpeed = "max_server_download_speed" diff --git a/server/middlewares/ip_limit.go b/server/middlewares/ip_limit.go new file mode 100644 index 000000000..fd570a71d --- /dev/null +++ b/server/middlewares/ip_limit.go @@ -0,0 +1,65 @@ +package middlewares + +import ( + "net/http" + "sync" + + "github.com/OpenListTeam/OpenList/v4/internal/conf" + "github.com/OpenListTeam/OpenList/v4/internal/setting" + "github.com/gin-gonic/gin" +) + +var ( + proxyIPCounts = make(map[string]int) + proxyIPCountsMu sync.Mutex +) + +// ProxyIPConcurrencyLimit limits the maximum concurrent server proxy requests per IP Address +func ProxyIPConcurrencyLimit() gin.HandlerFunc { + return func(c *gin.Context) { + limit := setting.GetInt(conf.ProxyMaxConcurrentRequestsPerIP, -1) + + if limit < 0 { + // -1 or less means unlimited + c.Next() + return + } + + if limit == 0 { + // 0 means completely disabled + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ + "code": http.StatusForbidden, + "message": "Proxy is disabled", + "data": nil, + }) + return + } + + ip := c.ClientIP() + + proxyIPCountsMu.Lock() + count := proxyIPCounts[ip] + if count >= limit { + proxyIPCountsMu.Unlock() + c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{ + "code": http.StatusTooManyRequests, + "message": "Too Many Proxy Requests from this IP", + "data": nil, + }) + return + } + proxyIPCounts[ip] = count + 1 + proxyIPCountsMu.Unlock() + + defer func() { + proxyIPCountsMu.Lock() + proxyIPCounts[ip]-- + if proxyIPCounts[ip] <= 0 { + delete(proxyIPCounts, ip) + } + proxyIPCountsMu.Unlock() + }() + + c.Next() + } +} diff --git a/server/router.go b/server/router.go index 57d1166ae..09a4f8e74 100644 --- a/server/router.go +++ b/server/router.go @@ -43,17 +43,18 @@ func Init(e *gin.Engine) { S3(g.Group("/s3")) downloadLimiter := middlewares.DownloadRateLimiter(stream.ClientDownloadLimit) + proxyConcurrencyLimiter := middlewares.ProxyIPConcurrencyLimit() signCheck := middlewares.Down(sign.Verify) g.GET("/d/*path", middlewares.PathParse, signCheck, downloadLimiter, handles.Down) - g.GET("/p/*path", middlewares.PathParse, signCheck, downloadLimiter, handles.Proxy) + g.GET("/p/*path", middlewares.PathParse, signCheck, downloadLimiter, proxyConcurrencyLimiter, handles.Proxy) g.HEAD("/d/*path", middlewares.PathParse, signCheck, handles.Down) - g.HEAD("/p/*path", middlewares.PathParse, signCheck, handles.Proxy) + g.HEAD("/p/*path", middlewares.PathParse, signCheck, proxyConcurrencyLimiter, handles.Proxy) archiveSignCheck := middlewares.Down(sign.VerifyArchive) g.GET("/ad/*path", middlewares.PathParse, archiveSignCheck, downloadLimiter, handles.ArchiveDown) - g.GET("/ap/*path", middlewares.PathParse, archiveSignCheck, downloadLimiter, handles.ArchiveProxy) + g.GET("/ap/*path", middlewares.PathParse, archiveSignCheck, downloadLimiter, proxyConcurrencyLimiter, handles.ArchiveProxy) g.GET("/ae/*path", middlewares.PathParse, archiveSignCheck, downloadLimiter, handles.ArchiveInternalExtract) g.HEAD("/ad/*path", middlewares.PathParse, archiveSignCheck, handles.ArchiveDown) - g.HEAD("/ap/*path", middlewares.PathParse, archiveSignCheck, handles.ArchiveProxy) + g.HEAD("/ap/*path", middlewares.PathParse, archiveSignCheck, proxyConcurrencyLimiter, handles.ArchiveProxy) g.HEAD("/ae/*path", middlewares.PathParse, archiveSignCheck, handles.ArchiveInternalExtract) g.GET("/sd/:sid", middlewares.EmptyPathParse, middlewares.SharingIdParse, downloadLimiter, handles.SharingDown) From 82d4f3ed217a4b9f6894189fe32a9034b28c8ee0 Mon Sep 17 00:00:00 2001 From: Roberta001 <2596628651@qq.com> Date: Sat, 18 Apr 2026 08:45:37 +0800 Subject: [PATCH 2/2] feat(proxy): enhance IP concurrency limit with custom headers and standard error formatting --- internal/bootstrap/data/setting.go | 3 ++- internal/conf/const.go | 1 + server/middlewares/ip_limit.go | 37 +++++++++++++++++++++--------- server/router.go | 4 ++-- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/internal/bootstrap/data/setting.go b/internal/bootstrap/data/setting.go index f99ae26a1..669b296f6 100644 --- a/internal/bootstrap/data/setting.go +++ b/internal/bootstrap/data/setting.go @@ -238,7 +238,8 @@ func InitialSettings() []model.SettingItem { {Key: conf.TaskCopyThreadsNum, Value: strconv.Itoa(conf.Conf.Tasks.Copy.Workers), Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE}, {Key: conf.TaskDecompressDownloadThreadsNum, Value: strconv.Itoa(conf.Conf.Tasks.Decompress.Workers), Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE}, {Key: conf.TaskDecompressUploadThreadsNum, Value: strconv.Itoa(conf.Conf.Tasks.DecompressUpload.Workers), Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE}, - {Key: conf.ProxyMaxConcurrentRequestsPerIP, Value: "-1", Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE}, + {Key: conf.ProxyMaxConcurrentRequestsPerIP, Value: "-1", Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE, Help: "Limit the maximum number of concurrent proxy requests per IP. -1 means unlimited, 0 means disabled. NOTE: This limit relies on the client IP address. To prevent IP spoofing via headers, ensure your reverse proxy correctly overwrites X-Forwarded-For and X-Real-IP headers. Also, these counts are per-process; in multi-instance deployments, the effective limit is limit * N instances."}, + {Key: conf.ProxyClientIPHeader, Value: "", Type: conf.TypeString, Group: model.TRAFFIC, Flag: model.PRIVATE, Help: "Custom HTTP header to extract the client IP for proxy limits (e.g., 'X-Forwarded-For', 'CF-Connecting-IP'). Leave empty to use strict strict remote address (c.Request.RemoteAddr) which prevents IP spoofing but breaks behind reverse proxies without transparent IP capabilities."}, {Key: conf.StreamMaxClientDownloadSpeed, Value: "-1", Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE}, {Key: conf.StreamMaxClientUploadSpeed, Value: "-1", Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE}, {Key: conf.StreamMaxServerDownloadSpeed, Value: "-1", Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE}, diff --git a/internal/conf/const.go b/internal/conf/const.go index 23fce9fa0..f11aa170a 100644 --- a/internal/conf/const.go +++ b/internal/conf/const.go @@ -158,6 +158,7 @@ const ( TaskDecompressDownloadThreadsNum = "decompress_download_task_threads_num" TaskDecompressUploadThreadsNum = "decompress_upload_task_threads_num" ProxyMaxConcurrentRequestsPerIP = "proxy_max_concurrent_requests_per_ip" + ProxyClientIPHeader = "proxy_client_ip_header" StreamMaxClientDownloadSpeed = "max_client_download_speed" StreamMaxClientUploadSpeed = "max_client_upload_speed" StreamMaxServerDownloadSpeed = "max_server_download_speed" diff --git a/server/middlewares/ip_limit.go b/server/middlewares/ip_limit.go index fd570a71d..2ca12eb89 100644 --- a/server/middlewares/ip_limit.go +++ b/server/middlewares/ip_limit.go @@ -1,14 +1,19 @@ package middlewares import ( + "errors" + "net" "net/http" + "strings" "sync" "github.com/OpenListTeam/OpenList/v4/internal/conf" "github.com/OpenListTeam/OpenList/v4/internal/setting" + "github.com/OpenListTeam/OpenList/v4/server/common" "github.com/gin-gonic/gin" ) +// NOTE: Counts are per-process; in multi-instance deployments the effective limit is limit * N. var ( proxyIPCounts = make(map[string]int) proxyIPCountsMu sync.Mutex @@ -27,25 +32,35 @@ func ProxyIPConcurrencyLimit() gin.HandlerFunc { if limit == 0 { // 0 means completely disabled - c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ - "code": http.StatusForbidden, - "message": "Proxy is disabled", - "data": nil, - }) + common.ErrorPage(c, errors.New("Proxy is disabled"), http.StatusForbidden) return } - ip := c.ClientIP() + ipHeader := setting.GetStr(conf.ProxyClientIPHeader) + var ip string + + // Extract IP based on user configuration to prevent spoofing + if ipHeader != "" { + ip = c.Request.Header.Get(ipHeader) + if idx := strings.Index(ip, ","); idx != -1 { + ip = ip[:idx] + } + ip = strings.TrimSpace(ip) + } + + // Fallback to strict remote address if missing or not configured + if ip == "" { + ip = c.Request.RemoteAddr + if host, _, err := net.SplitHostPort(ip); err == nil { + ip = host + } + } proxyIPCountsMu.Lock() count := proxyIPCounts[ip] if count >= limit { proxyIPCountsMu.Unlock() - c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{ - "code": http.StatusTooManyRequests, - "message": "Too Many Proxy Requests from this IP", - "data": nil, - }) + common.ErrorPage(c, errors.New("Too Many Proxy Requests from this IP"), http.StatusTooManyRequests) return } proxyIPCounts[ip] = count + 1 diff --git a/server/router.go b/server/router.go index 09a4f8e74..4db213b9a 100644 --- a/server/router.go +++ b/server/router.go @@ -46,12 +46,12 @@ func Init(e *gin.Engine) { proxyConcurrencyLimiter := middlewares.ProxyIPConcurrencyLimit() signCheck := middlewares.Down(sign.Verify) g.GET("/d/*path", middlewares.PathParse, signCheck, downloadLimiter, handles.Down) - g.GET("/p/*path", middlewares.PathParse, signCheck, downloadLimiter, proxyConcurrencyLimiter, handles.Proxy) + g.GET("/p/*path", middlewares.PathParse, signCheck, proxyConcurrencyLimiter, downloadLimiter, handles.Proxy) g.HEAD("/d/*path", middlewares.PathParse, signCheck, handles.Down) g.HEAD("/p/*path", middlewares.PathParse, signCheck, proxyConcurrencyLimiter, handles.Proxy) archiveSignCheck := middlewares.Down(sign.VerifyArchive) g.GET("/ad/*path", middlewares.PathParse, archiveSignCheck, downloadLimiter, handles.ArchiveDown) - g.GET("/ap/*path", middlewares.PathParse, archiveSignCheck, downloadLimiter, proxyConcurrencyLimiter, handles.ArchiveProxy) + g.GET("/ap/*path", middlewares.PathParse, archiveSignCheck, proxyConcurrencyLimiter, downloadLimiter, handles.ArchiveProxy) g.GET("/ae/*path", middlewares.PathParse, archiveSignCheck, downloadLimiter, handles.ArchiveInternalExtract) g.HEAD("/ad/*path", middlewares.PathParse, archiveSignCheck, handles.ArchiveDown) g.HEAD("/ap/*path", middlewares.PathParse, archiveSignCheck, proxyConcurrencyLimiter, handles.ArchiveProxy)