├── .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 |
--------------------------------------------------------------------------------