-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathratelimit.go
More file actions
103 lines (87 loc) · 2.16 KB
/
Copy pathratelimit.go
File metadata and controls
103 lines (87 loc) · 2.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
package smtp
import (
"sync"
"time"
)
// RateLimiter provides per-IP connection and message rate limiting.
type RateLimiter struct {
mu sync.Mutex
connections map[string]*rateWindow
messages map[string]*rateWindow
maxConnsPerMin int
maxMsgsPerMin int
}
type rateWindow struct {
timestamps []time.Time
}
// NewRateLimiter creates a rate limiter with the given per-minute limits.
func NewRateLimiter(connsPerMin, msgsPerMin int) *RateLimiter {
rl := &RateLimiter{
connections: make(map[string]*rateWindow),
messages: make(map[string]*rateWindow),
maxConnsPerMin: connsPerMin,
maxMsgsPerMin: msgsPerMin,
}
// Start cleanup goroutine
go rl.cleanup()
return rl
}
// AllowConnection checks if a new connection from this IP is allowed.
func (rl *RateLimiter) AllowConnection(ip string) bool {
return rl.allow(rl.connections, ip, rl.maxConnsPerMin)
}
// AllowMessage checks if a new message from this IP is allowed.
func (rl *RateLimiter) AllowMessage(ip string) bool {
return rl.allow(rl.messages, ip, rl.maxMsgsPerMin)
}
func (rl *RateLimiter) allow(windows map[string]*rateWindow, ip string, limit int) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
cutoff := now.Add(-time.Minute)
w, ok := windows[ip]
if !ok {
w = &rateWindow{}
windows[ip] = w
}
// Prune old timestamps
valid := w.timestamps[:0]
for _, t := range w.timestamps {
if t.After(cutoff) {
valid = append(valid, t)
}
}
w.timestamps = valid
if len(w.timestamps) >= limit {
return false
}
w.timestamps = append(w.timestamps, now)
return true
}
// cleanup periodically removes stale entries.
func (rl *RateLimiter) cleanup() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
rl.mu.Lock()
cutoff := time.Now().Add(-time.Minute)
cleanMap(rl.connections, cutoff)
cleanMap(rl.messages, cutoff)
rl.mu.Unlock()
}
}
func cleanMap(m map[string]*rateWindow, cutoff time.Time) {
for ip, w := range m {
valid := w.timestamps[:0]
for _, t := range w.timestamps {
if t.After(cutoff) {
valid = append(valid, t)
}
}
if len(valid) == 0 {
delete(m, ip)
} else {
w.timestamps = valid
}
}
}