pax_global_header00006660000000000000000000000064135667323220014523gustar00rootroot0000000000000052 comment=6911899e37a5788df86f770b3f85c1c3eb0313d5 limiter-3.3.3/000077500000000000000000000000001356673232200131765ustar00rootroot00000000000000limiter-3.3.3/.circleci/000077500000000000000000000000001356673232200150315ustar00rootroot00000000000000limiter-3.3.3/.circleci/config.yml000066400000000000000000000013571356673232200170270ustar00rootroot00000000000000version: 2 jobs: build: machine: image: circleci/classic:edge docker_layer_caching: true steps: - checkout - run: name: Checkout submodules command: | git submodule sync git submodule update --init - run: name: Start docker container for redis command: scripts/redis - run: name: Run tests command: scripts/go-wrapper scripts/test environment: GO111MODULE: on REDIS_DISABLE_BOOTSTRAP: false REDIS_URI: redis://localhost:26379/1 - run: name: Run linters command: scripts/go-wrapper scripts/lint environment: GO111MODULE: on limiter-3.3.3/.dockerignore000066400000000000000000000000761356673232200156550ustar00rootroot00000000000000# Circle CI directory .circleci # Example directory examples limiter-3.3.3/.editorconfig000066400000000000000000000005311356673232200156520ustar00rootroot00000000000000root = true [*] end_of_line = lf indent_size = 4 indent_style = space insert_final_newline = true trim_trailing_whitespace = true insert_final_newline = true charset = utf-8 [*.{yml,yaml}] indent_size = 2 [*.go] indent_size = 8 indent_style = tab [*.json] indent_size = 4 indent_style = space [Makefile] indent_style = tab indent_size = 4 limiter-3.3.3/.gitignore000066400000000000000000000000101356673232200151550ustar00rootroot00000000000000/vendor limiter-3.3.3/.golangci.yml000066400000000000000000000022721356673232200155650ustar00rootroot00000000000000run: concurrency: 4 deadline: 1m issues-exit-code: 1 tests: true output: format: colored-line-number print-issued-lines: true print-linter-name: true linters-settings: errcheck: check-type-assertions: false check-blank: false govet: check-shadowing: false use-installed-packages: false golint: min-confidence: 0.8 gofmt: simplify: true gocyclo: min-complexity: 10 maligned: suggest-new: true dupl: threshold: 80 goconst: min-len: 3 min-occurrences: 3 misspell: locale: US lll: line-length: 120 unused: check-exported: false unparam: algo: cha check-exported: false nakedret: max-func-lines: 30 linters: enable: - megacheck - govet - errcheck - gas - structcheck - varcheck - ineffassign - deadcode - typecheck - golint - interfacer - unconvert - gocyclo - gofmt - misspell - lll - nakedret enable-all: false disable: - depguard - prealloc - dupl - maligned disable-all: false issues: exclude-use-default: false max-per-linter: 1024 max-same: 1024 exclude: - "G304" - "G101" - "G104" limiter-3.3.3/AUTHORS000066400000000000000000000002071356673232200142450ustar00rootroot00000000000000Primary contributors: Gilles FABIO Florent MESSA Thomas LE ROUX limiter-3.3.3/LICENSE000066400000000000000000000020651356673232200142060ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015-2018 Ulule Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. limiter-3.3.3/Makefile000066400000000000000000000001021356673232200146270ustar00rootroot00000000000000.PHONY: test lint test: @(scripts/test) lint: @(scripts/lint) limiter-3.3.3/README.md000066400000000000000000000145721356673232200144660ustar00rootroot00000000000000# Limiter [![Documentation][godoc-img]][godoc-url] ![License][license-img] [![Build Status][circle-img]][circle-url] [![Go Report Card][goreport-img]][goreport-url] *Dead simple rate limit middleware for Go.* * Simple API * "Store" approach for backend * Redis support (but not tied too) * Middlewares: HTTP and [Gin][4] ## Installation Using [Go Modules](https://github.com/golang/go/wiki/Modules) ```bash $ go get github.com/ulule/limiter/v3@v3.3.3 ``` **Dep backport:** Please use [v3-dep](https://github.com/ulule/limiter/tree/v3-dep) branch. ## Usage In five steps: * Create a `limiter.Rate` instance _(the number of requests per period)_ * Create a `limiter.Store` instance _(see [Redis](https://github.com/ulule/limiter/blob/master/drivers/store/redis/store.go) or [In-Memory](https://github.com/ulule/limiter/blob/master/drivers/store/memory/store.go))_ * Create a `limiter.Limiter` instance that takes store and rate instances as arguments * Create a middleware instance using the middleware of your choice * Give the limiter instance to your middleware initializer **Example:** ```go // Create a rate with the given limit (number of requests) for the given // period (a time.Duration of your choice). import "github.com/ulule/limiter/v3" rate := limiter.Rate{ Period: 1 * time.Hour, Limit: 1000, } // You can also use the simplified format "-"", with the given // periods: // // * "S": second // * "M": minute // * "H": hour // * "D": day // // Examples: // // * 5 reqs/second: "5-S" // * 10 reqs/minute: "10-M" // * 1000 reqs/hour: "1000-H" // * 2000 reqs/day: "2000-D" // rate, err := limiter.NewRateFromFormatted("1000-H") if err != nil { panic(err) } // Then, create a store. Here, we use the bundled Redis store. Any store // compliant to limiter.Store interface will do the job. The defaults are // "limiter" as Redis key prefix and a maximum of 3 retries for the key under // race condition. import "github.com/ulule/limiter/v3/drivers/store/redis" store, err := redis.NewStore(client) if err != nil { panic(err) } // Alternatively, you can pass options to the store with the "WithOptions" // function. For example, for Redis store: import "github.com/ulule/limiter/v3/drivers/store/redis" store, err := redis.NewStoreWithOptions(pool, limiter.StoreOptions{ Prefix: "your_own_prefix", MaxRetry: 4, }) if err != nil { panic(err) } // Or use a in-memory store with a goroutine which clears expired keys. import "github.com/ulule/limiter/v3/drivers/store/memory" store := memory.NewStore() // Then, create the limiter instance which takes the store and the rate as arguments. // Now, you can give this instance to any supported middleware. instance := limiter.New(store, rate) ``` See middleware examples: * [HTTP](https://github.com/ulule/limiter-examples/tree/master//http/main.go) * [Gin](https://github.com/ulule/limiter-examples/tree/master//gin/main.go) * [Beego](https://github.com/ulule/limiter-examples/blob/master//beego/main.go) * [Chi](https://github.com/ulule/limiter-examples/tree/master//chi/main.go) * [Echo](https://github.com/ulule/limiter-examples/tree/master//echo/main.go) ## How it works The ip address of the request is used as a key in the store. If the key does not exist in the store we set a default value with an expiration period. You will find two stores: * Redis: rely on [TTL](http://redis.io/commands/ttl) and incrementing the rate limit on each request. * In-Memory: rely on a fork of [go-cache](https://github.com/patrickmn/go-cache) with a goroutine to clear expired keys using a default interval. When the limit is reached, a `429` HTTP status code is sent. ## Why Yet Another Package You could ask us: why yet another rate limit package? Because existing packages did not suit our needs. We tried a lot of alternatives: 1. [Throttled][1]. This package uses the generic cell-rate algorithm. To cite the documentation: *"The algorithm has been slightly modified from its usual form to support limiting with an additional quantity parameter, such as for limiting the number of bytes uploaded"*. It is brillant in term of algorithm but documentation is quite unclear at the moment, we don't need *burst* feature for now, impossible to get a correct `After-Retry` (when limit exceeds, we can still make a few requests, because of the max burst) and it only supports ``http.Handler`` middleware (we use [Gin][4]). Currently, we only need to return `429` and `X-Ratelimit-*` headers for `n reqs/duration`. 2. [Speedbump][3]. Good package but maybe too lightweight. No `Reset` support, only one middleware for [Gin][4] framework and too Redis-coupled. We rather prefer to use a "store" approach. 3. [Tollbooth][5]. Good one too but does both too much and too little. It limits by remote IP, path, methods, custom headers and basic auth usernames... but does not provide any Redis support (only *in-memory*) and a ready-to-go middleware that sets `X-Ratelimit-*` headers. `tollbooth.LimitByRequest(limiter, r)` only returns an HTTP code. 4. [ratelimit][2]. Probably the closer to our needs but, once again, too lightweight, no middleware available and not active (last commit was in August 2014). Some parts of code (Redis) comes from this project. It should deserve much more love. There are other many packages on GitHub but most are either too lightweight, too old (only support old Go versions) or unmaintained. So that's why we decided to create yet another one. ## Contributing * Ping us on twitter: * [@oibafsellig](https://twitter.com/oibafsellig) * [@thoas](https://twitter.com/thoas) * [@novln_](https://twitter.com/novln_) * Fork the [project](https://github.com/ulule/limiter) * Fix [bugs](https://github.com/ulule/limiter/issues) Don't hesitate ;) [1]: https://github.com/throttled/throttled [2]: https://github.com/r8k/ratelimit [3]: https://github.com/etcinit/speedbump [4]: https://github.com/gin-gonic/gin [5]: https://github.com/didip/tollbooth [godoc-url]: https://godoc.org/github.com/ulule/limiter [godoc-img]: https://godoc.org/github.com/ulule/limiter?status.svg [license-img]: https://img.shields.io/badge/license-MIT-blue.svg [goreport-url]: https://goreportcard.com/report/github.com/ulule/limiter [goreport-img]: https://goreportcard.com/badge/github.com/ulule/limiter [circle-url]: https://circleci.com/gh/ulule/limiter/tree/master [circle-img]: https://circleci.com/gh/ulule/limiter.svg?style=shield&circle-token=baf62ec320dd871b3a4a7e67fa99530fbc877c99 limiter-3.3.3/defaults.go000066400000000000000000000006311356673232200153340ustar00rootroot00000000000000package limiter import "time" const ( // DefaultPrefix is the default prefix to use for the key in the store. DefaultPrefix = "limiter" // DefaultMaxRetry is the default maximum number of key retries under // race condition (mainly used with database-based stores). DefaultMaxRetry = 3 // DefaultCleanUpInterval is the default time duration for cleanup. DefaultCleanUpInterval = 30 * time.Second ) limiter-3.3.3/drivers/000077500000000000000000000000001356673232200146545ustar00rootroot00000000000000limiter-3.3.3/drivers/middleware/000077500000000000000000000000001356673232200167715ustar00rootroot00000000000000limiter-3.3.3/drivers/middleware/gin/000077500000000000000000000000001356673232200175465ustar00rootroot00000000000000limiter-3.3.3/drivers/middleware/gin/middleware.go000066400000000000000000000024221356673232200222120ustar00rootroot00000000000000package gin import ( "strconv" "github.com/gin-gonic/gin" "github.com/ulule/limiter/v3" ) // Middleware is the middleware for basic http.Handler. type Middleware struct { Limiter *limiter.Limiter OnError ErrorHandler OnLimitReached LimitReachedHandler KeyGetter KeyGetter } // NewMiddleware return a new instance of a basic HTTP middleware. func NewMiddleware(limiter *limiter.Limiter, options ...Option) gin.HandlerFunc { middleware := &Middleware{ Limiter: limiter, OnError: DefaultErrorHandler, OnLimitReached: DefaultLimitReachedHandler, KeyGetter: DefaultKeyGetter, } for _, option := range options { option.apply(middleware) } return func(ctx *gin.Context) { middleware.Handle(ctx) } } // Handle gin request. func (middleware *Middleware) Handle(c *gin.Context) { key := middleware.KeyGetter(c) context, err := middleware.Limiter.Get(c, key) if err != nil { middleware.OnError(c, err) c.Abort() return } c.Header("X-RateLimit-Limit", strconv.FormatInt(context.Limit, 10)) c.Header("X-RateLimit-Remaining", strconv.FormatInt(context.Remaining, 10)) c.Header("X-RateLimit-Reset", strconv.FormatInt(context.Reset, 10)) if context.Reached { middleware.OnLimitReached(c) c.Abort() return } c.Next() } limiter-3.3.3/drivers/middleware/gin/middleware_test.go000066400000000000000000000046001356673232200232510ustar00rootroot00000000000000package gin_test import ( "net/http" "net/http/httptest" "strconv" "sync" "sync/atomic" "testing" libgin "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" "github.com/ulule/limiter/v3" "github.com/ulule/limiter/v3/drivers/middleware/gin" "github.com/ulule/limiter/v3/drivers/store/memory" ) func TestHTTPMiddleware(t *testing.T) { is := require.New(t) libgin.SetMode(libgin.TestMode) request, err := http.NewRequest("GET", "/", nil) is.NoError(err) is.NotNil(request) store := memory.NewStore() is.NotZero(store) rate, err := limiter.NewRateFromFormatted("10-M") is.NoError(err) is.NotZero(rate) middleware := gin.NewMiddleware(limiter.New(store, rate)) is.NotZero(middleware) router := libgin.New() router.Use(middleware) router.GET("/", func(c *libgin.Context) { c.String(http.StatusOK, "hello") }) success := int64(10) clients := int64(100) // // Sequential // for i := int64(1); i <= clients; i++ { resp := httptest.NewRecorder() router.ServeHTTP(resp, request) if i <= success { is.Equal(resp.Code, http.StatusOK) } else { is.Equal(resp.Code, http.StatusTooManyRequests) } } // // Concurrent // store = memory.NewStore() is.NotZero(store) middleware = gin.NewMiddleware(limiter.New(store, rate)) is.NotZero(middleware) router = libgin.New() router.Use(middleware) router.GET("/", func(c *libgin.Context) { c.String(http.StatusOK, "hello") }) wg := &sync.WaitGroup{} counter := int64(0) for i := int64(1); i <= clients; i++ { wg.Add(1) go func() { resp := httptest.NewRecorder() router.ServeHTTP(resp, request) if resp.Code == http.StatusOK { atomic.AddInt64(&counter, 1) } wg.Done() }() } wg.Wait() is.Equal(success, atomic.LoadInt64(&counter)) // // Custom KeyGetter // store = memory.NewStore() is.NotZero(store) j := 0 KeyGetter := func(c *libgin.Context) string { j++ return strconv.Itoa(j) } middleware = gin.NewMiddleware(limiter.New(store, rate), gin.WithKeyGetter(KeyGetter)) is.NotZero(middleware) router = libgin.New() router.Use(middleware) router.GET("/", func(c *libgin.Context) { c.String(http.StatusOK, "hello") }) for i := int64(1); i <= clients; i++ { resp := httptest.NewRecorder() router.ServeHTTP(resp, request) // We should always be ok as the key changes for each request is.Equal(http.StatusOK, resp.Code, strconv.Itoa(int(i))) } } limiter-3.3.3/drivers/middleware/gin/options.go000066400000000000000000000034501356673232200215720ustar00rootroot00000000000000package gin import ( "net/http" "github.com/gin-gonic/gin" ) // Option is used to define Middleware configuration. type Option interface { apply(*Middleware) } type option func(*Middleware) func (o option) apply(middleware *Middleware) { o(middleware) } // ErrorHandler is an handler used to inform when an error has occurred. type ErrorHandler func(c *gin.Context, err error) // WithErrorHandler will configure the Middleware to use the given ErrorHandler. func WithErrorHandler(handler ErrorHandler) Option { return option(func(middleware *Middleware) { middleware.OnError = handler }) } // DefaultErrorHandler is the default ErrorHandler used by a new Middleware. func DefaultErrorHandler(c *gin.Context, err error) { panic(err) } // LimitReachedHandler is an handler used to inform when the limit has exceeded. type LimitReachedHandler func(c *gin.Context) // WithLimitReachedHandler will configure the Middleware to use the given LimitReachedHandler. func WithLimitReachedHandler(handler LimitReachedHandler) Option { return option(func(middleware *Middleware) { middleware.OnLimitReached = handler }) } // DefaultLimitReachedHandler is the default LimitReachedHandler used by a new Middleware. func DefaultLimitReachedHandler(c *gin.Context) { c.String(http.StatusTooManyRequests, "Limit exceeded") } // KeyGetter will define the rate limiter key given the gin Context type KeyGetter func(c *gin.Context) string // WithKeyGetter will configure the Middleware to use the given KeyGetter func WithKeyGetter(KeyGetter KeyGetter) Option { return option(func(middleware *Middleware) { middleware.KeyGetter = KeyGetter }) } // DefaultKeyGetter is the default KeyGetter used by a new Middleware // It returns the Client IP address func DefaultKeyGetter(c *gin.Context) string { return c.ClientIP() } limiter-3.3.3/drivers/middleware/stdlib/000077500000000000000000000000001356673232200202525ustar00rootroot00000000000000limiter-3.3.3/drivers/middleware/stdlib/middleware.go000066400000000000000000000024761356673232200227270ustar00rootroot00000000000000package stdlib import ( "net/http" "strconv" "github.com/ulule/limiter/v3" ) // Middleware is the middleware for basic http.Handler. type Middleware struct { Limiter *limiter.Limiter OnError ErrorHandler OnLimitReached LimitReachedHandler TrustForwardHeader bool } // NewMiddleware return a new instance of a basic HTTP middleware. func NewMiddleware(limiter *limiter.Limiter, options ...Option) *Middleware { middleware := &Middleware{ Limiter: limiter, OnError: DefaultErrorHandler, OnLimitReached: DefaultLimitReachedHandler, } for _, option := range options { option.apply(middleware) } return middleware } // Handler the middleware handler. func (middleware *Middleware) Handler(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { context, err := middleware.Limiter.Get(r.Context(), middleware.Limiter.GetIPKey(r)) if err != nil { middleware.OnError(w, r, err) return } w.Header().Add("X-RateLimit-Limit", strconv.FormatInt(context.Limit, 10)) w.Header().Add("X-RateLimit-Remaining", strconv.FormatInt(context.Remaining, 10)) w.Header().Add("X-RateLimit-Reset", strconv.FormatInt(context.Reset, 10)) if context.Reached { middleware.OnLimitReached(w, r) return } h.ServeHTTP(w, r) }) } limiter-3.3.3/drivers/middleware/stdlib/middleware_test.go000066400000000000000000000031701356673232200237560ustar00rootroot00000000000000package stdlib_test import ( "net/http" "net/http/httptest" "sync" "sync/atomic" "testing" "github.com/stretchr/testify/require" "github.com/ulule/limiter/v3" "github.com/ulule/limiter/v3/drivers/middleware/stdlib" "github.com/ulule/limiter/v3/drivers/store/memory" ) func TestHTTPMiddleware(t *testing.T) { is := require.New(t) request, err := http.NewRequest("GET", "/", nil) is.NoError(err) is.NotNil(request) handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, thr := w.Write([]byte("hello")) if thr != nil { panic(thr) } }) store := memory.NewStore() is.NotZero(store) rate, err := limiter.NewRateFromFormatted("10-M") is.NoError(err) is.NotZero(rate) middleware := stdlib.NewMiddleware(limiter.New(store, rate)).Handler(handler) is.NotZero(middleware) success := int64(10) clients := int64(100) // // Sequential // for i := int64(1); i <= clients; i++ { resp := httptest.NewRecorder() middleware.ServeHTTP(resp, request) if i <= success { is.Equal(resp.Code, http.StatusOK) } else { is.Equal(resp.Code, http.StatusTooManyRequests) } } // // Concurrent // store = memory.NewStore() is.NotZero(store) middleware = stdlib.NewMiddleware(limiter.New(store, rate)).Handler(handler) is.NotZero(middleware) wg := &sync.WaitGroup{} counter := int64(0) for i := int64(1); i <= clients; i++ { wg.Add(1) go func() { resp := httptest.NewRecorder() middleware.ServeHTTP(resp, request) if resp.Code == http.StatusOK { atomic.AddInt64(&counter, 1) } wg.Done() }() } wg.Wait() is.Equal(success, atomic.LoadInt64(&counter)) } limiter-3.3.3/drivers/middleware/stdlib/options.go000066400000000000000000000025771356673232200223070ustar00rootroot00000000000000package stdlib import ( "net/http" ) // Option is used to define Middleware configuration. type Option interface { apply(*Middleware) } type option func(*Middleware) func (o option) apply(middleware *Middleware) { o(middleware) } // ErrorHandler is an handler used to inform when an error has occurred. type ErrorHandler func(w http.ResponseWriter, r *http.Request, err error) // WithErrorHandler will configure the Middleware to use the given ErrorHandler. func WithErrorHandler(handler ErrorHandler) Option { return option(func(middleware *Middleware) { middleware.OnError = handler }) } // DefaultErrorHandler is the default ErrorHandler used by a new Middleware. func DefaultErrorHandler(w http.ResponseWriter, r *http.Request, err error) { panic(err) } // LimitReachedHandler is an handler used to inform when the limit has exceeded. type LimitReachedHandler func(w http.ResponseWriter, r *http.Request) // WithLimitReachedHandler will configure the Middleware to use the given LimitReachedHandler. func WithLimitReachedHandler(handler LimitReachedHandler) Option { return option(func(middleware *Middleware) { middleware.OnLimitReached = handler }) } // DefaultLimitReachedHandler is the default LimitReachedHandler used by a new Middleware. func DefaultLimitReachedHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, "Limit exceeded", http.StatusTooManyRequests) } limiter-3.3.3/drivers/store/000077500000000000000000000000001356673232200160105ustar00rootroot00000000000000limiter-3.3.3/drivers/store/common/000077500000000000000000000000001356673232200173005ustar00rootroot00000000000000limiter-3.3.3/drivers/store/common/context.go000066400000000000000000000010221356673232200213060ustar00rootroot00000000000000package common import ( "time" "github.com/ulule/limiter/v3" ) // GetContextFromState generate a new limiter.Context from given state. func GetContextFromState(now time.Time, rate limiter.Rate, expiration time.Time, count int64) limiter.Context { limit := rate.Limit remaining := int64(0) reached := true if count <= limit { remaining = limit - count reached = false } reset := expiration.Unix() return limiter.Context{ Limit: limit, Remaining: remaining, Reset: reset, Reached: reached, } } limiter-3.3.3/drivers/store/memory/000077500000000000000000000000001356673232200173205ustar00rootroot00000000000000limiter-3.3.3/drivers/store/memory/cache.go000066400000000000000000000070441356673232200207170ustar00rootroot00000000000000package memory import ( "runtime" "sync" "time" ) // Forked from https://github.com/patrickmn/go-cache // CacheWrapper is used to ensure that the underlying cleaner goroutine used to clean expired keys will not prevent // Cache from being garbage collected. type CacheWrapper struct { *Cache } // A cleaner will periodically delete expired keys from cache. type cleaner struct { interval time.Duration stop chan bool } // Run will periodically delete expired keys from given cache until GC notify that it should stop. func (cleaner *cleaner) Run(cache *Cache) { ticker := time.NewTicker(cleaner.interval) for { select { case <-ticker.C: cache.Clean() case <-cleaner.stop: ticker.Stop() return } } } // stopCleaner is a callback from GC used to stop cleaner goroutine. func stopCleaner(wrapper *CacheWrapper) { wrapper.cleaner.stop <- true } // startCleaner will start a cleaner goroutine for given cache. func startCleaner(cache *Cache, interval time.Duration) { cleaner := &cleaner{ interval: interval, stop: make(chan bool), } cache.cleaner = cleaner go cleaner.Run(cache) } // Counter is a simple counter with an optional expiration. type Counter struct { Value int64 Expiration int64 } // Expired returns true if the counter has expired. func (counter Counter) Expired() bool { if counter.Expiration == 0 { return false } return time.Now().UnixNano() > counter.Expiration } // Cache contains a collection of counters. type Cache struct { mutex sync.RWMutex counters map[string]Counter cleaner *cleaner } // NewCache returns a new cache. func NewCache(cleanInterval time.Duration) *CacheWrapper { cache := &Cache{ counters: map[string]Counter{}, } wrapper := &CacheWrapper{Cache: cache} if cleanInterval > 0 { startCleaner(cache, cleanInterval) runtime.SetFinalizer(wrapper, stopCleaner) } return wrapper } // Increment increments given value on key. // If key is undefined or expired, it will create it. func (cache *Cache) Increment(key string, value int64, duration time.Duration) (int64, time.Time) { cache.mutex.Lock() counter, ok := cache.counters[key] if !ok || counter.Expired() { expiration := time.Now().Add(duration).UnixNano() counter = Counter{ Value: value, Expiration: expiration, } cache.counters[key] = counter cache.mutex.Unlock() return value, time.Unix(0, expiration) } value = counter.Value + value counter.Value = value expiration := counter.Expiration cache.counters[key] = counter cache.mutex.Unlock() return value, time.Unix(0, expiration) } // Get returns key's value and expiration. func (cache *Cache) Get(key string, duration time.Duration) (int64, time.Time) { cache.mutex.RLock() counter, ok := cache.counters[key] if !ok || counter.Expired() { expiration := time.Now().Add(duration).UnixNano() cache.mutex.RUnlock() return 0, time.Unix(0, expiration) } value := counter.Value expiration := counter.Expiration cache.mutex.RUnlock() return value, time.Unix(0, expiration) } // Clean will deleted any expired keys. func (cache *Cache) Clean() { now := time.Now().UnixNano() cache.mutex.Lock() for key, counter := range cache.counters { if now > counter.Expiration { delete(cache.counters, key) } } cache.mutex.Unlock() } // Reset changes the key's value and resets the expiration. func (cache *Cache) Reset(key string, duration time.Duration) (int64, time.Time) { cache.mutex.Lock() delete(cache.counters, key) cache.mutex.Unlock() expiration := time.Now().Add(duration).UnixNano() return 0, time.Unix(0, expiration) } limiter-3.3.3/drivers/store/memory/cache_test.go000066400000000000000000000057561356673232200217660ustar00rootroot00000000000000package memory_test import ( "sync" "testing" "time" "github.com/stretchr/testify/require" "github.com/ulule/limiter/v3/drivers/store/memory" ) func TestCacheIncrementSequential(t *testing.T) { is := require.New(t) key := "foobar" cache := memory.NewCache(10 * time.Nanosecond) duration := 50 * time.Millisecond deleted := time.Now().Add(duration).UnixNano() epsilon := 0.001 x, expire := cache.Increment(key, 1, duration) is.Equal(int64(1), x) is.InEpsilon(deleted, expire.UnixNano(), epsilon) x, expire = cache.Increment(key, 2, duration) is.Equal(int64(3), x) is.InEpsilon(deleted, expire.UnixNano(), epsilon) time.Sleep(duration) deleted = time.Now().Add(duration).UnixNano() x, expire = cache.Increment(key, 1, duration) is.Equal(int64(1), x) is.InEpsilon(deleted, expire.UnixNano(), epsilon) } func TestCacheIncrementConcurrent(t *testing.T) { is := require.New(t) goroutines := 300 ops := 500 expected := int64(0) for i := 0; i < goroutines; i++ { if (i % 3) == 0 { for j := 0; j < ops; j++ { expected += int64(i + j) } } } key := "foobar" cache := memory.NewCache(10 * time.Nanosecond) wg := &sync.WaitGroup{} wg.Add(goroutines) for i := 0; i < goroutines; i++ { go func(i int) { if (i % 3) == 0 { time.Sleep(1 * time.Second) for j := 0; j < ops; j++ { cache.Increment(key, int64(i+j), (1 * time.Second)) } } else { time.Sleep(50 * time.Millisecond) stopAt := time.Now().Add(500 * time.Millisecond) for time.Now().Before(stopAt) { cache.Increment(key, int64(i), (75 * time.Millisecond)) } } wg.Done() }(i) } wg.Wait() value, expire := cache.Get(key, (100 * time.Millisecond)) is.Equal(expected, value) is.True(time.Now().Before(expire)) } func TestCacheGet(t *testing.T) { is := require.New(t) key := "foobar" cache := memory.NewCache(10 * time.Nanosecond) duration := 50 * time.Millisecond deleted := time.Now().Add(duration).UnixNano() epsilon := 0.001 x, expire := cache.Get(key, duration) is.Equal(int64(0), x) is.InEpsilon(deleted, expire.UnixNano(), epsilon) } func TestCacheReset(t *testing.T) { is := require.New(t) key := "foobar" cache := memory.NewCache(10 * time.Nanosecond) duration := 50 * time.Millisecond deleted := time.Now().Add(duration).UnixNano() epsilon := 0.001 x, expire := cache.Get(key, duration) is.Equal(int64(0), x) is.InEpsilon(deleted, expire.UnixNano(), epsilon) x, expire = cache.Increment(key, 1, duration) is.Equal(int64(1), x) is.InEpsilon(deleted, expire.UnixNano(), epsilon) x, expire = cache.Increment(key, 1, duration) is.Equal(int64(2), x) is.InEpsilon(deleted, expire.UnixNano(), epsilon) x, expire = cache.Reset(key, duration) is.Equal(int64(0), x) is.InEpsilon(deleted, expire.UnixNano(), epsilon) x, expire = cache.Increment(key, 1, duration) is.Equal(int64(1), x) is.InEpsilon(deleted, expire.UnixNano(), epsilon) x, expire = cache.Increment(key, 1, duration) is.Equal(int64(2), x) is.InEpsilon(deleted, expire.UnixNano(), epsilon) } limiter-3.3.3/drivers/store/memory/store.go000066400000000000000000000036101356673232200210030ustar00rootroot00000000000000package memory import ( "context" "fmt" "time" "github.com/ulule/limiter/v3" "github.com/ulule/limiter/v3/drivers/store/common" ) // Store is the in-memory store. type Store struct { // Prefix used for the key. Prefix string // cache used to store values in-memory. cache *CacheWrapper } // NewStore creates a new instance of memory store with defaults. func NewStore() limiter.Store { return NewStoreWithOptions(limiter.StoreOptions{ Prefix: limiter.DefaultPrefix, CleanUpInterval: limiter.DefaultCleanUpInterval, }) } // NewStoreWithOptions creates a new instance of memory store with options. func NewStoreWithOptions(options limiter.StoreOptions) limiter.Store { return &Store{ Prefix: options.Prefix, cache: NewCache(options.CleanUpInterval), } } // Get returns the limit for given identifier. func (store *Store) Get(ctx context.Context, key string, rate limiter.Rate) (limiter.Context, error) { key = fmt.Sprintf("%s:%s", store.Prefix, key) now := time.Now() count, expiration := store.cache.Increment(key, 1, rate.Period) lctx := common.GetContextFromState(now, rate, expiration, count) return lctx, nil } // Peek returns the limit for given identifier, without modification on current values. func (store *Store) Peek(ctx context.Context, key string, rate limiter.Rate) (limiter.Context, error) { key = fmt.Sprintf("%s:%s", store.Prefix, key) now := time.Now() count, expiration := store.cache.Get(key, rate.Period) lctx := common.GetContextFromState(now, rate, expiration, count) return lctx, nil } // Reset returns the limit for given identifier. func (store *Store) Reset(ctx context.Context, key string, rate limiter.Rate) (limiter.Context, error) { key = fmt.Sprintf("%s:%s", store.Prefix, key) now := time.Now() count, expiration := store.cache.Reset(key, rate.Period) lctx := common.GetContextFromState(now, rate, expiration, count) return lctx, nil } limiter-3.3.3/drivers/store/memory/store_test.go000066400000000000000000000012131356673232200220370ustar00rootroot00000000000000package memory_test import ( "testing" "time" "github.com/ulule/limiter/v3" "github.com/ulule/limiter/v3/drivers/store/memory" "github.com/ulule/limiter/v3/drivers/store/tests" ) func TestMemoryStoreSequentialAccess(t *testing.T) { tests.TestStoreSequentialAccess(t, memory.NewStoreWithOptions(limiter.StoreOptions{ Prefix: "limiter:memory:sequential", CleanUpInterval: 30 * time.Second, })) } func TestMemoryStoreConcurrentAccess(t *testing.T) { tests.TestStoreConcurrentAccess(t, memory.NewStoreWithOptions(limiter.StoreOptions{ Prefix: "limiter:memory:concurrent", CleanUpInterval: 1 * time.Nanosecond, })) } limiter-3.3.3/drivers/store/redis/000077500000000000000000000000001356673232200171165ustar00rootroot00000000000000limiter-3.3.3/drivers/store/redis/store.go000066400000000000000000000176651356673232200206200ustar00rootroot00000000000000package redis import ( "context" "fmt" "time" libredis "github.com/go-redis/redis" "github.com/pkg/errors" "github.com/ulule/limiter/v3" "github.com/ulule/limiter/v3/drivers/store/common" ) // Client is an interface thats allows to use a redis cluster or a redis single client seamlessly. type Client interface { Ping() *libredis.StatusCmd Get(key string) *libredis.StringCmd Set(key string, value interface{}, expiration time.Duration) *libredis.StatusCmd Watch(handler func(*libredis.Tx) error, keys ...string) error Del(keys ...string) *libredis.IntCmd SetNX(key string, value interface{}, expiration time.Duration) *libredis.BoolCmd Eval(script string, keys []string, args ...interface{}) *libredis.Cmd } // Store is the redis store. type Store struct { // Prefix used for the key. Prefix string // MaxRetry is the maximum number of retry under race conditions. MaxRetry int // client used to communicate with redis server. client Client } // NewStore returns an instance of redis store with defaults. func NewStore(client Client) (limiter.Store, error) { return NewStoreWithOptions(client, limiter.StoreOptions{ Prefix: limiter.DefaultPrefix, CleanUpInterval: limiter.DefaultCleanUpInterval, MaxRetry: limiter.DefaultMaxRetry, }) } // NewStoreWithOptions returns an instance of redis store with options. func NewStoreWithOptions(client Client, options limiter.StoreOptions) (limiter.Store, error) { store := &Store{ client: client, Prefix: options.Prefix, MaxRetry: options.MaxRetry, } if store.MaxRetry <= 0 { store.MaxRetry = 1 } _, err := store.ping() if err != nil { return nil, err } return store, nil } // Get returns the limit for given identifier. func (store *Store) Get(ctx context.Context, key string, rate limiter.Rate) (limiter.Context, error) { key = fmt.Sprintf("%s:%s", store.Prefix, key) now := time.Now() lctx := limiter.Context{} onWatch := func(rtx *libredis.Tx) error { created, err := store.doSetValue(rtx, key, rate.Period) if err != nil { return err } if created { expiration := now.Add(rate.Period) lctx = common.GetContextFromState(now, rate, expiration, 1) return nil } count, ttl, err := store.doUpdateValue(rtx, key, rate.Period) if err != nil { return err } expiration := now.Add(rate.Period) if ttl > 0 { expiration = now.Add(ttl) } lctx = common.GetContextFromState(now, rate, expiration, count) return nil } err := store.client.Watch(onWatch, key) if err != nil { err = errors.Wrapf(err, "limiter: cannot get value for %s", key) return limiter.Context{}, err } return lctx, nil } // Peek returns the limit for given identifier, without modification on current values. func (store *Store) Peek(ctx context.Context, key string, rate limiter.Rate) (limiter.Context, error) { key = fmt.Sprintf("%s:%s", store.Prefix, key) now := time.Now() lctx := limiter.Context{} onWatch := func(rtx *libredis.Tx) error { count, ttl, err := store.doPeekValue(rtx, key) if err != nil { return err } expiration := now.Add(rate.Period) if ttl > 0 { expiration = now.Add(ttl) } lctx = common.GetContextFromState(now, rate, expiration, count) return nil } err := store.client.Watch(onWatch, key) if err != nil { err = errors.Wrapf(err, "limiter: cannot peek value for %s", key) return limiter.Context{}, err } return lctx, nil } // Reset returns the limit for given identifier which is set to zero. func (store *Store) Reset(ctx context.Context, key string, rate limiter.Rate) (limiter.Context, error) { key = fmt.Sprintf("%s:%s", store.Prefix, key) now := time.Now() lctx := limiter.Context{} onWatch := func(rtx *libredis.Tx) error { err := store.doResetValue(rtx, key) if err != nil { return err } count := int64(0) expiration := now.Add(rate.Period) lctx = common.GetContextFromState(now, rate, expiration, count) return nil } err := store.client.Watch(onWatch, key) if err != nil { err = errors.Wrapf(err, "limiter: cannot reset value for %s", key) return limiter.Context{}, err } return lctx, nil } // doPeekValue will execute peekValue with a retry mecanism (optimistic locking) until store.MaxRetry is reached. func (store *Store) doPeekValue(rtx *libredis.Tx, key string) (int64, time.Duration, error) { for i := 0; i < store.MaxRetry; i++ { count, ttl, err := peekValue(rtx, key) if err == nil { return count, ttl, nil } } return 0, 0, errors.New("retry limit exceeded") } // peekValue will retrieve the counter and its expiration for given key. func peekValue(rtx *libredis.Tx, key string) (int64, time.Duration, error) { pipe := rtx.Pipeline() value := pipe.Get(key) expire := pipe.PTTL(key) _, err := pipe.Exec() if err != nil && err != libredis.Nil { return 0, 0, err } count, err := value.Int64() if err != nil && err != libredis.Nil { return 0, 0, err } ttl, err := expire.Result() if err != nil { return 0, 0, err } return count, ttl, nil } // doSetValue will execute setValue with a retry mecanism (optimistic locking) until store.MaxRetry is reached. func (store *Store) doSetValue(rtx *libredis.Tx, key string, expiration time.Duration) (bool, error) { for i := 0; i < store.MaxRetry; i++ { created, err := setValue(rtx, key, expiration) if err == nil { return created, nil } } return false, errors.New("retry limit exceeded") } // setValue will try to initialize a new counter if given key doesn't exists. func setValue(rtx *libredis.Tx, key string, expiration time.Duration) (bool, error) { value := rtx.SetNX(key, 1, expiration) created, err := value.Result() if err != nil { return false, err } return created, nil } // doUpdateValue will execute setValue with a retry mecanism (optimistic locking) until store.MaxRetry is reached. func (store *Store) doUpdateValue(rtx *libredis.Tx, key string, expiration time.Duration) (int64, time.Duration, error) { for i := 0; i < store.MaxRetry; i++ { count, ttl, err := updateValue(rtx, key, expiration) if err == nil { return count, ttl, nil } // If ttl is negative and there is an error, do not retry an update. if ttl < 0 { return 0, 0, err } } return 0, 0, errors.New("retry limit exceeded") } // updateValue will try to increment the counter identified by given key. func updateValue(rtx *libredis.Tx, key string, expiration time.Duration) (int64, time.Duration, error) { pipe := rtx.Pipeline() value := pipe.Incr(key) expire := pipe.PTTL(key) _, err := pipe.Exec() if err != nil { return 0, 0, err } count, err := value.Result() if err != nil { return 0, 0, err } ttl, err := expire.Result() if err != nil { return 0, 0, err } // If ttl is -1ms, we have to define key expiration. // PTTL return values changed as of Redis 2.8 // Now the command returns -2ms if the key does not exist, and -1ms if the key exists, but there is no expiry set // We shouldn't try to set an expiry on a key that doesn't exist if ttl == (-1 * time.Millisecond) { expire := rtx.Expire(key, expiration) ok, err := expire.Result() if err != nil { return count, ttl, err } if !ok { return count, ttl, errors.New("cannot configure timeout on key") } } return count, ttl, nil } // doResetValue will execute resetValue with a retry mecanism (optimistic locking) until store.MaxRetry is reached. func (store *Store) doResetValue(rtx *libredis.Tx, key string) error { for i := 0; i < store.MaxRetry; i++ { err := resetValue(rtx, key) if err == nil { return nil } } return errors.New("retry limit exceeded") } // resetValue will try to reset the counter identified by given key. func resetValue(rtx *libredis.Tx, key string) error { deletion := rtx.Del(key) _, err := deletion.Result() if err != nil { return err } return nil } // ping checks if redis is alive. func (store *Store) ping() (bool, error) { cmd := store.client.Ping() pong, err := cmd.Result() if err != nil { return false, errors.Wrap(err, "limiter: cannot ping redis server") } return (pong == "PONG"), nil } limiter-3.3.3/drivers/store/redis/store_test.go000066400000000000000000000023561356673232200216460ustar00rootroot00000000000000package redis_test import ( "os" "testing" libredis "github.com/go-redis/redis" "github.com/stretchr/testify/require" "github.com/ulule/limiter/v3" "github.com/ulule/limiter/v3/drivers/store/redis" "github.com/ulule/limiter/v3/drivers/store/tests" ) func TestRedisStoreSequentialAccess(t *testing.T) { is := require.New(t) client, err := newRedisClient() is.NoError(err) is.NotNil(client) store, err := redis.NewStoreWithOptions(client, limiter.StoreOptions{ Prefix: "limiter:redis:sequential", MaxRetry: 3, }) is.NoError(err) is.NotNil(store) tests.TestStoreSequentialAccess(t, store) } func TestRedisStoreConcurrentAccess(t *testing.T) { is := require.New(t) client, err := newRedisClient() is.NoError(err) is.NotNil(client) store, err := redis.NewStoreWithOptions(client, limiter.StoreOptions{ Prefix: "limiter:redis:concurrent", MaxRetry: 7, }) is.NoError(err) is.NotNil(store) tests.TestStoreConcurrentAccess(t, store) } func newRedisClient() (*libredis.Client, error) { uri := "redis://localhost:6379/0" if os.Getenv("REDIS_URI") != "" { uri = os.Getenv("REDIS_URI") } opt, err := libredis.ParseURL(uri) if err != nil { return nil, err } client := libredis.NewClient(opt) return client, nil } limiter-3.3.3/drivers/store/tests/000077500000000000000000000000001356673232200171525ustar00rootroot00000000000000limiter-3.3.3/drivers/store/tests/tests.go000066400000000000000000000057541356673232200206560ustar00rootroot00000000000000package tests import ( "context" "sync" "testing" "time" "github.com/stretchr/testify/require" "github.com/ulule/limiter/v3" ) // TestStoreSequentialAccess verify that store works as expected with a sequential access. func TestStoreSequentialAccess(t *testing.T, store limiter.Store) { is := require.New(t) ctx := context.Background() limiter := limiter.New(store, limiter.Rate{ Limit: 3, Period: time.Minute, }) // Check counter increment. { for i := 1; i <= 6; i++ { if i <= 3 { lctx, err := limiter.Peek(ctx, "foo") is.NoError(err) is.NotZero(lctx) is.Equal(int64(3-(i-1)), lctx.Remaining) is.False(lctx.Reached) } lctx, err := limiter.Get(ctx, "foo") is.NoError(err) is.NotZero(lctx) if i <= 3 { is.Equal(int64(3), lctx.Limit) is.Equal(int64(3-i), lctx.Remaining) is.True((lctx.Reset - time.Now().Unix()) <= 60) is.False(lctx.Reached) lctx, err = limiter.Peek(ctx, "foo") is.NoError(err) is.Equal(int64(3-i), lctx.Remaining) is.False(lctx.Reached) } else { is.Equal(int64(3), lctx.Limit) is.Equal(int64(0), lctx.Remaining) is.True((lctx.Reset - time.Now().Unix()) <= 60) is.True(lctx.Reached) } } } // Check counter reset. { lctx, err := limiter.Peek(ctx, "foo") is.NoError(err) is.NotZero(lctx) is.Equal(int64(3), lctx.Limit) is.Equal(int64(0), lctx.Remaining) is.True((lctx.Reset - time.Now().Unix()) <= 60) is.True(lctx.Reached) lctx, err = limiter.Reset(ctx, "foo") is.NoError(err) is.NotZero(lctx) is.Equal(int64(3), lctx.Limit) is.Equal(int64(3), lctx.Remaining) is.True((lctx.Reset - time.Now().Unix()) <= 60) is.False(lctx.Reached) lctx, err = limiter.Peek(ctx, "foo") is.NoError(err) is.NotZero(lctx) is.Equal(int64(3), lctx.Limit) is.Equal(int64(3), lctx.Remaining) is.True((lctx.Reset - time.Now().Unix()) <= 60) is.False(lctx.Reached) lctx, err = limiter.Get(ctx, "foo") is.NoError(err) is.NotZero(lctx) lctx, err = limiter.Reset(ctx, "foo") is.NoError(err) is.NotZero(lctx) is.Equal(int64(3), lctx.Limit) is.Equal(int64(3), lctx.Remaining) is.True((lctx.Reset - time.Now().Unix()) <= 60) is.False(lctx.Reached) lctx, err = limiter.Reset(ctx, "foo") is.NoError(err) is.NotZero(lctx) is.Equal(int64(3), lctx.Limit) is.Equal(int64(3), lctx.Remaining) is.True((lctx.Reset - time.Now().Unix()) <= 60) is.False(lctx.Reached) } } // TestStoreConcurrentAccess verify that store works as expected with a concurrent access. func TestStoreConcurrentAccess(t *testing.T, store limiter.Store) { is := require.New(t) ctx := context.Background() limiter := limiter.New(store, limiter.Rate{ Limit: 100000, Period: 10 * time.Second, }) goroutines := 500 ops := 500 wg := &sync.WaitGroup{} wg.Add(goroutines) for i := 0; i < goroutines; i++ { go func(i int) { for j := 0; j < ops; j++ { lctx, err := limiter.Get(ctx, "foo") is.NoError(err) is.NotZero(lctx) } wg.Done() }(i) } wg.Wait() } limiter-3.3.3/examples/000077500000000000000000000000001356673232200150145ustar00rootroot00000000000000limiter-3.3.3/examples/README.md000066400000000000000000000010511356673232200162700ustar00rootroot00000000000000# Limiter examples The examples has been moved here: https://github.com/ulule/limiter-examples Nonetheless, this is list of middleware examples with the new location: * [HTTP](https://github.com/ulule/limiter-examples/tree/master/http/main.go) * [Gin](https://github.com/ulule/limiter-examples/tree/master/gin/main.go) * [Beego](https://github.com/ulule/limiter-examples/blob/master/beego/main.go) * [Chi](https://github.com/ulule/limiter-examples/tree/master/chi/main.go) * [Echo](https://github.com/ulule/limiter-examples/tree/master/echo/main.go) limiter-3.3.3/go.mod000066400000000000000000000010401356673232200142770ustar00rootroot00000000000000module github.com/ulule/limiter/v3 go 1.12 require ( github.com/gin-gonic/gin v1.5.0 github.com/go-redis/redis v6.15.6+incompatible github.com/golang/protobuf v1.3.2 // indirect github.com/json-iterator/go v1.1.7 // indirect github.com/mattn/go-isatty v0.0.8 // indirect github.com/onsi/ginkgo v1.8.0 // indirect github.com/onsi/gomega v1.5.0 // indirect github.com/pkg/errors v0.8.1 github.com/stretchr/testify v1.4.0 github.com/ugorji/go/codec v1.1.7 // indirect golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 // indirect ) limiter-3.3.3/go.sum000066400000000000000000000222511356673232200143330ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ= github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc= github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/go-redis/redis v6.15.5+incompatible h1:pLky8I0rgiblWfa8C1EV7fPEUv0aH6vKRaYHc/YRHVk= github.com/go-redis/redis v6.15.5+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-redis/redis v6.15.6+incompatible h1:H9evprGPLI8+ci7fxQx6WNZHJSb7be8FqJQRhdQZ5Sg= github.com/go-redis/redis v6.15.6+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc= gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= limiter-3.3.3/limiter.go000066400000000000000000000027471356673232200152040ustar00rootroot00000000000000package limiter import ( "context" ) // ----------------------------------------------------------------- // Context // ----------------------------------------------------------------- // Context is the limit context. type Context struct { Limit int64 Remaining int64 Reset int64 Reached bool } // ----------------------------------------------------------------- // Limiter // ----------------------------------------------------------------- // Limiter is the limiter instance. type Limiter struct { Store Store Rate Rate Options Options } // New returns an instance of Limiter. func New(store Store, rate Rate, options ...Option) *Limiter { opt := Options{ IPv4Mask: DefaultIPv4Mask, IPv6Mask: DefaultIPv6Mask, TrustForwardHeader: false, } for _, o := range options { o(&opt) } return &Limiter{ Store: store, Rate: rate, Options: opt, } } // Get returns the limit for given identifier. func (limiter *Limiter) Get(ctx context.Context, key string) (Context, error) { return limiter.Store.Get(ctx, key, limiter.Rate) } // Peek returns the limit for given identifier, without modification on current values. func (limiter *Limiter) Peek(ctx context.Context, key string) (Context, error) { return limiter.Store.Peek(ctx, key, limiter.Rate) } // Reset sets the limit for given identifier to zero. func (limiter *Limiter) Reset(ctx context.Context, key string) (Context, error) { return limiter.Store.Reset(ctx, key, limiter.Rate) } limiter-3.3.3/limiter_test.go000066400000000000000000000005121356673232200162270ustar00rootroot00000000000000package limiter_test import ( "time" "github.com/ulule/limiter/v3" "github.com/ulule/limiter/v3/drivers/store/memory" ) func New(options ...limiter.Option) *limiter.Limiter { store := memory.NewStore() rate := limiter.Rate{ Period: 1 * time.Second, Limit: int64(10), } return limiter.New(store, rate, options...) } limiter-3.3.3/network.go000066400000000000000000000035111356673232200152160ustar00rootroot00000000000000package limiter import ( "net" "net/http" "strings" ) var ( // DefaultIPv4Mask defines the default IPv4 mask used to obtain user IP. DefaultIPv4Mask = net.CIDRMask(32, 32) // DefaultIPv6Mask defines the default IPv6 mask used to obtain user IP. DefaultIPv6Mask = net.CIDRMask(128, 128) ) // GetIP returns IP address from request. func (limiter *Limiter) GetIP(r *http.Request) net.IP { return GetIP(r, limiter.Options) } // GetIPWithMask returns IP address from request by applying a mask. func (limiter *Limiter) GetIPWithMask(r *http.Request) net.IP { return GetIPWithMask(r, limiter.Options) } // GetIPKey extracts IP from request and returns hashed IP to use as store key. func (limiter *Limiter) GetIPKey(r *http.Request) string { return limiter.GetIPWithMask(r).String() } // GetIP returns IP address from request. // If options is defined and TrustForwardHeader is true, it will lookup IP in // X-Forwarded-For and X-Real-IP headers. func GetIP(r *http.Request, options ...Options) net.IP { if len(options) >= 1 && options[0].TrustForwardHeader { ip := r.Header.Get("X-Forwarded-For") if ip != "" { parts := strings.SplitN(ip, ",", 2) part := strings.TrimSpace(parts[0]) return net.ParseIP(part) } ip = strings.TrimSpace(r.Header.Get("X-Real-IP")) if ip != "" { return net.ParseIP(ip) } } remoteAddr := strings.TrimSpace(r.RemoteAddr) host, _, err := net.SplitHostPort(remoteAddr) if err != nil { return net.ParseIP(remoteAddr) } return net.ParseIP(host) } // GetIPWithMask returns IP address from request by applying a mask. func GetIPWithMask(r *http.Request, options ...Options) net.IP { if len(options) == 0 { return GetIP(r) } ip := GetIP(r, options[0]) if ip.To4() != nil { return ip.Mask(options[0].IPv4Mask) } if ip.To16() != nil { return ip.Mask(options[0].IPv6Mask) } return ip } limiter-3.3.3/network_test.go000066400000000000000000000115271356673232200162630ustar00rootroot00000000000000package limiter_test import ( "fmt" "net" "net/http" "net/url" "testing" "github.com/stretchr/testify/require" "github.com/ulule/limiter/v3" ) func TestGetIP(t *testing.T) { is := require.New(t) limiter1 := New(limiter.WithTrustForwardHeader(false)) limiter2 := New(limiter.WithTrustForwardHeader(true)) limiter3 := New(limiter.WithIPv4Mask(net.CIDRMask(24, 32))) limiter4 := New(limiter.WithIPv6Mask(net.CIDRMask(48, 128))) request1 := &http.Request{ URL: &url.URL{Path: "/"}, Header: http.Header{}, RemoteAddr: "8.8.8.8:8888", } request2 := &http.Request{ URL: &url.URL{Path: "/foo"}, Header: http.Header{}, RemoteAddr: "8.8.8.8:8888", } request2.Header.Add("X-Forwarded-For", "9.9.9.9, 7.7.7.7, 6.6.6.6") request3 := &http.Request{ URL: &url.URL{Path: "/bar"}, Header: http.Header{}, RemoteAddr: "8.8.8.8:8888", } request3.Header.Add("X-Real-IP", "6.6.6.6") request4 := &http.Request{ URL: &url.URL{Path: "/"}, Header: http.Header{}, RemoteAddr: "[2001:db8:cafe:1234:beef::fafa]:8888", } scenarios := []struct { request *http.Request limiter *limiter.Limiter expected net.IP }{ { // // Scenario #1 : RemoteAddr without proxy. // request: request1, limiter: limiter1, expected: net.ParseIP("8.8.8.8").To4(), }, { // // Scenario #2 : X-Forwarded-For without proxy. // request: request2, limiter: limiter1, expected: net.ParseIP("8.8.8.8").To4(), }, { // // Scenario #3 : X-Real-IP without proxy. // request: request3, limiter: limiter1, expected: net.ParseIP("8.8.8.8").To4(), }, { // // Scenario #4 : RemoteAddr with proxy. // request: request1, limiter: limiter2, expected: net.ParseIP("8.8.8.8").To4(), }, { // // Scenario #5 : X-Forwarded-For with proxy. // request: request2, limiter: limiter2, expected: net.ParseIP("9.9.9.9").To4(), }, { // // Scenario #6 : X-Real-IP with proxy. // request: request3, limiter: limiter2, expected: net.ParseIP("6.6.6.6").To4(), }, { // // Scenario #7 : IPv4 with mask. // request: request1, limiter: limiter3, expected: net.ParseIP("8.8.8.0").To4(), }, { // // Scenario #8 : IPv6 with mask. // request: request4, limiter: limiter4, expected: net.ParseIP("2001:db8:cafe::").To16(), }, } for i, scenario := range scenarios { message := fmt.Sprintf("Scenario #%d", (i + 1)) ip := scenario.limiter.GetIPWithMask(scenario.request) is.Equal(scenario.expected, ip, message) } } func TestGetIPKey(t *testing.T) { is := require.New(t) limiter1 := New(limiter.WithTrustForwardHeader(false)) limiter2 := New(limiter.WithTrustForwardHeader(true)) limiter3 := New(limiter.WithIPv4Mask(net.CIDRMask(24, 32))) limiter4 := New(limiter.WithIPv6Mask(net.CIDRMask(48, 128))) request1 := &http.Request{ URL: &url.URL{Path: "/"}, Header: http.Header{}, RemoteAddr: "8.8.8.8:8888", } request2 := &http.Request{ URL: &url.URL{Path: "/foo"}, Header: http.Header{}, RemoteAddr: "8.8.8.8:8888", } request2.Header.Add("X-Forwarded-For", "9.9.9.9, 7.7.7.7, 6.6.6.6") request3 := &http.Request{ URL: &url.URL{Path: "/bar"}, Header: http.Header{}, RemoteAddr: "8.8.8.8:8888", } request3.Header.Add("X-Real-IP", "6.6.6.6") request4 := &http.Request{ URL: &url.URL{Path: "/"}, Header: http.Header{}, RemoteAddr: "[2001:db8:cafe:1234:beef::fafa]:8888", } scenarios := []struct { request *http.Request limiter *limiter.Limiter expected string }{ { // // Scenario #1 : RemoteAddr without proxy. // request: request1, limiter: limiter1, expected: "8.8.8.8", }, { // // Scenario #2 : X-Forwarded-For without proxy. // request: request2, limiter: limiter1, expected: "8.8.8.8", }, { // // Scenario #3 : X-Real-IP without proxy. // request: request3, limiter: limiter1, expected: "8.8.8.8", }, { // // Scenario #4 : RemoteAddr without proxy. // request: request1, limiter: limiter2, expected: "8.8.8.8", }, { // // Scenario #5 : X-Forwarded-For without proxy. // request: request2, limiter: limiter2, expected: "9.9.9.9", }, { // // Scenario #6 : X-Real-IP without proxy. // request: request3, limiter: limiter2, expected: "6.6.6.6", }, { // // Scenario #7 : IPv4 with mask. // request: request1, limiter: limiter3, expected: "8.8.8.0", }, { // // Scenario #8 : IPv6 with mask. // request: request4, limiter: limiter4, expected: "2001:db8:cafe::", }, } for i, scenario := range scenarios { message := fmt.Sprintf("Scenario #%d", (i + 1)) key := scenario.limiter.GetIPKey(scenario.request) is.Equal(scenario.expected, key, message) } } limiter-3.3.3/options.go000066400000000000000000000017611356673232200152250ustar00rootroot00000000000000package limiter import ( "net" ) // Option is a functional option. type Option func(*Options) // Options are limiter options. type Options struct { // IPv4Mask defines the mask used to obtain a IPv4 address. IPv4Mask net.IPMask // IPv6Mask defines the mask used to obtain a IPv6 address. IPv6Mask net.IPMask // TrustForwardHeader enable parsing of X-Real-IP and X-Forwarded-For headers to obtain user IP. TrustForwardHeader bool } // WithIPv4Mask will configure the limiter to use given mask for IPv4 address. func WithIPv4Mask(mask net.IPMask) Option { return func(o *Options) { o.IPv4Mask = mask } } // WithIPv6Mask will configure the limiter to use given mask for IPv6 address. func WithIPv6Mask(mask net.IPMask) Option { return func(o *Options) { o.IPv6Mask = mask } } // WithTrustForwardHeader will configure the limiter to trust X-Real-IP and X-Forwarded-For headers. func WithTrustForwardHeader(enable bool) Option { return func(o *Options) { o.TrustForwardHeader = enable } } limiter-3.3.3/rate.go000066400000000000000000000020171356673232200144600ustar00rootroot00000000000000package limiter import ( "strconv" "strings" "time" "github.com/pkg/errors" ) // Rate is the rate. type Rate struct { Formatted string Period time.Duration Limit int64 } // NewRateFromFormatted returns the rate from the formatted version. func NewRateFromFormatted(formatted string) (Rate, error) { rate := Rate{} values := strings.Split(formatted, "-") if len(values) != 2 { return rate, errors.Errorf("incorrect format '%s'", formatted) } periods := map[string]time.Duration{ "S": time.Second, // Second "M": time.Minute, // Minute "H": time.Hour, // Hour "D": time.Hour * 24, // Day } limit, period := values[0], strings.ToUpper(values[1]) duration, ok := periods[period] if !ok { return rate, errors.Errorf("incorrect period '%s'", period) } p := 1 * duration l, err := strconv.ParseInt(limit, 10, 64) if err != nil { return rate, errors.Errorf("incorrect limit '%s'", limit) } rate = Rate{ Formatted: formatted, Period: p, Limit: l, } return rate, nil } limiter-3.3.3/rate_test.go000066400000000000000000000016661356673232200155300ustar00rootroot00000000000000package limiter_test import ( "reflect" "testing" "time" "github.com/stretchr/testify/require" "github.com/ulule/limiter/v3" ) // TestRate tests Rate methods. func TestRate(t *testing.T) { is := require.New(t) expected := map[string]limiter.Rate{ "10-S": { Formatted: "10-S", Period: 1 * time.Second, Limit: int64(10), }, "356-M": { Formatted: "356-M", Period: 1 * time.Minute, Limit: int64(356), }, "3-H": { Formatted: "3-H", Period: 1 * time.Hour, Limit: int64(3), }, "2000-D": { Formatted: "2000-D", Period: 24 * time.Hour, Limit: int64(2000), }, } for k, v := range expected { r, err := limiter.NewRateFromFormatted(k) is.NoError(err) is.True(reflect.DeepEqual(v, r)) } wrongs := []string{ "10 S", "10:S", "AZERTY", "na wak", "H-10", } for _, w := range wrongs { _, err := limiter.NewRateFromFormatted(w) is.Error(err) } } limiter-3.3.3/scripts/000077500000000000000000000000001356673232200146655ustar00rootroot00000000000000limiter-3.3.3/scripts/conf/000077500000000000000000000000001356673232200156125ustar00rootroot00000000000000limiter-3.3.3/scripts/conf/go/000077500000000000000000000000001356673232200162175ustar00rootroot00000000000000limiter-3.3.3/scripts/conf/go/Dockerfile000066400000000000000000000012121356673232200202050ustar00rootroot00000000000000FROM golang:1.13-buster MAINTAINER thomas@leroux.io ENV DEBIAN_FRONTEND noninteractive ENV LANG C.UTF-8 ENV LC_ALL C.UTF-8 RUN apt-get -y update \ && apt-get upgrade -y \ && apt-get -y install git \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ && useradd -ms /bin/bash gopher COPY go.mod go.sum /media/ulule/limiter/ RUN chown -R gopher:gopher /media/ulule/limiter ENV GOPATH /home/gopher/go ENV PATH $GOPATH/bin:$PATH USER gopher RUN go get -u github.com/golangci/golangci-lint/cmd/golangci-lint WORKDIR /media/ulule/limiter RUN go mod download COPY --chown=gopher:gopher . /media/ulule/limiter CMD [ "/bin/bash" ] limiter-3.3.3/scripts/go-wrapper000077500000000000000000000021541356673232200167000ustar00rootroot00000000000000#!/bin/bash set -eo pipefail SOURCE_DIRECTORY=$(dirname "${BASH_SOURCE[0]}") cd "${SOURCE_DIRECTORY}/.." ROOT_DIRECTORY=`pwd` IMAGE_NAME="limiter-go" DOCKERFILE="scripts/conf/go/Dockerfile" CONTAINER_IMAGE="golang:1.13-buster" if [[ -n "$REDIS_DISABLE_BOOTSTRAP" ]]; then REDIS_DISABLE_BOOTSTRAP_OPTS="-e REDIS_DISABLE_BOOTSTRAP=$REDIS_DISABLE_BOOTSTRAP" fi if [[ -n "$REDIS_URI" ]]; then REDIS_URI_OPTS="-e REDIS_URI=$REDIS_URI" fi create_docker_image() { declare tag="$1" dockerfile="$2" path="$3" echo "[go-wrapper] update golang image" docker pull ${CONTAINER_IMAGE} || true echo "[go-wrapper] build docker image" docker build -f "${dockerfile}" -t "${tag}" "${path}" } do_command() { declare command="$@" echo "[go-wrapper] run '${command}' in docker container" docker run --rm --net=host ${REDIS_DISABLE_BOOTSTRAP_OPTS} ${REDIS_URI_OPTS} \ "${IMAGE_NAME}" ${command} } do_usage() { echo >&2 "Usage: $0 command" exit 255 } if [ -z "$1" ]; then do_usage fi create_docker_image "${IMAGE_NAME}" "${DOCKERFILE}" "${ROOT_DIRECTORY}" do_command "$@" exit 0 limiter-3.3.3/scripts/lint000077500000000000000000000011501356673232200155560ustar00rootroot00000000000000#!/bin/bash set -eo pipefail if [[ ! -x "$(command -v go)" ]]; then echo >&2 "go runtime is required: https://golang.org/doc/install" echo >&2 "You can use scripts/go-wrapper $0 to use go in a docker container." exit 1 fi golinter_path="${GOPATH}/bin/golangci-lint" if [[ ! -x "${golinter_path}" ]]; then go get -u -d github.com/golangci/golangci-lint/cmd/golangci-lint go install github.com/golangci/golangci-lint/cmd/golangci-lint fi SOURCE_DIRECTORY=$(dirname "${BASH_SOURCE[0]}") cd "${SOURCE_DIRECTORY}/.." if [[ -n $1 ]]; then golangci-lint run "$1" else golangci-lint run ./... fi limiter-3.3.3/scripts/redis000077500000000000000000000026671356673232200157340ustar00rootroot00000000000000#!/bin/bash set -eo pipefail DOCKER_REDIS_PORT=${DOCKER_REDIS_PORT:-26379} CONTAINER_NAME="limiter-redis" CONTAINER_IMAGE="redis:5.0" do_start() { if [[ -n "$(docker ps -q -f name="${CONTAINER_NAME}" 2> /dev/null)" ]]; then echo "[redis] ${CONTAINER_NAME} already started. (use --restart otherwise)" return 0 fi if [[ -n "$(docker ps -a -q -f name="${CONTAINER_NAME}" 2> /dev/null)" ]]; then echo "[redis] erase previous configuration" docker stop "${CONTAINER_NAME}" >/dev/null 2>&1 || true docker rm "${CONTAINER_NAME}" >/dev/null 2>&1 || true fi echo "[redis] update redis images" docker pull ${CONTAINER_IMAGE} || true echo "[redis] start new ${CONTAINER_NAME} container" docker run --name "${CONTAINER_NAME}" \ -p ${DOCKER_REDIS_PORT}:6379 \ -d ${CONTAINER_IMAGE} >/dev/null } do_stop() { echo "[redis] stop ${CONTAINER_NAME} container" docker stop "${CONTAINER_NAME}" >/dev/null 2>&1 || true docker rm "${CONTAINER_NAME}" >/dev/null 2>&1 || true } do_client() { echo "[redis] use redis-cli on ${CONTAINER_NAME}" docker run --rm -it \ --link "${CONTAINER_NAME}":redis \ ${CONTAINER_IMAGE} redis-cli -h redis -p 6379 -n 1 } case "$1" in --stop) do_stop ;; --restart) do_stop do_start ;; --client) do_client ;; --start | *) do_start ;; esac exit 0 limiter-3.3.3/scripts/test000077500000000000000000000007561356673232200156020ustar00rootroot00000000000000#!/bin/bash if [[ ! -x "$(command -v go)" ]]; then echo >&2 "go runtime is required: https://golang.org/doc/install" echo >&2 "You can use scripts/go-wrapper $0 to use go in a docker container." exit 1 fi SOURCE_DIRECTORY=$(dirname "${BASH_SOURCE[0]}") cd "${SOURCE_DIRECTORY}/.." if [ -z "$REDIS_DISABLE_BOOTSTRAP" ]; then export REDIS_URI="redis://localhost:26379/1" scripts/redis --restart fi go test -count=1 -race -v $(go list ./... | grep -v -E '\/(vendor|examples)\/') limiter-3.3.3/store.go000066400000000000000000000014551356673232200146660ustar00rootroot00000000000000package limiter import ( "context" "time" ) // Store is the common interface for limiter stores. type Store interface { // Get returns the limit for given identifier. Get(ctx context.Context, key string, rate Rate) (Context, error) // Peek returns the limit for given identifier, without modification on current values. Peek(ctx context.Context, key string, rate Rate) (Context, error) // Reset resets the limit to zero for given identifier. Reset(ctx context.Context, key string, rate Rate) (Context, error) } // StoreOptions are options for store. type StoreOptions struct { // Prefix is the prefix to use for the key. Prefix string // MaxRetry is the maximum number of retry under race conditions. MaxRetry int // CleanUpInterval is the interval for cleanup. CleanUpInterval time.Duration }