New Rate Limit middleware (still WIP though)

Former-commit-id: 99e282e4d400c83a56a808212d812cd701e1bcd8
This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-05-01 23:33:04 +03:00
parent f667bc5ff3
commit 3775189de8
6 changed files with 212 additions and 4 deletions

6
FAQ.md
View File

@ -47,12 +47,12 @@ open for Iris-specific developers the time we speak.
Go to our facebook page, like it and receive notifications about new job offers, we already have couple of them stay at the top of the page: https://www.facebook.com/iris.framework Go to our facebook page, like it and receive notifications about new job offers, we already have couple of them stay at the top of the page: https://www.facebook.com/iris.framework
## Do we have a community Chat? ## Do we have a Community chat?
Yes, https://chat.iris-go.com Yes, https://chat.iris-go.com
## How is the development of Iris supported? ## How is the development of Iris economically supported?
By normal people, like you, who help us by donating small or large amounts of money. By people like you, who help us by donating small or large amounts of money.
Help this project deliver awesome and unique features with the highest possible code quality by donating any amount via [PayPal](https://www.paypal.me/kataras). Your name will be published [here](https://iris-go.com) after your approval via e-mail. Help this project deliver awesome and unique features with the highest possible code quality by donating any amount via [PayPal](https://www.paypal.me/kataras). Your name will be published [here](https://iris-go.com) after your approval via e-mail.

View File

@ -283,13 +283,15 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her
### Miscellaneous ### Miscellaneous
- [Rate Limit](miscellaneous/ratelimit/main.go) **NEW**
- [HTTP Method Override](https://github.com/kataras/iris/blob/master/middleware/methodoverride/methodoverride_test.go) - [HTTP Method Override](https://github.com/kataras/iris/blob/master/middleware/methodoverride/methodoverride_test.go)
- [Request Logger](http_request/request-logger/main.go) - [Request Logger](http_request/request-logger/main.go)
* [log requests to a file](http_request/request-logger/request-logger-file/main.go) * [log requests to a file](http_request/request-logger/request-logger-file/main.go)
- [Recovery](miscellaneous/recover/main.go) - [Recovery](miscellaneous/recover/main.go)
- [Profiling (pprof)](miscellaneous/pprof/main.go) - [Profiling (pprof)](miscellaneous/pprof/main.go)
- [Internal Application File Logger](miscellaneous/file-logger/main.go) - [Internal Application File Logger](miscellaneous/file-logger/main.go)
- [Google reCAPTCHA](miscellaneous/recaptcha/main.go) - [Google reCAPTCHA](miscellaneous/recaptcha/main.go)
- [hCaptcha](miscellaneous/hcaptcha/main.go) **NEW**
### Community-based Handlers ### Community-based Handlers

View File

@ -0,0 +1,47 @@
package main
import (
"time"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/middleware/rate"
)
func main() {
app := newApp()
app.Logger().SetLevel("debug")
app.Listen(":8080")
}
func newApp() *iris.Application {
app := iris.New()
// Register the rate limiter middleware at the root router.
//
// Fist and second input parameters:
// Allow 1 request per second, with a maximum burst size of 5.
//
// Third optional variadic input parameter:
// Can be a cleanup function.
// Iris provides a cleanup function that will check for old entries and remove them.
// You can customize it, e.g. check every 1 minute
// if a client's last visit was 5 minutes ago ("old" entry)
// and remove it from the memory.
rateLimiter := rate.Limit(1, 5, rate.PurgeEvery(time.Minute, 5*time.Minute))
app.Use(rateLimiter)
// Routes.
app.Get("/", index)
app.Get("/other", other)
return app
}
func index(ctx iris.Context) {
ctx.HTML("<h1>Index Page</h1>")
}
func other(ctx iris.Context) {
ctx.HTML("<h1>Other Page</h1>")
}

1
go.mod
View File

@ -33,6 +33,7 @@ require (
go.etcd.io/bbolt v1.3.4 go.etcd.io/bbolt v1.3.4
golang.org/x/crypto v0.0.0-20200427165652-729f1e841bcc golang.org/x/crypto v0.0.0-20200427165652-729f1e841bcc
golang.org/x/text v0.3.2 golang.org/x/text v0.3.2
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1
gopkg.in/ini.v1 v1.55.0 gopkg.in/ini.v1 v1.55.0
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
) )

View File

@ -10,6 +10,7 @@ Builtin Handlers
| [Google reCAPTCHA](recaptcha) | [iris/_examples/miscellaneous/recaptcha](https://github.com/kataras/iris/tree/master/_examples/miscellaneous/recaptcha) | | [Google reCAPTCHA](recaptcha) | [iris/_examples/miscellaneous/recaptcha](https://github.com/kataras/iris/tree/master/_examples/miscellaneous/recaptcha) |
| [hCaptcha](hcaptcha) | [iris/_examples/miscellaneous/recaptcha](https://github.com/kataras/iris/tree/master/_examples/miscellaneous/hcaptcha) | | [hCaptcha](hcaptcha) | [iris/_examples/miscellaneous/recaptcha](https://github.com/kataras/iris/tree/master/_examples/miscellaneous/hcaptcha) |
| [recovery](recover) | [iris/_examples/miscellaneous/recover](https://github.com/kataras/iris/tree/master/_examples/miscellaneous/recover) | | [recovery](recover) | [iris/_examples/miscellaneous/recover](https://github.com/kataras/iris/tree/master/_examples/miscellaneous/recover) |
| [rate](rate) | [iris/_examples/miscellaneous/ratelimit](https://github.com/kataras/iris/tree/master/_examples/miscellaneous/ratelimit) |
Community made Community made
------------ ------------

157
middleware/rate/rate.go Normal file
View File

@ -0,0 +1,157 @@
// TODO: godoc and add tests.
package rate
import (
"math"
"sync"
"time"
"github.com/kataras/iris/v12/context"
"golang.org/x/time/rate"
)
func init() {
context.SetHandlerName("iris/middleware/rate.(*Limiter).serveHTTP-fm", "iris.ratelimit")
}
type Option func(*Limiter)
func ExceedHandler(handler context.Handler) Option {
return func(l *Limiter) {
l.exceedHandler = handler
}
}
func ClientData(clientDataFunc func(ctx context.Context) interface{}) Option {
return func(l *Limiter) {
l.clientDataFunc = clientDataFunc
}
}
func PurgeEvery(every time.Duration, maxLifetime time.Duration) Option {
condition := func(c *Client) bool {
// for a custom purger the end-developer may use the c.Data filled from a `ClientData` option.
return time.Since(c.LastSeen) > maxLifetime
}
return func(l *Limiter) {
go func() {
for {
time.Sleep(every)
l.Purge(condition)
}
}()
}
}
type (
Limiter struct {
clientDataFunc func(ctx context.Context) interface{} // fill the Client's Data field.
exceedHandler context.Handler // when too many requests.
clients map[string]*Client
mu sync.RWMutex // mutex for clients.
pool *sync.Pool // object pool for clients.
}
Client struct {
limiter *rate.Limiter
LastSeen time.Time
IP string
Data interface{}
}
)
// Inf is the infinite rate limit; it allows all events (even if burst is zero).
const Inf = math.MaxFloat64
func Limit(limit float64, burst int, options ...Option) context.Handler {
rateLimit := rate.Limit(limit)
l := &Limiter{
clients: make(map[string]*Client),
pool: &sync.Pool{New: func() interface{} {
return &Client{limiter: rate.NewLimiter(rateLimit, burst)}
}},
exceedHandler: func(ctx context.Context) {
ctx.StopWithStatus(429) // Too Many Requests.
},
}
for _, opt := range options {
opt(l)
}
return l.serveHTTP
}
func (l *Limiter) acquire() *Client {
v := l.pool.Get().(*Client)
v.LastSeen = time.Now()
return v
}
func (l *Limiter) release(client *Client) {
client.IP = ""
client.Data = nil
l.pool.Put(client)
}
func (l *Limiter) Purge(condition func(*Client) bool) {
l.mu.Lock()
for ip, client := range l.clients {
if condition(client) {
l.release(client)
delete(l.clients, ip)
}
}
l.mu.Unlock()
}
func (l *Limiter) serveHTTP(ctx context.Context) {
ip := ctx.RemoteAddr()
l.mu.RLock()
client, ok := l.clients[ip]
l.mu.RUnlock()
if !ok {
client = l.acquire()
client.IP = ip
if l.clientDataFunc != nil {
client.Data = l.clientDataFunc(ctx)
}
// if l.store(ctx, client) {
// ^ no, let's keep it simple.
l.mu.Lock()
l.clients[ip] = client
l.mu.Unlock()
}
client.LastSeen = time.Now()
ctx.Values().Set(clientContextKey, client)
if client.limiter.Allow() {
ctx.Next()
return
}
if l.exceedHandler != nil {
l.exceedHandler(ctx)
}
}
const clientContextKey = "iris.ratelimit.client"
func Get(ctx context.Context) *Client {
if v := ctx.Values().Get(clientContextKey); v != nil {
if c, ok := v.(*Client); ok {
return c
}
}
return nil
}