├── .gitignore ├── iplimiter_test.go ├── README.md └── iplimiter.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | -------------------------------------------------------------------------------- /iplimiter_test.go: -------------------------------------------------------------------------------- 1 | package iplimiter 2 | 3 | import ( 4 | "testing" 5 | "github.com/gin-gonic/gin" 6 | "github.com/go-redis/redis" 7 | "time" 8 | "net/http" 9 | ) 10 | 11 | func TestIpLimiter(t *testing.T) { 12 | r := gin.Default() 13 | 14 | r.Use(NewRateLimiterMiddleware(redis.NewClient( 15 | &redis.Options{ 16 | DB: 0, 17 | Password: "", 18 | Addr: "127.0.0.1:6379", 19 | }, 20 | ), "test", 100, time.Second*5)) 21 | 22 | r.GET("/", func(c *gin.Context) { 23 | c.String(200,"OK") 24 | }) 25 | 26 | go r.Run(":9999") 27 | 28 | for i := 0; i < 102; i++ { 29 | c := &http.Client{} 30 | 31 | resp, e := c.Get("http://127.0.0.1:9999") 32 | if e != nil { 33 | t.Error("Error during requests ", e.Error()) 34 | return 35 | } 36 | 37 | switch { 38 | case i < 100: 39 | break; 40 | case i == 100: 41 | if resp.StatusCode != 429 { 42 | t.Error("Threashold break not detected") 43 | } else { 44 | time.Sleep(time.Second * 5) 45 | } 46 | break; 47 | case i == 101: 48 | if resp.StatusCode != 200 { 49 | t.Error("Unnecessary block") 50 | } 51 | break; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | It works with redis. It's pluggable and all you need is a redis client for every limiter. 4 | It limits path access based on client ip-address. 5 | 6 | It's inspired by this article https://engagor.github.io/blog/2017/05/02/sliding-window-rate-limiter-redis/. 7 | 8 | It uses this golang redis library https://github.com/go-redis/redis. 9 | 10 | ## Installation 11 | 12 | Just run 13 | 14 | `go get -u github.com/Salvatore-Giordano/gin-redis-ip-limiter/` 15 | 16 | ## Example 17 | 18 | ```go 19 | package main 20 | 21 | import ( 22 | "github.com/Salvatore-Giordano/gin-redis-ip-limiter" 23 | "github.com/gin-gonic/gin" 24 | ) 25 | 26 | func main() { 27 | r := gin.Default() 28 | r.Use(iplimiter.NewRateLimiterMiddleware(redis.NewClient(&redis.Options{ 29 | Addr: "localhost:6379", 30 | Password: "", 31 | DB: 1, 32 | }), "general", 200, 60*time.Second)) 33 | // ... 34 | r.Run(":8080") 35 | } 36 | ``` 37 | **Key**: this is the key used to save data on redis. Data are saved as ip:key. 38 | 39 | **Limit**: number of request to accept. 40 | 41 | **SlidingWindow**: duration of the sliding window to consider. 42 | 43 | -------------------------------------------------------------------------------- /iplimiter.go: -------------------------------------------------------------------------------- 1 | package iplimiter 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/go-redis/redis" 10 | ) 11 | 12 | func NewRateLimiterMiddleware(redisClient *redis.Client, key string, limit int, slidingWindow time.Duration) gin.HandlerFunc { 13 | 14 | _, err := redisClient.Ping().Result() 15 | if err != nil { 16 | panic(fmt.Sprint("error init redis", err.Error())) 17 | } 18 | 19 | return func(c *gin.Context) { 20 | now := time.Now().UnixNano() 21 | userCntKey := fmt.Sprint(c.ClientIP(), ":", key) 22 | 23 | redisClient.ZRemRangeByScore(userCntKey, 24 | "0", 25 | fmt.Sprint(now-(slidingWindow.Nanoseconds()))).Result() 26 | 27 | reqs, _ := redisClient.ZRange(userCntKey, 0, -1).Result() 28 | 29 | if len(reqs) >= limit { 30 | c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{ 31 | "status": http.StatusTooManyRequests, 32 | "message": "too many request", 33 | }) 34 | return 35 | } 36 | 37 | c.Next() 38 | redisClient.ZAddNX(userCntKey, redis.Z{Score: float64(now), Member: float64(now)}) 39 | redisClient.Expire(userCntKey, slidingWindow) 40 | } 41 | 42 | } 43 | --------------------------------------------------------------------------------