Update go dependencies (#1972)
This commit is contained in:
Generated
Vendored
Generated
Vendored
Generated
Vendored
+41
-10
@@ -25,6 +25,9 @@ This is a generic middleware to rate-limit HTTP requests.
|
||||
|
||||
**v7.x.x:** Replaced `time/rate` with `embedded time/rate` so that we can support more rate limit headers.
|
||||
|
||||
**v8.x.x:** Address `RemoteIP` vulnerability concern by replacing `SetIPLookups` with `SetIPLookup`, an explicit way to pick the IP address.
|
||||
|
||||
|
||||
## Five Minute Tutorial
|
||||
|
||||
```go
|
||||
@@ -33,7 +36,8 @@ package main
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/didip/tollbooth/v7"
|
||||
"github.com/didip/tollbooth/v8"
|
||||
"github.com/didip/tollbooth/v8/limiter"
|
||||
)
|
||||
|
||||
func HelloHandler(w http.ResponseWriter, req *http.Request) {
|
||||
@@ -42,7 +46,15 @@ func HelloHandler(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
func main() {
|
||||
// Create a request limiter per handler.
|
||||
http.Handle("/", tollbooth.LimitFuncHandler(tollbooth.NewLimiter(1, nil), HelloHandler))
|
||||
lmt := tollbooth.NewLimiter(1, nil)
|
||||
|
||||
// New in version >= 8, you must explicitly define how to pick the IP address.
|
||||
lmt.SetIPLookup(limiter.IPLookup{
|
||||
Name: "X-Real-IP",
|
||||
IndexFromRight: 0,
|
||||
})
|
||||
|
||||
http.Handle("/", tollbooth.LimitFuncHandler(lmt, HelloHandler))
|
||||
http.ListenAndServe(":12345", nil)
|
||||
}
|
||||
```
|
||||
@@ -54,8 +66,8 @@ func main() {
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/didip/tollbooth/v7"
|
||||
"github.com/didip/tollbooth/v7/limiter"
|
||||
"github.com/didip/tollbooth/v8"
|
||||
"github.com/didip/tollbooth/v8/limiter"
|
||||
)
|
||||
|
||||
lmt := tollbooth.NewLimiter(1, nil)
|
||||
@@ -66,10 +78,24 @@ func main() {
|
||||
// every token bucket in it will expire 1 hour after it was initially set.
|
||||
lmt = tollbooth.NewLimiter(1, &limiter.ExpirableOptions{DefaultExpirationTTL: time.Hour})
|
||||
|
||||
// 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.
|
||||
lmt.SetIPLookups([]string{"RemoteAddr", "X-Forwarded-For", "X-Real-IP"})
|
||||
// New in version >= 8, you must explicitly define how to pick the IP address.
|
||||
// If IP address cannot be found, rate limiter will not be activated.
|
||||
lmt.SetIPLookup(limiter.IPLookup{
|
||||
// The name of lookup method.
|
||||
// Possible options are: RemoteAddr, X-Forwarded-For, X-Real-IP, CF-Connecting-IP
|
||||
// All other headers are considered unknown and will be ignored.
|
||||
Name: "X-Real-IP",
|
||||
|
||||
// The index position to pick the ip address from a comma separated list.
|
||||
// The index goes from right to left.
|
||||
//
|
||||
// When there are multiple of the same headers,
|
||||
// we will concat them together in the order of first to last seen.
|
||||
// And then we pick the IP using this index position.
|
||||
IndexFromRight: 0,
|
||||
})
|
||||
|
||||
// In version >= 8, lmt.SetIPLookups and lmt.GetIPLookups are removed.
|
||||
|
||||
// Limit only GET and POST requests.
|
||||
lmt.SetMethods([]string{"GET", "POST"})
|
||||
@@ -89,8 +115,7 @@ func main() {
|
||||
lmt.RemoveHeaderEntries("X-Access-Token", []string{"limitless-token"})
|
||||
|
||||
// By the way, the setters are chainable. Example:
|
||||
lmt.SetIPLookups([]string{"RemoteAddr", "X-Forwarded-For", "X-Real-IP"}).
|
||||
SetMethods([]string{"GET", "POST"}).
|
||||
lmt.SetMethods([]string{"GET", "POST"}).
|
||||
SetBasicAuthUsers([]string{"sansa"}).
|
||||
SetBasicAuthUsers([]string{"tyrion"})
|
||||
```
|
||||
@@ -137,6 +162,12 @@ func main() {
|
||||
```go
|
||||
lmt := tollbooth.NewLimiter(1, nil)
|
||||
|
||||
// New in version >= 8, you must explicitly define how to pick the IP address.
|
||||
lmt.SetIPLookup(limiter.IPLookup{
|
||||
Name: "X-Forwarded-For",
|
||||
IndexFromRight: 0,
|
||||
})
|
||||
|
||||
// Set a custom message.
|
||||
lmt.SetMessage("You have reached maximum request limit.")
|
||||
|
||||
Generated
Vendored
Generated
Vendored
Generated
Vendored
Generated
Vendored
Generated
Vendored
Generated
Vendored
Generated
Vendored
+26
-27
@@ -5,6 +5,8 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/didip/tollbooth/v8/limiter"
|
||||
)
|
||||
|
||||
// StringInSlice finds needle in a slice of strings.
|
||||
@@ -17,38 +19,35 @@ func StringInSlice(sliceString []string, needle string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// RemoteIP finds IP Address given http.Request struct.
|
||||
func RemoteIP(ipLookups []string, forwardedForIndexFromBehind int, 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" {
|
||||
// 1. Cover the basic use cases for both ipv4 and ipv6
|
||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
// 2. Upon error, just return the remote addr.
|
||||
return r.RemoteAddr
|
||||
}
|
||||
return ip
|
||||
// RemoteIPFromIPLookup picks an ip address explicitly from limiter.IPLookup criteria.
|
||||
// This function is intended to replace RemoteIP function.
|
||||
func RemoteIPFromIPLookup(ipLookup limiter.IPLookup, r *http.Request) string {
|
||||
switch ipLookup.Name {
|
||||
case "RemoteAddr":
|
||||
// 1. Cover the basic use cases for both ipv4 and ipv6
|
||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
// 2. Upon error, just return the remote addr.
|
||||
return 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 ip
|
||||
|
||||
partIndex := len(parts) - 1 - forwardedForIndexFromBehind
|
||||
if partIndex < 0 {
|
||||
partIndex = 0
|
||||
}
|
||||
case "X-Forwarded-For", "X-Real-IP", "CF-Connecting-IP":
|
||||
ipAddrListCommaSeparated := r.Header.Values(ipLookup.Name)
|
||||
|
||||
return parts[partIndex]
|
||||
ipAddrCommaSeparated := strings.Join(ipAddrListCommaSeparated, ",")
|
||||
|
||||
ips := strings.Split(ipAddrCommaSeparated, ",")
|
||||
for i, p := range ips {
|
||||
ips[i] = strings.TrimSpace(p)
|
||||
}
|
||||
if lookup == "X-Real-IP" && realIP != "" {
|
||||
return realIP
|
||||
|
||||
ipIndex := len(ips) - 1 - ipLookup.IndexFromRight
|
||||
if ipIndex < 0 {
|
||||
ipIndex = 0
|
||||
}
|
||||
|
||||
return ips[ipIndex]
|
||||
}
|
||||
|
||||
return ""
|
||||
Generated
Vendored
+27
-13
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
cache "github.com/go-pkgz/expirable-cache/v3"
|
||||
|
||||
"github.com/didip/tollbooth/v7/internal/time/rate"
|
||||
"github.com/didip/tollbooth/v8/internal/time/rate"
|
||||
)
|
||||
|
||||
// New is a constructor for Limiter.
|
||||
@@ -19,7 +19,6 @@ func New(generalExpirableOptions *ExpirableOptions) *Limiter {
|
||||
SetMessage("You have reached maximum request limit.").
|
||||
SetStatusCode(429).
|
||||
SetOnLimitReached(nil).
|
||||
SetIPLookups([]string{"RemoteAddr", "X-Forwarded-For", "X-Real-IP"}).
|
||||
SetForwardedForIndexFromBehind(0).
|
||||
SetHeaders(make(map[string][]string)).
|
||||
SetContextValues(make(map[string][]string)).
|
||||
@@ -43,6 +42,18 @@ func New(generalExpirableOptions *ExpirableOptions) *Limiter {
|
||||
return lmt
|
||||
}
|
||||
|
||||
// IPLookup is a config struct to define how users want to pick the remote IP address.
|
||||
type IPLookup struct {
|
||||
// The name of lookup method.
|
||||
// Possible options are: RemoteAddr, X-Forwarded-For, X-Real-IP, CF-Connecting-IP
|
||||
// All other headers are considered unknown and will be ignored.
|
||||
Name string
|
||||
|
||||
// The index position to pick the ip address from a comma separated list.
|
||||
// The index goes from right to left.
|
||||
IndexFromRight int
|
||||
}
|
||||
|
||||
// Limiter is a config struct to limit a particular request handler.
|
||||
type Limiter struct {
|
||||
// Maximum number of requests to limit per second.
|
||||
@@ -66,10 +77,9 @@ type Limiter struct {
|
||||
// An option to write back what you want upon reaching a limit.
|
||||
overrideDefaultResponseWriter bool
|
||||
|
||||
// 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
|
||||
// Explicitly define how to look up IP address.
|
||||
// This is intended to replace ipLookups
|
||||
explicitIPLookup IPLookup
|
||||
|
||||
forwardedForIndex int
|
||||
|
||||
@@ -270,10 +280,12 @@ func (l *Limiter) ExecOnLimitReached(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// SetOverrideDefaultResponseWriter is a thread-safe way of setting the response writer override variable.
|
||||
func (l *Limiter) SetOverrideDefaultResponseWriter(override bool) {
|
||||
func (l *Limiter) SetOverrideDefaultResponseWriter(override bool) *Limiter {
|
||||
l.Lock()
|
||||
l.overrideDefaultResponseWriter = override
|
||||
l.Unlock()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// GetOverrideDefaultResponseWriter is a thread-safe way of getting the response writer override variable.
|
||||
@@ -283,20 +295,22 @@ func (l *Limiter) GetOverrideDefaultResponseWriter() bool {
|
||||
return l.overrideDefaultResponseWriter
|
||||
}
|
||||
|
||||
// SetIPLookups is thread-safe way of setting list of places to look up IP address.
|
||||
func (l *Limiter) SetIPLookups(ipLookups []string) *Limiter {
|
||||
// SetIPLookup is thread-safe way of setting an explicit way to look up IP address.
|
||||
// This method is intended to replace SetIPLookups (version 6 or older).
|
||||
func (l *Limiter) SetIPLookup(lookup IPLookup) *Limiter {
|
||||
l.Lock()
|
||||
l.ipLookups = ipLookups
|
||||
l.explicitIPLookup = lookup
|
||||
l.Unlock()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// GetIPLookups is thread-safe way of getting list of places to look up IP address.
|
||||
func (l *Limiter) GetIPLookups() []string {
|
||||
// GetIPLookup is thread-safe way of getting an explicit way to look up IP address.
|
||||
// This method is intended to replace the old GetIPLookups (version 6 or older).
|
||||
func (l *Limiter) GetIPLookup() IPLookup {
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
return l.ipLookups
|
||||
return l.explicitIPLookup
|
||||
}
|
||||
|
||||
// SetIgnoreURL is thread-safe way of setting whenever ignore the URL on rate limit keys
|
||||
Generated
Vendored
Generated
Vendored
+33
-7
@@ -7,9 +7,9 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/didip/tollbooth/v7/errors"
|
||||
"github.com/didip/tollbooth/v7/libstring"
|
||||
"github.com/didip/tollbooth/v7/limiter"
|
||||
"github.com/didip/tollbooth/v8/errors"
|
||||
"github.com/didip/tollbooth/v8/libstring"
|
||||
"github.com/didip/tollbooth/v8/limiter"
|
||||
)
|
||||
|
||||
// setResponseHeaders configures X-Rate-Limit-Limit and X-Rate-Limit-Duration
|
||||
@@ -37,8 +37,7 @@ func setRateLimitResponseHeaders(lmt *limiter.Limiter, w http.ResponseWriter, to
|
||||
func NewLimiter(max float64, tbOptions *limiter.ExpirableOptions) *limiter.Limiter {
|
||||
return limiter.New(tbOptions).
|
||||
SetMax(max).
|
||||
SetBurst(int(math.Max(1, max))).
|
||||
SetIPLookups([]string{"X-Forwarded-For", "X-Real-IP", "RemoteAddr"})
|
||||
SetBurst(int(math.Max(1, max)))
|
||||
}
|
||||
|
||||
// LimitByKeys keeps track number of request made by keys separated by pipe.
|
||||
@@ -63,7 +62,7 @@ func ShouldSkipLimiter(lmt *limiter.Limiter, r *http.Request) bool {
|
||||
// ---------------------------------
|
||||
// Filter by remote ip
|
||||
// If we are unable to find remoteIP, skip limiter
|
||||
remoteIP := libstring.RemoteIP(lmt.GetIPLookups(), lmt.GetForwardedForIndexFromBehind(), r)
|
||||
remoteIP := libstring.RemoteIPFromIPLookup(lmt.GetIPLookup(), r)
|
||||
remoteIP = libstring.CanonicalizeIP(remoteIP)
|
||||
if remoteIP == "" {
|
||||
return true
|
||||
@@ -195,7 +194,7 @@ func ShouldSkipLimiter(lmt *limiter.Limiter, r *http.Request) bool {
|
||||
|
||||
// BuildKeys generates a slice of keys to rate-limit by given limiter and request structs.
|
||||
func BuildKeys(lmt *limiter.Limiter, r *http.Request) [][]string {
|
||||
remoteIP := libstring.RemoteIP(lmt.GetIPLookups(), lmt.GetForwardedForIndexFromBehind(), r)
|
||||
remoteIP := libstring.RemoteIPFromIPLookup(lmt.GetIPLookup(), r)
|
||||
remoteIP = libstring.CanonicalizeIP(remoteIP)
|
||||
path := r.URL.Path
|
||||
sliceKeys := make([][]string, 0)
|
||||
@@ -347,3 +346,30 @@ func LimitHandler(lmt *limiter.Limiter, next http.Handler) http.Handler {
|
||||
func LimitFuncHandler(lmt *limiter.Limiter, nextFunc func(http.ResponseWriter, *http.Request)) http.Handler {
|
||||
return LimitHandler(lmt, http.HandlerFunc(nextFunc))
|
||||
}
|
||||
|
||||
// HTTPMiddleware wraps http.Handler with tollbooth limiter
|
||||
func HTTPMiddleware(lmt *limiter.Limiter) func(http.Handler) http.Handler {
|
||||
// // set IP lookup only if not set
|
||||
if lmt.GetIPLookup().Name == "" {
|
||||
lmt.SetIPLookup(limiter.IPLookup{Name: "RemoteAddr"})
|
||||
}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
http.Error(w, "Context was canceled", http.StatusServiceUnavailable)
|
||||
return
|
||||
default:
|
||||
if httpError := LimitByRequest(lmt, w, r); httpError != nil {
|
||||
lmt.ExecOnLimitReached(w, r)
|
||||
w.Header().Add("Content-Type", lmt.GetMessageContentType())
|
||||
w.WriteHeader(httpError.StatusCode)
|
||||
w.Write([]byte(httpError.Message)) //nolint:gosec // not much we can do here with failed write
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
## tollbooth_chi
|
||||
|
||||
[Chi](https://github.com/pressly/chi) middleware for rate limiting HTTP requests.
|
||||
|
||||
|
||||
## Five Minutes Tutorial
|
||||
|
||||
```
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/didip/tollbooth"
|
||||
"github.com/didip/tollbooth_chi"
|
||||
"github.com/pressly/chi"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create a limiter struct.
|
||||
limiter := tollbooth.NewLimiter(1, nil)
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(tollbooth_chi.LimitHandler(limiter))
|
||||
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("Hello, world!"))
|
||||
})
|
||||
|
||||
http.ListenAndServe(":12345", r)
|
||||
}
|
||||
```
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
package tollbooth_chi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/didip/tollbooth/v7"
|
||||
"github.com/didip/tollbooth/v7/limiter"
|
||||
)
|
||||
|
||||
func LimitHandler(lmt *limiter.Limiter) func(http.Handler) http.Handler {
|
||||
return func(handler http.Handler) http.Handler {
|
||||
wrapper := &limiterWrapper{
|
||||
lmt: lmt,
|
||||
}
|
||||
|
||||
wrapper.handler = handler
|
||||
return wrapper
|
||||
}
|
||||
}
|
||||
|
||||
type limiterWrapper struct {
|
||||
lmt *limiter.Limiter
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func (l *limiterWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
http.Error(w, "Context was canceled", http.StatusServiceUnavailable)
|
||||
return
|
||||
|
||||
default:
|
||||
httpError := tollbooth.LimitByRequest(l.lmt, w, r)
|
||||
if httpError != nil {
|
||||
l.lmt.ExecOnLimitReached(w, r)
|
||||
w.Header().Add("Content-Type", l.lmt.GetMessageContentType())
|
||||
w.WriteHeader(httpError.StatusCode)
|
||||
w.Write([]byte(httpError.Message))
|
||||
return
|
||||
}
|
||||
|
||||
l.handler.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user