├── .gitignore ├── .github └── ISSUE_TEMPLATE │ └── bug-report.md ├── LICENSE ├── go.mod ├── gin_rate_limit.go ├── in_memory.go ├── README.md ├── redis.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | /tests -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Bug report template 4 | title: '' 5 | labels: bug 6 | assignees: Nebulizer1213 7 | 8 | --- 9 | 10 | # Bug Report 11 | 12 | Before you submit this request make sure you are using the latest version of GinRateLimit. 13 | 14 | ### Go version 15 | 16 | 17 | 18 | ### OS 19 | 20 | 21 | 22 | ### Code to generate the bug 23 | 24 | 25 | ```go 26 | // Code to generate the bug 27 | ``` 28 | 29 | 30 | ### What is expected to happen 31 | 32 | 33 | 34 | ### What actually happens 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 JGL Technologies 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/JGLTechnologies/gin-rate-limit 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.10.0 7 | github.com/redis/go-redis/v9 v9.7.3 8 | ) 9 | 10 | require ( 11 | github.com/goccy/go-json v0.10.5 // indirect 12 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 13 | golang.org/x/net v0.39.0 // indirect 14 | golang.org/x/text v0.24.0 // indirect 15 | ) 16 | 17 | require ( 18 | github.com/bytedance/sonic v1.13.2 // indirect 19 | github.com/bytedance/sonic/loader v0.2.4 // indirect 20 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 21 | github.com/cloudwego/base64x v0.1.5 // indirect 22 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 23 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 24 | github.com/gin-contrib/sse v1.1.0 // indirect 25 | github.com/go-playground/locales v0.14.1 // indirect 26 | github.com/go-playground/universal-translator v0.18.1 // indirect 27 | github.com/go-playground/validator/v10 v10.26.0 // indirect 28 | github.com/google/go-cmp v0.7.0 // indirect 29 | github.com/json-iterator/go v1.1.12 // indirect 30 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 31 | github.com/kr/pretty v0.1.0 // indirect 32 | github.com/kr/text v0.2.0 // indirect 33 | github.com/leodido/go-urn v1.4.0 // indirect 34 | github.com/mattn/go-isatty v0.0.20 // indirect 35 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 36 | github.com/modern-go/reflect2 v1.0.2 // indirect 37 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 38 | github.com/ugorji/go/codec v1.2.12 // indirect 39 | golang.org/x/arch v0.16.0 // indirect 40 | golang.org/x/crypto v0.37.0 // indirect 41 | golang.org/x/sys v0.32.0 // indirect 42 | google.golang.org/protobuf v1.36.6 // indirect 43 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 44 | gopkg.in/yaml.v3 v3.0.1 // indirect 45 | ) 46 | -------------------------------------------------------------------------------- /gin_rate_limit.go: -------------------------------------------------------------------------------- 1 | package ratelimit 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "time" 7 | ) 8 | 9 | type Info struct { 10 | Limit uint 11 | RateLimited bool 12 | ResetTime time.Time 13 | RemainingHits uint 14 | } 15 | 16 | type Store interface { 17 | // Limit takes in a key and *gin.Context and should return whether that key is allowed to make another request 18 | Limit(key string, c *gin.Context) Info 19 | } 20 | 21 | type Options struct { 22 | ErrorHandler func(*gin.Context, Info) 23 | KeyFunc func(*gin.Context) string 24 | // a function that lets you check the rate limiting info and modify the response 25 | BeforeResponse func(c *gin.Context, info Info) 26 | } 27 | 28 | // RateLimiter is a function to get gin.HandlerFunc 29 | func RateLimiter(s Store, options *Options) gin.HandlerFunc { 30 | if options == nil { 31 | options = &Options{} 32 | } 33 | if options.ErrorHandler == nil { 34 | options.ErrorHandler = func(c *gin.Context, info Info) { 35 | c.Header("X-Rate-Limit-Limit", fmt.Sprintf("%d", info.Limit)) 36 | c.Header("X-Rate-Limit-Reset", fmt.Sprintf("%d", info.ResetTime.Unix())) 37 | c.String(429, "Too many requests") 38 | } 39 | } 40 | if options.BeforeResponse == nil { 41 | options.BeforeResponse = func(c *gin.Context, info Info) { 42 | c.Header("X-Rate-Limit-Limit", fmt.Sprintf("%d", info.Limit)) 43 | c.Header("X-Rate-Limit-Remaining", fmt.Sprintf("%v", info.RemainingHits)) 44 | c.Header("X-Rate-Limit-Reset", fmt.Sprintf("%d", info.ResetTime.Unix())) 45 | } 46 | } 47 | if options.KeyFunc == nil { 48 | options.KeyFunc = func(c *gin.Context) string { 49 | return c.ClientIP() + c.FullPath() 50 | } 51 | } 52 | return func(c *gin.Context) { 53 | key := options.KeyFunc(c) 54 | info := s.Limit(key, c) 55 | options.BeforeResponse(c, info) 56 | if c.IsAborted() { 57 | return 58 | } 59 | if info.RateLimited { 60 | options.ErrorHandler(c, info) 61 | c.Abort() 62 | } else { 63 | c.Next() 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /in_memory.go: -------------------------------------------------------------------------------- 1 | package ratelimit 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | type user struct { 10 | ts int64 11 | tokens uint 12 | } 13 | 14 | func clearInBackground(data *sync.Map, rate int64) { 15 | for { 16 | data.Range(func(k, v interface{}) bool { 17 | if v.(user).ts+rate <= time.Now().Unix() { 18 | data.Delete(k) 19 | } 20 | return true 21 | }) 22 | time.Sleep(time.Minute) 23 | } 24 | } 25 | 26 | type inMemoryStoreType struct { 27 | rate int64 28 | limit uint 29 | data *sync.Map 30 | skip func(ctx *gin.Context) bool 31 | } 32 | 33 | func (s *inMemoryStoreType) Limit(key string, c *gin.Context) Info { 34 | var u user 35 | m, ok := s.data.Load(key) 36 | if !ok { 37 | u = user{time.Now().Unix(), s.limit} 38 | } else { 39 | u = m.(user) 40 | } 41 | if u.ts+s.rate <= time.Now().Unix() { 42 | u.tokens = s.limit 43 | } 44 | if s.skip != nil && s.skip(c) { 45 | return Info{ 46 | Limit: s.limit, 47 | RateLimited: false, 48 | ResetTime: time.Now().Add(time.Duration((s.rate - (time.Now().Unix() - u.ts)) * time.Second.Nanoseconds())), 49 | RemainingHits: u.tokens, 50 | } 51 | } 52 | if u.tokens <= 0 { 53 | return Info{ 54 | Limit: s.limit, 55 | RateLimited: true, 56 | ResetTime: time.Now().Add(time.Duration((s.rate - (time.Now().Unix() - u.ts)) * time.Second.Nanoseconds())), 57 | RemainingHits: 0, 58 | } 59 | } 60 | u.tokens-- 61 | u.ts = time.Now().Unix() 62 | s.data.Store(key, u) 63 | return Info{ 64 | Limit: s.limit, 65 | RateLimited: false, 66 | ResetTime: time.Now().Add(time.Duration((s.rate - (time.Now().Unix() - u.ts)) * time.Second.Nanoseconds())), 67 | RemainingHits: u.tokens, 68 | } 69 | } 70 | 71 | type InMemoryOptions struct { 72 | // the user can make Limit amount of requests every Rate 73 | Rate time.Duration 74 | // the amount of requests that can be made every Rate 75 | Limit uint 76 | // a function that returns true if the request should not count toward the rate limit 77 | Skip func(*gin.Context) bool 78 | } 79 | 80 | func InMemoryStore(options *InMemoryOptions) Store { 81 | data := &sync.Map{} 82 | store := inMemoryStoreType{int64(options.Rate.Seconds()), options.Limit, data, options.Skip} 83 | go clearInBackground(data, store.rate) 84 | return &store 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # gin-rate-limit 6 | 7 | gin-rate-limit is a rate limiter for the gin framework. By default, it 8 | can only store rate limit info in memory and with redis. If you want to store it somewhere else you can make your own 9 | store or use third party stores. The library is relatively new so there are no third party stores yet. 10 | Contributions would be appreciated. 11 | 12 | Install 13 | 14 | ```shell 15 | go get github.com/JGLTechnologies/gin-rate-limit 16 | ``` 17 | 18 |
19 | 20 | Redis Example 21 | 22 | ```go 23 | package main 24 | 25 | import ( 26 | "github.com/JGLTechnologies/gin-rate-limit" 27 | "github.com/gin-gonic/gin" 28 | "github.com/redis/go-redis/v9" 29 | "time" 30 | ) 31 | 32 | func keyFunc(c *gin.Context) string { 33 | return c.ClientIP() 34 | } 35 | 36 | func errorHandler(c *gin.Context, info ratelimit.Info) { 37 | c.String(429, "Too many requests. Try again in "+time.Until(info.ResetTime).String()) 38 | } 39 | 40 | func main() { 41 | server := gin.Default() 42 | // This makes it so each ip can only make 5 requests per second 43 | store := ratelimit.RedisStore(&ratelimit.RedisOptions{ 44 | RedisClient: redis.NewClient(&redis.Options{ 45 | Addr: "localhost:7680", 46 | }), 47 | Rate: time.Second, 48 | Limit: 5, 49 | }) 50 | mw := ratelimit.RateLimiter(store, &ratelimit.Options{ 51 | ErrorHandler: errorHandler, 52 | KeyFunc: keyFunc, 53 | }) 54 | server.GET("/", mw, func(c *gin.Context) { 55 | c.String(200, "Hello World") 56 | }) 57 | server.Run(":8080") 58 | } 59 | ``` 60 | 61 |
62 | 63 | Basic Setup 64 | 65 | ```go 66 | package main 67 | 68 | import ( 69 | "github.com/gin-gonic/gin" 70 | "github.com/JGLTechnologies/gin-rate-limit" 71 | "time" 72 | ) 73 | 74 | func keyFunc(c *gin.Context) string { 75 | return c.ClientIP() 76 | } 77 | 78 | func errorHandler(c *gin.Context, info ratelimit.Info) { 79 | c.String(429, "Too many requests. Try again in "+time.Until(info.ResetTime).String()) 80 | } 81 | 82 | func main() { 83 | server := gin.Default() 84 | // This makes it so each ip can only make 5 requests per second 85 | store := ratelimit.InMemoryStore(&ratelimit.InMemoryOptions{ 86 | Rate: time.Second, 87 | Limit: 5, 88 | }) 89 | mw := ratelimit.RateLimiter(store, &ratelimit.Options{ 90 | ErrorHandler: errorHandler, 91 | KeyFunc: keyFunc, 92 | }) 93 | server.GET("/", mw, func(c *gin.Context) { 94 | c.String(200, "Hello World") 95 | }) 96 | server.Run(":8080") 97 | } 98 | ``` 99 | 100 |
101 | 102 | 103 | Custom Store Example 104 | 105 | ```go 106 | package main 107 | 108 | import ( 109 | "github.com/JGLTechnologies/gin-rate-limit" 110 | "github.com/gin-gonic/gin" 111 | ) 112 | 113 | type CustomStore struct { 114 | } 115 | 116 | // Your store must have a method called Limit that takes a key, *gin.Context and returns ratelimit.Info 117 | func (s *CustomStore) Limit(key string, c *gin.Context) Info { 118 | if UserWentOverLimit { 119 | return Info{ 120 | Limit: 100, 121 | RateLimited: true, 122 | ResetTime: reset, 123 | RemainingHits: 0, 124 | } 125 | } 126 | return Info{ 127 | Limit: 100, 128 | RateLimited: false, 129 | ResetTime: reset, 130 | RemainingHits: remaining, 131 | } 132 | } 133 | ``` -------------------------------------------------------------------------------- /redis.go: -------------------------------------------------------------------------------- 1 | package ratelimit 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/redis/go-redis/v9" 9 | ) 10 | 11 | type redisStoreType struct { 12 | rate int64 13 | limit uint 14 | client *redis.Client 15 | ctx context.Context 16 | panicOnErr bool 17 | skip func(c *gin.Context) bool 18 | } 19 | 20 | func (s *redisStoreType) Limit(key string, c *gin.Context) Info { 21 | p := s.client.Pipeline() 22 | cmds, _ := s.client.Pipelined(s.ctx, func(pipeliner redis.Pipeliner) error { 23 | pipeliner.Get(s.ctx, key+"ts") 24 | pipeliner.Get(s.ctx, key+"hits") 25 | return nil 26 | }) 27 | ts, err := cmds[0].(*redis.StringCmd).Int64() 28 | if err != nil { 29 | ts = time.Now().Unix() 30 | } 31 | hits, err := cmds[1].(*redis.StringCmd).Int64() 32 | if err != nil { 33 | hits = 0 34 | } 35 | if ts+s.rate <= time.Now().Unix() { 36 | hits = 0 37 | p.Set(s.ctx, key+"hits", hits, time.Duration(0)) 38 | } 39 | if s.skip != nil && s.skip(c) { 40 | return Info{ 41 | Limit: s.limit, 42 | RateLimited: false, 43 | ResetTime: time.Now().Add(time.Duration((s.rate - (time.Now().Unix() - ts)) * time.Second.Nanoseconds())), 44 | RemainingHits: s.limit - uint(hits), 45 | } 46 | } 47 | if hits >= int64(s.limit) { 48 | _, err = p.Exec(s.ctx) 49 | if err != nil { 50 | if s.panicOnErr { 51 | panic(err) 52 | } else { 53 | return Info{ 54 | Limit: s.limit, 55 | RateLimited: false, 56 | ResetTime: time.Now().Add(time.Duration((s.rate - (time.Now().Unix() - ts)) * time.Second.Nanoseconds())), 57 | RemainingHits: 0, 58 | } 59 | } 60 | } 61 | return Info{ 62 | Limit: s.limit, 63 | RateLimited: true, 64 | ResetTime: time.Now().Add(time.Duration((s.rate - (time.Now().Unix() - ts)) * time.Second.Nanoseconds())), 65 | RemainingHits: 0, 66 | } 67 | } 68 | ts = time.Now().Unix() 69 | hits++ 70 | p.Incr(s.ctx, key+"hits") 71 | p.Set(s.ctx, key+"ts", time.Now().Unix(), time.Duration(0)) 72 | p.Expire(s.ctx, key+"hits", time.Duration(int64(time.Second)*s.rate*2)) 73 | p.Expire(s.ctx, key+"ts", time.Duration(int64(time.Second)*s.rate*2)) 74 | _, err = p.Exec(s.ctx) 75 | if err != nil { 76 | if s.panicOnErr { 77 | panic(err) 78 | } else { 79 | return Info{ 80 | Limit: s.limit, 81 | RateLimited: false, 82 | ResetTime: time.Now().Add(time.Duration((s.rate - (time.Now().Unix() - ts)) * time.Second.Nanoseconds())), 83 | RemainingHits: s.limit - uint(hits), 84 | } 85 | } 86 | } 87 | return Info{ 88 | Limit: s.limit, 89 | RateLimited: false, 90 | ResetTime: time.Now().Add(time.Duration((s.rate - (time.Now().Unix() - ts)) * time.Second.Nanoseconds())), 91 | RemainingHits: s.limit - uint(hits), 92 | } 93 | } 94 | 95 | type RedisOptions struct { 96 | // the user can make Limit amount of requests every Rate 97 | Rate time.Duration 98 | // the amount of requests that can be made every Rate 99 | Limit uint 100 | RedisClient *redis.Client 101 | // should gin-rate-limit panic when there is an error with redis 102 | PanicOnErr bool 103 | // a function that returns true if the request should not count toward the rate limit 104 | Skip func(*gin.Context) bool 105 | } 106 | 107 | func RedisStore(options *RedisOptions) Store { 108 | return &redisStoreType{ 109 | client: options.RedisClient, 110 | rate: int64(options.Rate.Seconds()), 111 | limit: options.Limit, 112 | ctx: context.TODO(), 113 | panicOnErr: options.PanicOnErr, 114 | skip: options.Skip, 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 3 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 4 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 5 | github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= 6 | github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= 7 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 8 | github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= 9 | github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 10 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 11 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 12 | github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= 13 | github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 14 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 15 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 16 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 18 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 20 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 21 | github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= 22 | github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 23 | github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 24 | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 25 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 26 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 27 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 28 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 29 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 30 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 31 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 32 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 33 | github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= 34 | github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 35 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 36 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 37 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 38 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 39 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 40 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 41 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 42 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 43 | github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= 44 | github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 45 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 46 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 47 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 48 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 49 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 50 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 51 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 52 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 53 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 54 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 55 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 56 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 57 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 58 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 59 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 60 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 61 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 62 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 63 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 64 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 65 | github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= 66 | github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= 67 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 68 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 69 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 70 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 71 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 72 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 73 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 74 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 75 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 76 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 77 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 78 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 79 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 80 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 81 | golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U= 82 | golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= 83 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 84 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 85 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 86 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 87 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 88 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 89 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 90 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 91 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 92 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 93 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 94 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 95 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 96 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 97 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 98 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 99 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 100 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 101 | --------------------------------------------------------------------------------