all repos

x @ ef0e1c202e9b8d5b9049d41cca5a6924e794ca74

go extra()

x/ratelimit/ratelimit.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
add ratelimit modules, 1 month ago
1
// Package ratelimit provides an HTTP middleware that rate-limits requests
2
// on a per-client basis using the token bucket algorithm.
3
//
4
// Ecample:
5
//
6
//	limiter := ratelimit.MiddlewareWithConfig(ratelimit.Config{
7
//	    RPS:   10,
8
//	    Burst: 20,
9
//	    TTL:   time.Minute,
10
//	})
11
//	http.Handle("/api", limiter(yourHandler))
12
//
13
package ratelimit
14
15
import (
16
	"net"
17
	"net/http"
18
	"strings"
19
	"sync"
20
	"time"
21
22
	"olexsmir.xyz/x/ratelimit/internal/time/rate"
23
)
24
25
type (
26
	rateLimiter struct {
27
		mu sync.RWMutex
28
29
		visitors map[visitorIP]*visitor
30
31
		// limit is the maximum number of requests per second
32
		limit rate.Limit
33
34
		// ttl is the time after which a visitor is forgotten
35
		ttl time.Duration
36
37
		// burst is the maximum number of requests allowed in a short burst
38
		burst int
39
	}
40
41
	visitorIP string
42
	visitor   struct {
43
		limiter  *rate.Limiter
44
		lastSeen time.Time
45
	}
46
)
47
48
func newLimiter(rps, burst int, ttl time.Duration) *rateLimiter {
49
	return &rateLimiter{
50
		visitors: make(map[visitorIP]*visitor),
51
		limit:    rate.Limit(rps),
52
		burst:    burst,
53
		ttl:      ttl,
54
	}
55
}
56
57
// getVisitor retrieves the rate limiter for the given IP, or creates one.
58
func (r *rateLimiter) getVisitor(ip visitorIP) *rate.Limiter {
59
	r.mu.RLock()
60
	v, exists := r.visitors[ip]
61
	r.mu.RUnlock()
62
63
	if !exists {
64
		limit := rate.NewLimiter(r.limit, r.burst)
65
66
		r.mu.Lock()
67
		r.visitors[ip] = &visitor{
68
			limiter:  limit,
69
			lastSeen: time.Now(),
70
		}
71
		r.mu.Unlock()
72
73
		return limit
74
	}
75
76
	r.mu.Lock()
77
	v.lastSeen = time.Now()
78
	r.mu.Unlock()
79
80
	return v.limiter
81
}
82
83
// cleanupVisitors removes visitors that haven't been seen for longer than TTL.
84
func (r *rateLimiter) cleanupVisitors() {
85
	r.mu.Lock()
86
	defer r.mu.Unlock()
87
88
	for ip, v := range r.visitors {
89
		if time.Since(v.lastSeen) > r.ttl {
90
			delete(r.visitors, ip)
91
		}
92
	}
93
}
94
95
// cleanupVisitorsLoop runs cleanupVisitors every minute.
96
func (r *rateLimiter) cleanupVisitorsLoop() {
97
	for {
98
		time.Sleep(time.Minute)
99
		r.cleanupVisitors()
100
	}
101
}
102
103
// Config configures the rate limiter middleware.
104
type Config struct {
105
	// RPS is the maximum number of requests per second per visitor.
106
	RPS int
107
108
	// TTL is the duration after which an idle visitor's limiter is cleaned up.
109
	TTL time.Duration
110
111
	// Burst is the maximum number of requests that can be made in a short burst.
112
	Burst int
113
}
114
115
// Middleware returns a rate-limiting HTTP middleware.
116
//
117
// It limits requests per client IP using the token bucket algorithm.
118
// When the limit is exceeded it responds with 429 Too Many Requests.
119
func Middleware(c Config) func(http.Handler) http.Handler {
120
	lmt := newLimiter(c.RPS, c.Burst, c.TTL)
121
	go lmt.cleanupVisitorsLoop()
122
123
	return func(next http.Handler) http.Handler {
124
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
125
			ip := getIP(r)
126
			limiter := lmt.getVisitor(ip)
127
			if limiter == nil {
128
				http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
129
				return
130
			}
131
132
			if !limiter.Allow() {
133
				http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
134
				return
135
			}
136
137
			next.ServeHTTP(w, r)
138
		})
139
	}
140
}
141
142
func getIP(r *http.Request) visitorIP {
143
	if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
144
		if i := strings.IndexByte(xff, ','); i != -1 {
145
			xff = xff[:i]
146
		}
147
		if ip := strings.TrimSpace(xff); ip != "" {
148
			return visitorIP(ip)
149
		}
150
	}
151
152
	if xri := r.Header.Get("X-Real-IP"); xri != "" {
153
		return visitorIP(xri)
154
	}
155
156
	if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
157
		return visitorIP(host)
158
	}
159
160
	return visitorIP(r.RemoteAddr)
161
}