-
Notifications
You must be signed in to change notification settings - Fork 0
/
limiter.go
144 lines (128 loc) · 3.55 KB
/
limiter.go
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
136
137
138
139
140
141
142
143
144
package crossover_limiter
import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/kotalco/resp"
"log"
"net/http"
"regexp"
)
const (
UserRateKeySuffix = "-rate"
)
// Config holds configuration to passed to the plugin
type Config struct {
RequestIdPattern string
RateLimitPlanLimitURL string
APIKey string
RedisAuth string
RedisAddress string
}
// CreateConfig populates the config data object
func CreateConfig() *Config {
return &Config{}
}
type Limiter struct {
next http.Handler
name string
compiledPattern *regexp.Regexp
redisAuth string
redisAddress string
planProxy IPlanProxy
}
// New created a new plugin.
func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
if len(config.APIKey) == 0 {
return nil, fmt.Errorf("APIKey can't be empty")
}
if len(config.RequestIdPattern) == 0 {
return nil, fmt.Errorf("GetRequestIdPattern can't be empty")
}
if len(config.RateLimitPlanLimitURL) == 0 {
return nil, fmt.Errorf("RateLimitPlanLimitURL can't be empty")
}
if len(config.RedisAddress) == 0 {
return nil, fmt.Errorf("RedisAddress can't be empty")
}
compiledPattern := regexp.MustCompile(config.RequestIdPattern)
planProxy := NewPlanProxy(config.APIKey, config.RateLimitPlanLimitURL)
handler := &Limiter{
next: next,
name: name,
compiledPattern: compiledPattern,
redisAuth: config.RedisAuth,
redisAddress: config.RedisAddress,
planProxy: planProxy,
}
return handler, nil
}
// ServeHTTP serve http request for the users
func (a *Limiter) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
respClient, err := resp.NewRedisClient(a.redisAddress, a.redisAuth)
if err != nil {
log.Printf("Failed to create Redis Connection %s", err.Error())
rw.WriteHeader(http.StatusInternalServerError)
rw.Write([]byte("something went wrong"))
return
}
defer respClient.Close()
planCache := NewPlanCache(respClient)
userId := a.extractUserID(req.URL.Path)
if userId == "" {
rw.WriteHeader(http.StatusBadRequest)
rw.Write([]byte("invalid requestId"))
return
}
userPlan, err := planCache.getUserPlan(req.Context(), userId)
if err != nil {
log.Println(err.Error())
rw.WriteHeader(http.StatusInternalServerError)
rw.Write([]byte(err.Error()))
return
}
if err == nil && userPlan == 0 {
userPlan, err = a.planProxy.fetch(userId)
if err != nil {
log.Println(err.Error())
rw.WriteHeader(http.StatusInternalServerError)
rw.Write([]byte("something went wrong"))
return
}
//set user plan
err = planCache.setUserPlan(req.Context(), userId, userPlan)
if err != nil {
log.Println(err.Error())
rw.WriteHeader(http.StatusInternalServerError)
rw.Write([]byte("something went wrong"))
return
}
}
key := fmt.Sprintf("%s%s", userId, UserRateKeySuffix)
allow, err := planCache.limit(req.Context(), key, userPlan, 1)
if err != nil {
log.Println(err.Error())
rw.WriteHeader(http.StatusInternalServerError)
rw.Write([]byte("something went wrong"))
return
}
if !allow {
rw.WriteHeader(http.StatusTooManyRequests)
rw.Write([]byte("too many requests"))
return
}
a.next.ServeHTTP(rw, req)
}
// extractUserID extract user id from the request
func (a *Limiter) extractUserID(path string) (userId string) {
// Find the first match of the pattern in the URL Path
match := a.compiledPattern.FindStringSubmatch(path)
if len(match) == 0 {
return
}
parsedUUID, err := uuid.Parse(match[0][10:])
if err != nil {
return
}
return parsedUUID.String()
}