Skip to content

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
gillesfabio committed Oct 2, 2015
0 parents commit e5b91ee
Show file tree
Hide file tree
Showing 17 changed files with 870 additions and 0 deletions.
25 changes: 25 additions & 0 deletions .editorconfig
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
21 changes: 21 additions & 0 deletions LICENSE
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.
7 changes: 7 additions & 0 deletions Makefile
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)
133 changes: 133 additions & 0 deletions README.md
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
50 changes: 50 additions & 0 deletions examples/gjr/main.go
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()))
}
45 changes: 45 additions & 0 deletions examples/http/main.go
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"}`))
}
46 changes: 46 additions & 0 deletions limiter.go
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)
}
Loading

0 comments on commit e5b91ee

Please sign in to comment.