init
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Didip Kerabat
|
||||
|
||||
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.
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
[](http://godoc.org/github.com/didip/tollbooth)
|
||||
[](https://raw.githubusercontent.com/didip/tollbooth/master/LICENSE)
|
||||
|
||||
## Tollbooth
|
||||
|
||||
This is a generic middleware to rate-limit HTTP requests.
|
||||
|
||||
**NOTE:** This library is considered finished, any new activities are probably centered around `thirdparty` modules.
|
||||
|
||||
|
||||
## Five Minutes Tutorial
|
||||
```
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/didip/tollbooth"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func HelloHandler(w http.ResponseWriter, req *http.Request) {
|
||||
w.Write([]byte("Hello, World!"))
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Create a request limiter per handler.
|
||||
http.Handle("/", tollbooth.LimitFuncHandler(tollbooth.NewLimiter(1, time.Second), HelloHandler))
|
||||
http.ListenAndServe(":12345", nil)
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
1. Rate-limit by request's remote IP, path, methods, custom headers, & basic auth usernames.
|
||||
```
|
||||
limiter := tollbooth.NewLimiter(1, time.Second)
|
||||
|
||||
// Configure list of places to look for IP address.
|
||||
// By default it's: "RemoteAddr", "X-Forwarded-For", "X-Real-IP"
|
||||
// If your application is behind a proxy, set "X-Forwarded-For" first.
|
||||
limiter.IPLookups = []string{"RemoteAddr", "X-Forwarded-For", "X-Real-IP"}
|
||||
|
||||
// Limit only GET and POST requests.
|
||||
limiter.Methods = []string{"GET", "POST"}
|
||||
|
||||
// Limit request headers containing certain values.
|
||||
// Typically, you prefetched these values from the database.
|
||||
limiter.Headers = make(map[string][]string)
|
||||
limiter.Headers["X-Access-Token"] = []string{"abc123", "xyz098"}
|
||||
|
||||
// Limit based on basic auth usernames.
|
||||
// Typically, you prefetched these values from the database.
|
||||
limiter.BasicAuthUsers = []string{"bob", "joe", "didip"}
|
||||
```
|
||||
|
||||
2. Each request handler can be rate-limited individually.
|
||||
|
||||
3. Compose your own middleware by using `LimitByKeys()`.
|
||||
|
||||
4. Tollbooth does not require external storage since it uses an algorithm called [Token Bucket](http://en.wikipedia.org/wiki/Token_bucket) [(Go library: ratelimit)](https://github.com/juju/ratelimit).
|
||||
|
||||
|
||||
# Other Web Frameworks
|
||||
|
||||
Support for other web frameworks are defined under `/thirdparty` directory.
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
// Package config provides data structure to configure rate-limiter.
|
||||
package config
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/juju/ratelimit"
|
||||
)
|
||||
|
||||
// NewLimiter is a constructor for Limiter.
|
||||
func NewLimiter(max int64, ttl time.Duration) *Limiter {
|
||||
limiter := &Limiter{Max: max, TTL: ttl}
|
||||
limiter.MessageContentType = "text/plain; charset=utf-8"
|
||||
limiter.Message = "You have reached maximum request limit."
|
||||
limiter.StatusCode = 429
|
||||
limiter.tokenBuckets = make(map[string]*ratelimit.Bucket)
|
||||
limiter.IPLookups = []string{"RemoteAddr", "X-Forwarded-For", "X-Real-IP"}
|
||||
|
||||
return limiter
|
||||
}
|
||||
|
||||
// Limiter is a config struct to limit a particular request handler.
|
||||
type Limiter struct {
|
||||
// HTTP message when limit is reached.
|
||||
Message string
|
||||
|
||||
// Content-Type for Message
|
||||
MessageContentType string
|
||||
|
||||
// HTTP status code when limit is reached.
|
||||
StatusCode int
|
||||
|
||||
// Maximum number of requests to limit per duration.
|
||||
Max int64
|
||||
|
||||
// Duration of rate-limiter.
|
||||
TTL time.Duration
|
||||
|
||||
// List of places to look up IP address.
|
||||
// Default is "RemoteAddr", "X-Forwarded-For", "X-Real-IP".
|
||||
// You can rearrange the order as you like.
|
||||
IPLookups []string
|
||||
|
||||
// List of HTTP Methods to limit (GET, POST, PUT, etc.).
|
||||
// Empty means limit all methods.
|
||||
Methods []string
|
||||
|
||||
// List of HTTP headers to limit.
|
||||
// Empty means skip headers checking.
|
||||
Headers map[string][]string
|
||||
|
||||
// List of basic auth usernames to limit.
|
||||
BasicAuthUsers []string
|
||||
|
||||
// Throttler struct
|
||||
tokenBuckets map[string]*ratelimit.Bucket
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// LimitReached returns a bool indicating if the Bucket identified by key ran out of tokens.
|
||||
func (l *Limiter) LimitReached(key string) bool {
|
||||
l.Lock()
|
||||
if _, found := l.tokenBuckets[key]; !found {
|
||||
l.tokenBuckets[key] = ratelimit.NewBucket(l.TTL, l.Max)
|
||||
}
|
||||
|
||||
_, isSoonerThanMaxWait := l.tokenBuckets[key].TakeMaxDuration(1, 0)
|
||||
l.Unlock()
|
||||
|
||||
if isSoonerThanMaxWait {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Package errors provide data structure for errors.
|
||||
package errors
|
||||
|
||||
import "fmt"
|
||||
|
||||
// HTTPError is an error struct that returns both message and status code.
|
||||
type HTTPError struct {
|
||||
Message string
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
// Error returns error message.
|
||||
func (httperror *HTTPError) Error() string {
|
||||
return fmt.Sprintf("%v: %v", httperror.StatusCode, httperror.Message)
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
// Package libstring provides various string related functions.
|
||||
package libstring
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StringInSlice finds needle in a slice of strings.
|
||||
func StringInSlice(sliceString []string, needle string) bool {
|
||||
for _, b := range sliceString {
|
||||
if b == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ipAddrFromRemoteAddr(s string) string {
|
||||
idx := strings.LastIndex(s, ":")
|
||||
if idx == -1 {
|
||||
return s
|
||||
}
|
||||
return s[:idx]
|
||||
}
|
||||
|
||||
// RemoteIP finds IP Address given http.Request struct.
|
||||
func RemoteIP(ipLookups []string, r *http.Request) string {
|
||||
realIP := r.Header.Get("X-Real-IP")
|
||||
forwardedFor := r.Header.Get("X-Forwarded-For")
|
||||
|
||||
for _, lookup := range ipLookups {
|
||||
if lookup == "RemoteAddr" {
|
||||
return ipAddrFromRemoteAddr(r.RemoteAddr)
|
||||
}
|
||||
if lookup == "X-Forwarded-For" && forwardedFor != "" {
|
||||
// X-Forwarded-For is potentially a list of addresses separated with ","
|
||||
parts := strings.Split(forwardedFor, ",")
|
||||
for i, p := range parts {
|
||||
parts[i] = strings.TrimSpace(p)
|
||||
}
|
||||
return parts[0]
|
||||
}
|
||||
if lookup == "X-Real-IP" && realIP != "" {
|
||||
return realIP
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
// Package tollbooth provides rate-limiting logic to HTTP request handler.
|
||||
package tollbooth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/didip/tollbooth/config"
|
||||
"github.com/didip/tollbooth/errors"
|
||||
"github.com/didip/tollbooth/libstring"
|
||||
)
|
||||
|
||||
// NewLimiter is a convenience function to config.NewLimiter.
|
||||
func NewLimiter(max int64, ttl time.Duration) *config.Limiter {
|
||||
return config.NewLimiter(max, ttl)
|
||||
}
|
||||
|
||||
// LimitByKeys keeps track number of request made by keys separated by pipe.
|
||||
// It returns HTTPError when limit is exceeded.
|
||||
func LimitByKeys(limiter *config.Limiter, keys []string) *errors.HTTPError {
|
||||
if limiter.LimitReached(strings.Join(keys, "|")) {
|
||||
return &errors.HTTPError{Message: limiter.Message, StatusCode: limiter.StatusCode}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LimitByRequest builds keys based on http.Request struct,
|
||||
// loops through all the keys, and check if any one of them returns HTTPError.
|
||||
func LimitByRequest(limiter *config.Limiter, r *http.Request) *errors.HTTPError {
|
||||
sliceKeys := BuildKeys(limiter, r)
|
||||
|
||||
// Loop sliceKeys and check if one of them has error.
|
||||
for _, keys := range sliceKeys {
|
||||
httpError := LimitByKeys(limiter, keys)
|
||||
if httpError != nil {
|
||||
return httpError
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildKeys generates a slice of keys to rate-limit by given config and request structs.
|
||||
func BuildKeys(limiter *config.Limiter, r *http.Request) [][]string {
|
||||
remoteIP := libstring.RemoteIP(limiter.IPLookups, r)
|
||||
path := r.URL.Path
|
||||
sliceKeys := make([][]string, 0)
|
||||
|
||||
// Don't BuildKeys if remoteIP is blank.
|
||||
if remoteIP == "" {
|
||||
return sliceKeys
|
||||
}
|
||||
|
||||
if limiter.Methods != nil && limiter.Headers != nil && limiter.BasicAuthUsers != nil {
|
||||
// Limit by HTTP methods and HTTP headers+values and Basic Auth credentials.
|
||||
if libstring.StringInSlice(limiter.Methods, r.Method) {
|
||||
for headerKey, headerValues := range limiter.Headers {
|
||||
if (headerValues == nil || len(headerValues) <= 0) && r.Header.Get(headerKey) != "" {
|
||||
// If header values are empty, rate-limit all request with headerKey.
|
||||
username, _, ok := r.BasicAuth()
|
||||
if ok && libstring.StringInSlice(limiter.BasicAuthUsers, username) {
|
||||
sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method, headerKey, username})
|
||||
}
|
||||
|
||||
} else if len(headerValues) > 0 && r.Header.Get(headerKey) != "" {
|
||||
// If header values are not empty, rate-limit all request with headerKey and headerValues.
|
||||
for _, headerValue := range headerValues {
|
||||
username, _, ok := r.BasicAuth()
|
||||
if ok && libstring.StringInSlice(limiter.BasicAuthUsers, username) {
|
||||
sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method, headerKey, headerValue, username})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if limiter.Methods != nil && limiter.Headers != nil {
|
||||
// Limit by HTTP methods and HTTP headers+values.
|
||||
if libstring.StringInSlice(limiter.Methods, r.Method) {
|
||||
for headerKey, headerValues := range limiter.Headers {
|
||||
if (headerValues == nil || len(headerValues) <= 0) && r.Header.Get(headerKey) != "" {
|
||||
// If header values are empty, rate-limit all request with headerKey.
|
||||
sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method, headerKey})
|
||||
|
||||
} else if len(headerValues) > 0 && r.Header.Get(headerKey) != "" {
|
||||
// If header values are not empty, rate-limit all request with headerKey and headerValues.
|
||||
for _, headerValue := range headerValues {
|
||||
sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method, headerKey, headerValue})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if limiter.Methods != nil && limiter.BasicAuthUsers != nil {
|
||||
// Limit by HTTP methods and Basic Auth credentials.
|
||||
if libstring.StringInSlice(limiter.Methods, r.Method) {
|
||||
username, _, ok := r.BasicAuth()
|
||||
if ok && libstring.StringInSlice(limiter.BasicAuthUsers, username) {
|
||||
sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method, username})
|
||||
}
|
||||
}
|
||||
|
||||
} else if limiter.Methods != nil {
|
||||
// Limit by HTTP methods.
|
||||
if libstring.StringInSlice(limiter.Methods, r.Method) {
|
||||
sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method})
|
||||
}
|
||||
|
||||
} else if limiter.Headers != nil {
|
||||
// Limit by HTTP headers+values.
|
||||
for headerKey, headerValues := range limiter.Headers {
|
||||
if (headerValues == nil || len(headerValues) <= 0) && r.Header.Get(headerKey) != "" {
|
||||
// If header values are empty, rate-limit all request with headerKey.
|
||||
sliceKeys = append(sliceKeys, []string{remoteIP, path, headerKey})
|
||||
|
||||
} else if len(headerValues) > 0 && r.Header.Get(headerKey) != "" {
|
||||
// If header values are not empty, rate-limit all request with headerKey and headerValues.
|
||||
for _, headerValue := range headerValues {
|
||||
sliceKeys = append(sliceKeys, []string{remoteIP, path, headerKey, headerValue})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if limiter.BasicAuthUsers != nil {
|
||||
// Limit by Basic Auth credentials.
|
||||
username, _, ok := r.BasicAuth()
|
||||
if ok && libstring.StringInSlice(limiter.BasicAuthUsers, username) {
|
||||
sliceKeys = append(sliceKeys, []string{remoteIP, path, username})
|
||||
}
|
||||
} else {
|
||||
// Default: Limit by remoteIP and path.
|
||||
sliceKeys = append(sliceKeys, []string{remoteIP, path})
|
||||
}
|
||||
|
||||
return sliceKeys
|
||||
}
|
||||
|
||||
// LimitHandler is a middleware that performs rate-limiting given http.Handler struct.
|
||||
func LimitHandler(limiter *config.Limiter, next http.Handler) http.Handler {
|
||||
middle := func(w http.ResponseWriter, r *http.Request) {
|
||||
httpError := LimitByRequest(limiter, r)
|
||||
if httpError != nil {
|
||||
w.Header().Add("Content-Type", limiter.MessageContentType)
|
||||
w.WriteHeader(httpError.StatusCode)
|
||||
w.Write([]byte(httpError.Message))
|
||||
return
|
||||
}
|
||||
|
||||
// There's no rate-limit error, serve the next handler.
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(middle)
|
||||
}
|
||||
|
||||
// LimitFuncHandler is a middleware that performs rate-limiting given request handler function.
|
||||
func LimitFuncHandler(limiter *config.Limiter, nextFunc func(http.ResponseWriter, *http.Request)) http.Handler {
|
||||
return LimitHandler(limiter, http.HandlerFunc(nextFunc))
|
||||
}
|
||||
Reference in New Issue
Block a user