forked from ulule/limiter
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit e5b91ee
Showing
17 changed files
with
870 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
root = 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
The MIT License (MIT) | ||
|
||
Copyright (c) 2015 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
.PHONY: test | ||
|
||
cleandb: | ||
@(redis-cli KEYS "limitertests*" | xargs redis-cli DEL) | ||
|
||
test: cleandb | ||
@(go test -v -run ^Test) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
# Limiter | ||
|
||
*Dead simple rate limit middleware for Go.* | ||
|
||
* Simple API (your grandmother can use it) | ||
* "Store" approach for backend | ||
* Redis support but not tied too | ||
* go-json-rest middleware | ||
|
||
## The Why | ||
|
||
Why yet another rate limit package? Because existing packages did not suit our needs. | ||
|
||
We tried: | ||
|
||
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 [go-json-rest][2]). 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 less. 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][6]. 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. | ||
|
||
## Installation | ||
|
||
```bash | ||
$ go get github.com/ulule/limiter | ||
``` | ||
|
||
## Usage | ||
|
||
See `examples` folder for live examples. | ||
|
||
### Create a Limiter | ||
|
||
Create a `limiter.Limiter` instance for your middleware: | ||
|
||
```go | ||
// First, create a rate with the given limit (number of requests) for the given | ||
// period (a time.Duration of your choice). | ||
rate := limiter.Rate{ | ||
Period: 1 * time.Hour, | ||
Limit: int64(1000), | ||
} | ||
|
||
// You can also use the simplified format "<limit>-<period>"", with the given | ||
// periods: | ||
// | ||
// * "S": second | ||
// * "M": minute | ||
// * "H": hour | ||
// | ||
// Examples: | ||
// | ||
// * 5 reqs/second: "5-S" | ||
// * 10 reqs/minute: "10-M" | ||
// * 1000 reqs/hour: "1000-H" | ||
// | ||
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. | ||
store, err := ratelimit.NewRedisStore(pool) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
// Then, create the limiter instance which takes the store and the rate as arguments. | ||
// Now, you can give this instance to any supported middleware. | ||
limiter := ratelimit.NewLimiter(store, rate) | ||
``` | ||
|
||
### go-json-rest middleware | ||
|
||
Simply give your limiter instance to the middleware. Take a coffee. That's it. | ||
|
||
```go | ||
package main | ||
|
||
import ( | ||
"github.com/ant0ine/go-json-rest/rest" | ||
"log" | ||
"net/http" | ||
) | ||
|
||
func main() { | ||
// Here we create a new API instance with default development stack. | ||
api := rest.NewApi() | ||
api.Use(rest.DefaultDevStack...) | ||
|
||
// Let's add the bundled middleware with the limiter instance created above. | ||
api.Use(limiter.NewGJRMiddleware(limiter)) | ||
|
||
// Create a simple app just to play with. | ||
api.SetApp(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { | ||
w.WriteJson(map[string]string{"message": "ok"}) | ||
})) | ||
|
||
// Run server! | ||
log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) | ||
} | ||
``` | ||
|
||
[1]: https://github.com/throttled/throttled | ||
[2]: https://github.com/ant0ine/go-json-rest | ||
[3]: https://github.com/etcinit/speedbump | ||
[4]: https://github.com/gin-gonic/gin | ||
[5]: https://github.com/didip/tollbooth | ||
[6]: https://github.com/r8k/ratelimit |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"net/http" | ||
|
||
"github.com/ant0ine/go-json-rest/rest" | ||
"github.com/garyburd/redigo/redis" | ||
"github.com/ulule/limiter" | ||
) | ||
|
||
func main() { | ||
// 4 reqs/hour | ||
rate, err := limiter.NewRateFromFormatted("4-H") | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
// Create a Redis pool. | ||
pool := redis.NewPool(func() (redis.Conn, error) { | ||
c, err := redis.Dial("tcp", ":6379") | ||
if err != nil { | ||
return nil, err | ||
} | ||
return c, err | ||
}, 100) | ||
|
||
// Create a store with the pool. | ||
store, err := limiter.NewRedisStore(pool, "limitergjrexample") | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
// Create API. | ||
api := rest.NewApi() | ||
api.Use(rest.DefaultDevStack...) | ||
|
||
// Add middleware with the limiter instance. | ||
api.Use(limiter.NewGJRMiddleware(limiter.NewLimiter(store, rate))) | ||
|
||
// Set stupid app. | ||
api.SetApp(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { | ||
w.WriteJson(map[string]string{"message": "ok"}) | ||
})) | ||
|
||
// Run server! | ||
fmt.Println("Server is running on :3339...") | ||
log.Fatal(http.ListenAndServe(":3339", api.MakeHandler())) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"net/http" | ||
|
||
"github.com/garyburd/redigo/redis" | ||
"github.com/ulule/limiter" | ||
) | ||
|
||
func main() { | ||
// 4 reqs/hour | ||
rate, err := limiter.NewRateFromFormatted("4-H") | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
// Create a Redis pool. | ||
pool := redis.NewPool(func() (redis.Conn, error) { | ||
c, err := redis.Dial("tcp", ":6379") | ||
if err != nil { | ||
return nil, err | ||
} | ||
return c, err | ||
}, 100) | ||
|
||
// Create a store with the pool. | ||
store, err := limiter.NewRedisStore(pool, "limitergjrexample") | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
mw := limiter.NewHTTPMiddleware(limiter.NewLimiter(store, rate)) | ||
http.Handle("/", mw.Handler(http.HandlerFunc(index))) | ||
|
||
fmt.Println("Server is runnnig on port 7777...") | ||
log.Fatal(http.ListenAndServe(":7777", nil)) | ||
|
||
} | ||
|
||
func index(w http.ResponseWriter, r *http.Request) { | ||
w.Header().Set("Content-Type", "application/json") | ||
w.Write([]byte(`{"message": "ok"}`)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package limiter | ||
|
||
// ----------------------------------------------------------------- | ||
// Store | ||
// ----------------------------------------------------------------- | ||
|
||
// Store is the common interface for limiter stores. | ||
type Store interface { | ||
Get(key string, rate Rate) (Context, error) | ||
} | ||
|
||
// ----------------------------------------------------------------- | ||
// Context | ||
// ----------------------------------------------------------------- | ||
|
||
// Context is the limit context. | ||
type Context struct { | ||
Limit int64 | ||
Remaining int64 | ||
Used int64 | ||
Reset int64 | ||
Reached bool | ||
} | ||
|
||
// ----------------------------------------------------------------- | ||
// Limiter | ||
// ----------------------------------------------------------------- | ||
|
||
// Limiter is the limiter instance. | ||
type Limiter struct { | ||
Store Store | ||
Rate Rate | ||
} | ||
|
||
// NewLimiter returns an instance of ratelimit. | ||
func NewLimiter(store Store, rate Rate) *Limiter { | ||
return &Limiter{ | ||
Store: store, | ||
Rate: rate, | ||
} | ||
} | ||
|
||
// Get returns the limit for the identifier. | ||
func (l *Limiter) Get(key string) (Context, error) { | ||
return l.Store.Get(key, l.Rate) | ||
} |
Oops, something went wrong.