Skip to content

Commit e57bbac

Browse files
committed
feat: rate-limit
1 parent ac57eb1 commit e57bbac

8 files changed

Lines changed: 639 additions & 43 deletions

File tree

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
module github.com/psumaps/icalmiddleware
22

33
go 1.19
4+
5+
require golang.org/x/time v0.10.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
2+
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=

main.go

Lines changed: 90 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,33 @@ import (
88
"net/http"
99
"net/netip"
1010
"strings"
11+
"sync"
1112
"time"
13+
14+
"golang.org/x/time/rate"
1215
)
1316

1417
type Config struct {
15-
ForwardToken bool `json:"forwardToken,omitempty"`
16-
Freshness int64 `json:"freshness,omitempty"`
17-
HeaderName string `json:"headerName,omitempty"`
18-
AllowSubnet []string `json:"allowSubnet,omitempty"`
19-
Timeout int64 `json:"timeout,omitempty"`
18+
ForwardToken bool `json:"forwardToken,omitempty"`
19+
Freshness int64 `json:"freshness,omitempty"`
20+
HeaderName string `json:"headerName,omitempty"`
21+
AllowSubnet []string `json:"allowSubnet,omitempty"`
22+
Timeout int64 `json:"timeout,omitempty"`
23+
GlobalRateLimit int `json:"globalRateLimit,omitempty"`
24+
PerIPRateLimit int `json:"perIPRateLimit,omitempty"`
25+
RateLimitWindow int64 `json:"rateLimitWindow,omitempty"`
2026
}
2127

2228
func CreateConfig() *Config {
2329
return &Config{
24-
HeaderName: "Authorization",
25-
ForwardToken: false,
26-
Freshness: 3600,
27-
AllowSubnet: []string{"0.0.0.0/24"},
28-
Timeout: 5,
30+
HeaderName: "Authorization",
31+
ForwardToken: false,
32+
Freshness: 3600,
33+
AllowSubnet: []string{"0.0.0.0/24"},
34+
Timeout: 10,
35+
GlobalRateLimit: 100,
36+
PerIPRateLimit: 10,
37+
RateLimitWindow: 60,
2938
}
3039
}
3140

@@ -38,6 +47,13 @@ type ICalMiddleware struct {
3847
allowSubnet []netip.Prefix
3948
timeout time.Duration
4049
name string
50+
51+
// Поля для rate limiting
52+
globalLimiter *rate.Limiter
53+
ipLimiters map[string]*rate.Limiter
54+
ipLimiterMutex sync.Mutex
55+
perIPRateLimit int
56+
rateLimitWindow time.Duration
4157
}
4258

4359
func New(_ context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
@@ -60,15 +76,25 @@ func New(_ context.Context, next http.Handler, config *Config, name string) (htt
6076
}
6177
cache := NewCache(time.Duration(config.Freshness)*time.Second, 8*time.Hour)
6278

79+
var globalLimiter *rate.Limiter
80+
if config.GlobalRateLimit > 0 && config.RateLimitWindow > 0 {
81+
ratePerSec := float64(config.GlobalRateLimit) / float64(config.RateLimitWindow)
82+
globalLimiter = rate.NewLimiter(rate.Limit(ratePerSec), config.GlobalRateLimit)
83+
}
84+
6385
return &ICalMiddleware{
64-
headerName: config.HeaderName,
65-
forwardToken: config.ForwardToken,
66-
freshness: config.Freshness,
67-
allowSubnet: cidrs,
68-
next: next,
69-
cache: cache,
70-
timeout: timeout,
71-
name: name,
86+
headerName: config.HeaderName,
87+
forwardToken: config.ForwardToken,
88+
freshness: config.Freshness,
89+
allowSubnet: cidrs,
90+
next: next,
91+
cache: cache,
92+
timeout: timeout,
93+
name: name,
94+
globalLimiter: globalLimiter,
95+
ipLimiters: make(map[string]*rate.Limiter),
96+
perIPRateLimit: config.PerIPRateLimit,
97+
rateLimitWindow: time.Duration(config.RateLimitWindow) * time.Second,
7298
}, nil
7399
}
74100

@@ -110,7 +136,6 @@ func (plugin *ICalMiddleware) httpRequestAndCache(url string) error {
110136
return nil
111137
}
112138

113-
114139
func (plugin *ICalMiddleware) extractTokenFromHeader(request *http.Request) string {
115140
canonicalHeaderName := http.CanonicalHeaderKey(plugin.headerName)
116141
token := request.Header.Get(canonicalHeaderName)
@@ -130,14 +155,14 @@ func (plugin *ICalMiddleware) extractTokenFromHeader(request *http.Request) stri
130155
}
131156

132157
func ReadUserIP(r *http.Request) string {
133-
IPAddress := r.Header.Get("X-Real-Ip")
134-
if IPAddress == "" {
135-
IPAddress = r.Header.Get("X-Forwarded-For")
158+
ipAddress := r.Header.Get("X-Real-Ip")
159+
if ipAddress == "" {
160+
ipAddress = r.Header.Get("X-Forwarded-For")
136161
}
137-
if IPAddress == "" {
138-
IPAddress, _, _ = net.SplitHostPort(r.RemoteAddr)
162+
if ipAddress == "" {
163+
ipAddress, _, _ = net.SplitHostPort(r.RemoteAddr)
139164
}
140-
return IPAddress
165+
return ipAddress
141166
}
142167

143168
func (plugin *ICalMiddleware) containsSubnet(address string) bool {
@@ -161,29 +186,51 @@ func (plugin *ICalMiddleware) validate(request *http.Request) (int, error) {
161186
userIP := ReadUserIP(request)
162187
fmt.Printf("[DEBUG] [%s] Обработка запроса от IP: %s, URL: %s\n", plugin.name, userIP, request.URL.String())
163188

164-
if !plugin.containsSubnet(userIP) {
165-
token := plugin.extractTokenFromHeader(request)
166-
if token == "" {
167-
fmt.Printf("[ERROR] [%s] Токен не предоставлен в заголовке '%s' для запроса от IP %s\n", plugin.name, plugin.headerName, userIP)
168-
return http.StatusUnauthorized, fmt.Errorf("no token provided")
189+
if plugin.containsSubnet(userIP) {
190+
fmt.Printf("[DEBUG] [%s] IP %s входит в разрешённую подсет\n", plugin.name, userIP)
191+
return http.StatusOK, nil
192+
}
193+
194+
if plugin.globalLimiter != nil && !plugin.globalLimiter.Allow() {
195+
fmt.Printf("[ERROR] [%s] Превышен глобальный лимит запросов\n", plugin.name)
196+
return http.StatusTooManyRequests, fmt.Errorf("global rate limit exceeded")
197+
}
198+
199+
if plugin.perIPRateLimit > 0 && plugin.rateLimitWindow > 0 {
200+
plugin.ipLimiterMutex.Lock()
201+
limiter, exists := plugin.ipLimiters[userIP]
202+
if !exists {
203+
ratePerSec := float64(plugin.perIPRateLimit) / plugin.rateLimitWindow.Seconds()
204+
limiter = rate.NewLimiter(rate.Limit(ratePerSec), plugin.perIPRateLimit)
205+
plugin.ipLimiters[userIP] = limiter
169206
}
170-
if len(token) != 16 {
171-
fmt.Printf("[ERROR] [%s] Неверная длина токена '%s' для запроса от IP %s\n", plugin.name, token, userIP)
172-
return http.StatusUnauthorized, fmt.Errorf("incorrect token len")
207+
plugin.ipLimiterMutex.Unlock()
208+
if !limiter.Allow() {
209+
fmt.Printf("[ERROR] [%s] Превышен лимит запросов для IP: %s\n", plugin.name, userIP)
210+
return http.StatusTooManyRequests, fmt.Errorf("rate limit exceeded for IP %s", userIP)
173211
}
174-
if !plugin.cache.Has(token) {
175-
err := plugin.httpRequestAndCache(token)
176-
if err != nil {
177-
fmt.Printf("[ERROR] [%s] Проверка токена '%s' не пройдена для запроса от IP %s: %v\n", plugin.name, token, userIP, err)
178-
return http.StatusUnauthorized, err
179-
}
180-
fmt.Printf("[DEBUG] [%s] Токен '%s' валидирован и кэширован для IP %s\n", plugin.name, token, userIP)
181-
} else {
182-
fmt.Printf("[DEBUG] [%s] Токен '%s' найден в кэше для IP %s\n", plugin.name, token, userIP)
212+
}
213+
214+
token := plugin.extractTokenFromHeader(request)
215+
if token == "" {
216+
fmt.Printf("[ERROR] [%s] Токен не предоставлен в заголовке '%s' для запроса от IP %s\n", plugin.name, plugin.headerName, userIP)
217+
return http.StatusUnauthorized, fmt.Errorf("no token provided")
218+
}
219+
if len(token) != 16 {
220+
fmt.Printf("[ERROR] [%s] Неверная длина токена '%s' для запроса от IP %s\n", plugin.name, token, userIP)
221+
return http.StatusUnauthorized, fmt.Errorf("incorrect token len")
222+
}
223+
if !plugin.cache.Has(token) {
224+
err := plugin.httpRequestAndCache(token)
225+
if err != nil {
226+
fmt.Printf("[ERROR] [%s] Проверка токена '%s' не пройдена для запроса от IP %s: %v\n", plugin.name, token, userIP, err)
227+
return http.StatusUnauthorized, err
183228
}
229+
fmt.Printf("[DEBUG] [%s] Токен '%s' валидирован и кэширован для IP %s\n", plugin.name, token, userIP)
184230
} else {
185-
fmt.Printf("[DEBUG] [%s] Запрос от IP %s пропущен без проверки токена (разрешённая подсеть)\n", plugin.name, userIP)
231+
fmt.Printf("[DEBUG] [%s] Токен '%s' найден в кэше для IP %s\n", plugin.name, token, userIP)
186232
}
233+
187234
return http.StatusOK, nil
188235
}
189236

vendor/golang.org/x/time/LICENSE

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/golang.org/x/time/PATENTS

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)