-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathratelimit.go
More file actions
135 lines (112 loc) · 3.79 KB
/
ratelimit.go
File metadata and controls
135 lines (112 loc) · 3.79 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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package limidder
import (
"errors"
"fmt"
"github.com/go-redis/redis"
"net/http"
"strconv"
"time"
)
// add yaml and json tag so people can use config file
type Config struct {
Limit uint64 `yaml:"limit" json:"limit"`
Duration int64 `yaml:"duration" json:"duration"`
}
type Request struct {
Key string
Limit uint64
Duration time.Duration
}
type Result struct {
State State
TotalRequests uint64
ExpiresAt time.Time
}
type RateLimiterConfig struct {
Extractor Extractor
StrategyName string
strategy Strategy
Config map[string]*Config
ApplyConfigToAllPaths bool
ApplyUserRateLimitToAllPaths bool
}
func (r *RateLimiterConfig) SetStrategy(strategyName string, client *redis.Client) {
switch strategyName {
case SlidingWindow:
r.strategy = NewSlidingWindowStrategy(client)
default:
r.strategy = nil
}
}
type httpRateLimiterHandler struct {
config *RateLimiterConfig
}
func InitRateLimiterMiddleware(config *RateLimiterConfig, client *redis.Client) *httpRateLimiterHandler {
config.SetStrategy(config.StrategyName, client)
return &httpRateLimiterHandler{
config: config,
}
}
func (h *httpRateLimiterHandler) writeRespone(writer http.ResponseWriter, status int, msg string, args ...interface{}) {
writer.Header().Set("Content-Type", "text/plain")
writer.WriteHeader(status)
if _, err := writer.Write([]byte(fmt.Sprintf(msg, args...))); err != nil {
fmt.Printf("failed to write body to HTTP request: %v", err)
}
}
func (h *httpRateLimiterHandler) getLimitAndDuration(request *http.Request) (limit uint64, duration time.Duration, err error) {
var configData *Config
if h.config.ApplyConfigToAllPaths {
configData = h.config.Config["all"]
if configData == nil {
return limit, duration, errRateLimitNotSetForAllPaths
}
} else {
route := request.Method + " " + request.URL.String()
configData = h.config.Config[route]
if configData == nil {
return limit, duration, errRateLimitNotSet
}
}
limit = configData.Limit
duration = time.Duration(configData.Duration * int64(time.Second))
return
}
func (h *httpRateLimiterHandler) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
key, err := h.config.Extractor.ExtractKey(request, h.config.ApplyUserRateLimitToAllPaths)
if err != nil {
h.writeRespone(writer, http.StatusBadRequest, "failed to collect rate limiting key from request: %v", err)
return
}
limit, duration, err := h.getLimitAndDuration(request)
// not all paths need to be rate limited, so if the config is not set, just let it passes
if errors.Is(err, errRateLimitNotSet) {
next.ServeHTTP(writer, request)
return
}
if err != nil {
h.writeRespone(writer, http.StatusInternalServerError, "failed to run rate limiting for request: %v", err)
return
}
result, err := h.config.strategy.Run(request.Context(), &Request{
Key: key,
Limit: limit,
Duration: duration,
})
if err != nil {
h.writeRespone(writer, http.StatusInternalServerError, "failed to run rate limiting for request: %v", err)
return
}
// set the rate limiting headers both on allow or deny results so the client knows what is going on
writer.Header().Set(rateLimitingTotalRequests, strconv.FormatUint(result.TotalRequests, 10))
writer.Header().Set(rateLimitingState, stateStrings[result.State])
writer.Header().Set(rateLimitingExpiresAt, result.ExpiresAt.Format(time.RFC3339))
// when the state is Deny, just return a 429 response to the client and stop the request handling flow
if result.State == Deny {
h.writeRespone(writer, http.StatusTooManyRequests, "you have sent too many requests to this service, slow down please")
return
}
next.ServeHTTP(writer, request)
})
}