Skip to content

Commit 82d4f3e

Browse files
Roberta001jyxjjj
authored andcommitted
feat(proxy): enhance IP concurrency limit with custom headers and standard error formatting
1 parent 7571d9f commit 82d4f3e

4 files changed

Lines changed: 31 additions & 14 deletions

File tree

internal/bootstrap/data/setting.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,8 @@ func InitialSettings() []model.SettingItem {
238238
{Key: conf.TaskCopyThreadsNum, Value: strconv.Itoa(conf.Conf.Tasks.Copy.Workers), Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},
239239
{Key: conf.TaskDecompressDownloadThreadsNum, Value: strconv.Itoa(conf.Conf.Tasks.Decompress.Workers), Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},
240240
{Key: conf.TaskDecompressUploadThreadsNum, Value: strconv.Itoa(conf.Conf.Tasks.DecompressUpload.Workers), Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},
241-
{Key: conf.ProxyMaxConcurrentRequestsPerIP, Value: "-1", Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},
241+
{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."},
242+
{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."},
242243
{Key: conf.StreamMaxClientDownloadSpeed, Value: "-1", Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},
243244
{Key: conf.StreamMaxClientUploadSpeed, Value: "-1", Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},
244245
{Key: conf.StreamMaxServerDownloadSpeed, Value: "-1", Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},

internal/conf/const.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ const (
158158
TaskDecompressDownloadThreadsNum = "decompress_download_task_threads_num"
159159
TaskDecompressUploadThreadsNum = "decompress_upload_task_threads_num"
160160
ProxyMaxConcurrentRequestsPerIP = "proxy_max_concurrent_requests_per_ip"
161+
ProxyClientIPHeader = "proxy_client_ip_header"
161162
StreamMaxClientDownloadSpeed = "max_client_download_speed"
162163
StreamMaxClientUploadSpeed = "max_client_upload_speed"
163164
StreamMaxServerDownloadSpeed = "max_server_download_speed"

server/middlewares/ip_limit.go

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
package middlewares
22

33
import (
4+
"errors"
5+
"net"
46
"net/http"
7+
"strings"
58
"sync"
69

710
"github.com/OpenListTeam/OpenList/v4/internal/conf"
811
"github.com/OpenListTeam/OpenList/v4/internal/setting"
12+
"github.com/OpenListTeam/OpenList/v4/server/common"
913
"github.com/gin-gonic/gin"
1014
)
1115

16+
// NOTE: Counts are per-process; in multi-instance deployments the effective limit is limit * N.
1217
var (
1318
proxyIPCounts = make(map[string]int)
1419
proxyIPCountsMu sync.Mutex
@@ -27,25 +32,35 @@ func ProxyIPConcurrencyLimit() gin.HandlerFunc {
2732

2833
if limit == 0 {
2934
// 0 means completely disabled
30-
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
31-
"code": http.StatusForbidden,
32-
"message": "Proxy is disabled",
33-
"data": nil,
34-
})
35+
common.ErrorPage(c, errors.New("Proxy is disabled"), http.StatusForbidden)
3536
return
3637
}
3738

38-
ip := c.ClientIP()
39+
ipHeader := setting.GetStr(conf.ProxyClientIPHeader)
40+
var ip string
41+
42+
// Extract IP based on user configuration to prevent spoofing
43+
if ipHeader != "" {
44+
ip = c.Request.Header.Get(ipHeader)
45+
if idx := strings.Index(ip, ","); idx != -1 {
46+
ip = ip[:idx]
47+
}
48+
ip = strings.TrimSpace(ip)
49+
}
50+
51+
// Fallback to strict remote address if missing or not configured
52+
if ip == "" {
53+
ip = c.Request.RemoteAddr
54+
if host, _, err := net.SplitHostPort(ip); err == nil {
55+
ip = host
56+
}
57+
}
3958

4059
proxyIPCountsMu.Lock()
4160
count := proxyIPCounts[ip]
4261
if count >= limit {
4362
proxyIPCountsMu.Unlock()
44-
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
45-
"code": http.StatusTooManyRequests,
46-
"message": "Too Many Proxy Requests from this IP",
47-
"data": nil,
48-
})
63+
common.ErrorPage(c, errors.New("Too Many Proxy Requests from this IP"), http.StatusTooManyRequests)
4964
return
5065
}
5166
proxyIPCounts[ip] = count + 1

server/router.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,12 @@ func Init(e *gin.Engine) {
4646
proxyConcurrencyLimiter := middlewares.ProxyIPConcurrencyLimit()
4747
signCheck := middlewares.Down(sign.Verify)
4848
g.GET("/d/*path", middlewares.PathParse, signCheck, downloadLimiter, handles.Down)
49-
g.GET("/p/*path", middlewares.PathParse, signCheck, downloadLimiter, proxyConcurrencyLimiter, handles.Proxy)
49+
g.GET("/p/*path", middlewares.PathParse, signCheck, proxyConcurrencyLimiter, downloadLimiter, handles.Proxy)
5050
g.HEAD("/d/*path", middlewares.PathParse, signCheck, handles.Down)
5151
g.HEAD("/p/*path", middlewares.PathParse, signCheck, proxyConcurrencyLimiter, handles.Proxy)
5252
archiveSignCheck := middlewares.Down(sign.VerifyArchive)
5353
g.GET("/ad/*path", middlewares.PathParse, archiveSignCheck, downloadLimiter, handles.ArchiveDown)
54-
g.GET("/ap/*path", middlewares.PathParse, archiveSignCheck, downloadLimiter, proxyConcurrencyLimiter, handles.ArchiveProxy)
54+
g.GET("/ap/*path", middlewares.PathParse, archiveSignCheck, proxyConcurrencyLimiter, downloadLimiter, handles.ArchiveProxy)
5555
g.GET("/ae/*path", middlewares.PathParse, archiveSignCheck, downloadLimiter, handles.ArchiveInternalExtract)
5656
g.HEAD("/ad/*path", middlewares.PathParse, archiveSignCheck, handles.ArchiveDown)
5757
g.HEAD("/ap/*path", middlewares.PathParse, archiveSignCheck, proxyConcurrencyLimiter, handles.ArchiveProxy)

0 commit comments

Comments
 (0)