diff --git a/FAQ.md b/FAQ.md index 5f925ed4..1d7a986f 100644 --- a/FAQ.md +++ b/FAQ.md @@ -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 -## Do we have a community Chat? +## Do we have a Community chat? 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. diff --git a/_examples/README.md b/_examples/README.md index aa836c25..723f27dd 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -283,13 +283,15 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her ### Miscellaneous +- [Rate Limit](miscellaneous/ratelimit/main.go) **NEW** - [HTTP Method Override](https://github.com/kataras/iris/blob/master/middleware/methodoverride/methodoverride_test.go) - [Request Logger](http_request/request-logger/main.go) * [log requests to a file](http_request/request-logger/request-logger-file/main.go) - [Recovery](miscellaneous/recover/main.go) - [Profiling (pprof)](miscellaneous/pprof/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 diff --git a/_examples/miscellaneous/ratelimit/main.go b/_examples/miscellaneous/ratelimit/main.go new file mode 100644 index 00000000..b204702d --- /dev/null +++ b/_examples/miscellaneous/ratelimit/main.go @@ -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("

Index Page

") +} + +func other(ctx iris.Context) { + ctx.HTML("

Other Page

") +} diff --git a/go.mod b/go.mod index aabb3938..78c8330b 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( go.etcd.io/bbolt v1.3.4 golang.org/x/crypto v0.0.0-20200427165652-729f1e841bcc 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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c ) diff --git a/middleware/README.md b/middleware/README.md index c8dfb4f2..95ab1160 100644 --- a/middleware/README.md +++ b/middleware/README.md @@ -10,6 +10,7 @@ Builtin Handlers | [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) | | [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 ------------ diff --git a/middleware/rate/rate.go b/middleware/rate/rate.go new file mode 100644 index 00000000..94005537 --- /dev/null +++ b/middleware/rate/rate.go @@ -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 +}