Bump the go-modules-updates group in /backend with 8 updates
Bumps the go-modules-updates group in /backend with 8 updates: | Package | From | To | | --- | --- | --- | | [github.com/PuerkitoBio/goquery](https://github.com/PuerkitoBio/goquery) | `1.10.1` | `1.10.2` | | [github.com/alecthomas/chroma/v2](https://github.com/alecthomas/chroma) | `2.14.0` | `2.15.0` | | [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) | `5.2.0` | `5.2.1` | | [github.com/go-pkgz/jrpc](https://github.com/go-pkgz/jrpc) | `0.3.0` | `0.3.1` | | [go.etcd.io/bbolt](https://github.com/etcd-io/bbolt) | `1.3.11` | `1.4.0` | | [golang.org/x/crypto](https://github.com/golang/crypto) | `0.31.0` | `0.33.0` | | [golang.org/x/image](https://github.com/golang/image) | `0.23.0` | `0.25.0` | | [golang.org/x/net](https://github.com/golang/net) | `0.33.0` | `0.35.0` | Updates `github.com/PuerkitoBio/goquery` from 1.10.1 to 1.10.2 - [Release notes](https://github.com/PuerkitoBio/goquery/releases) - [Commits](https://github.com/PuerkitoBio/goquery/compare/v1.10.1...v1.10.2) Updates `github.com/alecthomas/chroma/v2` from 2.14.0 to 2.15.0 - [Release notes](https://github.com/alecthomas/chroma/releases) - [Changelog](https://github.com/alecthomas/chroma/blob/master/.goreleaser.yml) - [Commits](https://github.com/alecthomas/chroma/compare/v2.14.0...v2.15.0) Updates `github.com/go-chi/chi/v5` from 5.2.0 to 5.2.1 - [Release notes](https://github.com/go-chi/chi/releases) - [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md) - [Commits](https://github.com/go-chi/chi/compare/v5.2.0...v5.2.1) Updates `github.com/go-pkgz/jrpc` from 0.3.0 to 0.3.1 - [Release notes](https://github.com/go-pkgz/jrpc/releases) - [Commits](https://github.com/go-pkgz/jrpc/compare/v0.3.0...v0.3.1) Updates `go.etcd.io/bbolt` from 1.3.11 to 1.4.0 - [Release notes](https://github.com/etcd-io/bbolt/releases) - [Commits](https://github.com/etcd-io/bbolt/compare/v1.3.11...v1.4.0) Updates `golang.org/x/crypto` from 0.31.0 to 0.33.0 - [Commits](https://github.com/golang/crypto/compare/v0.31.0...v0.33.0) Updates `golang.org/x/image` from 0.23.0 to 0.25.0 - [Commits](https://github.com/golang/image/compare/v0.23.0...v0.25.0) Updates `golang.org/x/net` from 0.33.0 to 0.35.0 - [Commits](https://github.com/golang/net/compare/v0.33.0...v0.35.0) --- updated-dependencies: - dependency-name: github.com/PuerkitoBio/goquery dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-modules-updates - dependency-name: github.com/alecthomas/chroma/v2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-modules-updates - dependency-name: github.com/go-chi/chi/v5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-modules-updates - dependency-name: github.com/go-pkgz/jrpc dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-modules-updates - dependency-name: go.etcd.io/bbolt dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-modules-updates - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-modules-updates - dependency-name: golang.org/x/image dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-modules-updates - dependency-name: golang.org/x/net dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-modules-updates ... Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
committed by
Dmitry V
parent
e6afc58b34
commit
89221ff2bc
@@ -0,0 +1,3 @@
|
||||
/debug
|
||||
/.vscode
|
||||
/.idea
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
linters:
|
||||
enable:
|
||||
- revive
|
||||
- govet
|
||||
- unconvert
|
||||
- megacheck
|
||||
- gas
|
||||
- gocyclo
|
||||
- dupl
|
||||
- misspell
|
||||
- unparam
|
||||
- unused
|
||||
- typecheck
|
||||
- ineffassign
|
||||
- stylecheck
|
||||
- gochecknoinits
|
||||
- exportloopref
|
||||
- gocritic
|
||||
- nakedret
|
||||
- gosimple
|
||||
- prealloc
|
||||
fast: false
|
||||
disable-all: true
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- dupl
|
||||
- text: "Errors unhandled"
|
||||
linters:
|
||||
- gosec
|
||||
exclude-use-default: false
|
||||
+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.
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
[](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 1:** This library is considered finished.
|
||||
|
||||
**NOTE 2:** Major version changes are backward-incompatible. `v2.0.0` streamlines the ugliness of the old API.
|
||||
|
||||
## Versions
|
||||
|
||||
**v1.0.0:** This version maintains the old API but all the thirdparty modules are moved to their own repo.
|
||||
|
||||
**v2.x.x:** Brand-new API for the sake of code cleanup, thread safety, & auto-expiring data structures.
|
||||
|
||||
**v3.x.x:** Apparently we have been using golang.org/x/time/rate incorrectly. See issue #48. It always limits X number per 1 second. The time duration is not changeable, so it does not make sense to pass TTL to tollbooth.
|
||||
|
||||
**v4.x.x:** Float64 for max requests per second
|
||||
|
||||
**v5.x.x:** go.mod and go.sum
|
||||
|
||||
**v6.x.x:** Replaced `go-cache` with `github.com/go-pkgz/expirable-cache` because `go-cache` leaks goroutines.
|
||||
|
||||
**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
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/didip/tollbooth/v8"
|
||||
"github.com/didip/tollbooth/v8/limiter"
|
||||
)
|
||||
|
||||
func HelloHandler(w http.ResponseWriter, req *http.Request) {
|
||||
w.Write([]byte("Hello, World!"))
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Create a request limiter per handler.
|
||||
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)
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
1. Rate-limit by request's remote IP, path, methods, custom headers, & basic auth usernames.
|
||||
```go
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/didip/tollbooth/v8"
|
||||
"github.com/didip/tollbooth/v8/limiter"
|
||||
)
|
||||
|
||||
lmt := tollbooth.NewLimiter(1, nil)
|
||||
|
||||
// or create a limiter with expirable token buckets
|
||||
// This setting means:
|
||||
// create a 1 request/second limiter and
|
||||
// every token bucket in it will expire 1 hour after it was initially set.
|
||||
lmt = tollbooth.NewLimiter(1, &limiter.ExpirableOptions{DefaultExpirationTTL: time.Hour})
|
||||
|
||||
// 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"})
|
||||
|
||||
// Limit based on basic auth usernames.
|
||||
// You add them on-load, or later as you handle requests.
|
||||
lmt.SetBasicAuthUsers([]string{"bob", "jane", "didip", "vip"})
|
||||
// You can remove them later as well.
|
||||
lmt.RemoveBasicAuthUsers([]string{"vip"})
|
||||
|
||||
// Limit request headers containing certain values.
|
||||
// You add them on-load, or later as you handle requests.
|
||||
lmt.SetHeader("X-Access-Token", []string{"abc123", "xyz098"})
|
||||
// You can remove all entries at once.
|
||||
lmt.RemoveHeader("X-Access-Token")
|
||||
// Or remove specific ones.
|
||||
lmt.RemoveHeaderEntries("X-Access-Token", []string{"limitless-token"})
|
||||
|
||||
// By the way, the setters are chainable. Example:
|
||||
lmt.SetMethods([]string{"GET", "POST"}).
|
||||
SetBasicAuthUsers([]string{"sansa"}).
|
||||
SetBasicAuthUsers([]string{"tyrion"})
|
||||
```
|
||||
|
||||
2. Compose your own middleware by using `LimitByKeys()`.
|
||||
|
||||
3. Header entries and basic auth users can expire over time (to conserve memory).
|
||||
|
||||
```go
|
||||
import "time"
|
||||
|
||||
lmt := tollbooth.NewLimiter(1, nil)
|
||||
|
||||
// Set a custom expiration TTL for token bucket.
|
||||
lmt.SetTokenBucketExpirationTTL(time.Hour)
|
||||
|
||||
// Set a custom expiration TTL for basic auth users.
|
||||
lmt.SetBasicAuthExpirationTTL(time.Hour)
|
||||
|
||||
// Set a custom expiration TTL for header entries.
|
||||
lmt.SetHeaderEntryExpirationTTL(time.Hour)
|
||||
```
|
||||
|
||||
4. Upon rejection, the following HTTP response headers are available to users:
|
||||
|
||||
* `X-Rate-Limit-Limit` The maximum request limit.
|
||||
|
||||
* `X-Rate-Limit-Duration` The rate-limiter duration.
|
||||
|
||||
* `X-Rate-Limit-Request-Forwarded-For` The rejected request `X-Forwarded-For`.
|
||||
|
||||
* `X-Rate-Limit-Request-Remote-Addr` The rejected request `RemoteAddr`.
|
||||
|
||||
Upon both success and rejection [RateLimit](https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-ratelimit-headers) headers are sent:
|
||||
|
||||
* `RateLimit-Limit` The maximum request limit within the time window (1s).
|
||||
|
||||
* `RateLimit-Reset` The rate-limiter time window duration in seconds (always 1s).
|
||||
|
||||
* `RateLimit-Remaining` The remaining tokens.
|
||||
|
||||
5. Customize your own message or function when limit is reached.
|
||||
|
||||
```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.")
|
||||
|
||||
// Set a custom content-type.
|
||||
lmt.SetMessageContentType("text/plain; charset=utf-8")
|
||||
|
||||
// Set a custom function for rejection.
|
||||
lmt.SetOnLimitReached(func(w http.ResponseWriter, r *http.Request) { fmt.Println("A request was rejected") })
|
||||
```
|
||||
|
||||
6. Tollbooth does not require external storage since it uses an algorithm called [Token Bucket](http://en.wikipedia.org/wiki/Token_bucket) [(Go library: golang.org/x/time/rate)](https://godoc.org/golang.org/x/time/rate).
|
||||
|
||||
## Other Web Frameworks
|
||||
|
||||
Sometimes, other frameworks require a little bit of shim to use Tollbooth. These shims below are contributed by the community, so I make no promises on how well they work. The one I am familiar with are: Chi, Gin, and Negroni.
|
||||
|
||||
* [Chi](https://github.com/didip/tollbooth_chi)
|
||||
|
||||
* [Echo](https://github.com/didip/tollbooth_echo)
|
||||
|
||||
* [FastHTTP](https://github.com/didip/tollbooth_fasthttp)
|
||||
|
||||
* [Gin](https://github.com/didip/tollbooth_gin)
|
||||
|
||||
* [GoRestful](https://github.com/didip/tollbooth_gorestful)
|
||||
|
||||
* [HTTPRouter](https://github.com/didip/tollbooth_httprouter)
|
||||
|
||||
* [Iris](https://github.com/didip/tollbooth_iris)
|
||||
|
||||
* [Negroni](https://github.com/didip/tollbooth_negroni)
|
||||
|
||||
## My other Go libraries
|
||||
|
||||
* [ErrStack](https://github.com/didip/errstack): A small library to combine errors and also display filename and line number.
|
||||
|
||||
* [Stopwatch](https://github.com/didip/stopwatch): A small library to measure latency of things. Useful if you want to report latency data to Graphite.
|
||||
|
||||
* [LaborUnion](https://github.com/didip/laborunion): A dynamic worker pool library.
|
||||
|
||||
* [Gomet](https://github.com/didip/gomet): Simple HTTP client & server long poll library for Go. Useful for receiving live updates without needing Websocket.
|
||||
|
||||
## Contributions
|
||||
|
||||
Before sending a PR with code changes, please make sure altered code is covered with tests which are passing, and that golangci-lint shows no errors.
|
||||
|
||||
To check the linter output, [install it](https://golangci-lint.run/usage/install/#local-installation) and then run `golangci-lint run` in the root directory of the repository.
|
||||
+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)
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
# This source code refers to The Go Authors for copyright purposes.
|
||||
# The master list of authors is in the main Go distribution,
|
||||
# visible at http://tip.golang.org/AUTHORS.
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
# This source code was written by the Go contributors.
|
||||
# The master list of contributors is in the main Go distribution,
|
||||
# visible at http://tip.golang.org/CONTRIBUTORS.
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
Additional IP Rights Grant (Patents)
|
||||
|
||||
"This implementation" means the copyrightable works distributed by
|
||||
Google as part of the Go project.
|
||||
|
||||
Google hereby grants to You a perpetual, worldwide, non-exclusive,
|
||||
no-charge, royalty-free, irrevocable (except as stated in this section)
|
||||
patent license to make, have made, use, offer to sell, sell, import,
|
||||
transfer and otherwise run, modify and propagate the contents of this
|
||||
implementation of Go, where such license applies only to those patent
|
||||
claims, both currently owned or controlled by Google and acquired in
|
||||
the future, licensable by Google that are necessarily infringed by this
|
||||
implementation of Go. This grant does not include claims that would be
|
||||
infringed only as a consequence of further modification of this
|
||||
implementation. If you or your agent or exclusive licensee institute or
|
||||
order or agree to the institution of patent litigation against any
|
||||
entity (including a cross-claim or counterclaim in a lawsuit) alleging
|
||||
that this implementation of Go or any code incorporated within this
|
||||
implementation of Go constitutes direct or contributory patent
|
||||
infringement, or inducement of patent infringement, then any patent
|
||||
rights granted to you under this License for this implementation of Go
|
||||
shall terminate as of the date such litigation is filed.
|
||||
+396
@@ -0,0 +1,396 @@
|
||||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package rate provides a rate limiter.
|
||||
package rate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Limit defines the maximum frequency of some events.
|
||||
// Limit is represented as number of events per second.
|
||||
// A zero Limit allows no events.
|
||||
type Limit float64
|
||||
|
||||
// Inf is the infinite rate limit; it allows all events (even if burst is zero).
|
||||
const Inf = Limit(math.MaxFloat64)
|
||||
|
||||
// Every converts a minimum time interval between events to a Limit.
|
||||
func Every(interval time.Duration) Limit {
|
||||
if interval <= 0 {
|
||||
return Inf
|
||||
}
|
||||
return 1 / Limit(interval.Seconds())
|
||||
}
|
||||
|
||||
// A Limiter controls how frequently events are allowed to happen.
|
||||
// It implements a "token bucket" of size b, initially full and refilled
|
||||
// at rate r tokens per second.
|
||||
// Informally, in any large enough time interval, the Limiter limits the
|
||||
// rate to r tokens per second, with a maximum burst size of b events.
|
||||
// As a special case, if r == Inf (the infinite rate), b is ignored.
|
||||
// See https://en.wikipedia.org/wiki/Token_bucket for more about token buckets.
|
||||
//
|
||||
// The zero value is a valid Limiter, but it will reject all events.
|
||||
// Use NewLimiter to create non-zero Limiters.
|
||||
//
|
||||
// Limiter has three main methods, Allow, Reserve, and Wait.
|
||||
// Most callers should use Wait.
|
||||
//
|
||||
// Each of the three methods consumes a single token.
|
||||
// They differ in their behavior when no token is available.
|
||||
// If no token is available, Allow returns false.
|
||||
// If no token is available, Reserve returns a reservation for a future token
|
||||
// and the amount of time the caller must wait before using it.
|
||||
// If no token is available, Wait blocks until one can be obtained
|
||||
// or its associated context.Context is canceled.
|
||||
//
|
||||
// The methods AllowN, ReserveN, and WaitN consume n tokens.
|
||||
type Limiter struct {
|
||||
mu sync.Mutex
|
||||
limit Limit
|
||||
burst int
|
||||
tokens float64
|
||||
// last is the last time the limiter's tokens field was updated
|
||||
last time.Time
|
||||
// lastEvent is the latest time of a rate-limited event (past or future)
|
||||
lastEvent time.Time
|
||||
}
|
||||
|
||||
// Limit returns the maximum overall event rate.
|
||||
func (lim *Limiter) Limit() Limit {
|
||||
lim.mu.Lock()
|
||||
defer lim.mu.Unlock()
|
||||
return lim.limit
|
||||
}
|
||||
|
||||
// Burst returns the maximum burst size. Burst is the maximum number of tokens
|
||||
// that can be consumed in a single call to Allow, Reserve, or Wait, so higher
|
||||
// Burst values allow more events to happen at once.
|
||||
// A zero Burst allows no events, unless limit == Inf.
|
||||
func (lim *Limiter) Burst() int {
|
||||
lim.mu.Lock()
|
||||
defer lim.mu.Unlock()
|
||||
return lim.burst
|
||||
}
|
||||
|
||||
// NewLimiter returns a new Limiter that allows events up to rate r and permits
|
||||
// bursts of at most b tokens.
|
||||
func NewLimiter(r Limit, b int) *Limiter {
|
||||
return &Limiter{
|
||||
limit: r,
|
||||
burst: b,
|
||||
}
|
||||
}
|
||||
|
||||
// Allow is shorthand for AllowN(time.Now(), 1).
|
||||
func (lim *Limiter) Allow() bool {
|
||||
return lim.AllowN(time.Now(), 1)
|
||||
}
|
||||
|
||||
// TokensAt returns the number of tokens available for the given time.
|
||||
func (lim *Limiter) TokensAt(t time.Time) float64 {
|
||||
lim.mu.Lock()
|
||||
_, _, tokens := lim.advance(t) // does not mutate lim
|
||||
lim.mu.Unlock()
|
||||
return tokens
|
||||
}
|
||||
|
||||
// AllowN reports whether n events may happen at time now.
|
||||
// Use this method if you intend to drop / skip events that exceed the rate limit.
|
||||
// Otherwise use Reserve or Wait.
|
||||
func (lim *Limiter) AllowN(now time.Time, n int) bool {
|
||||
return lim.reserveN(now, n, 0).ok
|
||||
}
|
||||
|
||||
// A Reservation holds information about events that are permitted by a Limiter to happen after a delay.
|
||||
// A Reservation may be canceled, which may enable the Limiter to permit additional events.
|
||||
type Reservation struct {
|
||||
ok bool
|
||||
lim *Limiter
|
||||
tokens int
|
||||
timeToAct time.Time
|
||||
// This is the Limit at reservation time, it can change later.
|
||||
limit Limit
|
||||
}
|
||||
|
||||
// OK returns whether the limiter can provide the requested number of tokens
|
||||
// within the maximum wait time. If OK is false, Delay returns InfDuration, and
|
||||
// Cancel does nothing.
|
||||
func (r *Reservation) OK() bool {
|
||||
return r.ok
|
||||
}
|
||||
|
||||
// Delay is shorthand for DelayFrom(time.Now()).
|
||||
func (r *Reservation) Delay() time.Duration {
|
||||
return r.DelayFrom(time.Now())
|
||||
}
|
||||
|
||||
// InfDuration is the duration returned by Delay when a Reservation is not OK.
|
||||
const InfDuration = time.Duration(1<<63 - 1)
|
||||
|
||||
// DelayFrom returns the duration for which the reservation holder must wait
|
||||
// before taking the reserved action. Zero duration means act immediately.
|
||||
// InfDuration means the limiter cannot grant the tokens requested in this
|
||||
// Reservation within the maximum wait time.
|
||||
func (r *Reservation) DelayFrom(now time.Time) time.Duration {
|
||||
if !r.ok {
|
||||
return InfDuration
|
||||
}
|
||||
delay := r.timeToAct.Sub(now)
|
||||
if delay < 0 {
|
||||
return 0
|
||||
}
|
||||
return delay
|
||||
}
|
||||
|
||||
// Cancel is shorthand for CancelAt(time.Now()).
|
||||
func (r *Reservation) Cancel() {
|
||||
r.CancelAt(time.Now())
|
||||
}
|
||||
|
||||
// CancelAt indicates that the reservation holder will not perform the reserved action
|
||||
// and reverses the effects of this Reservation on the rate limit as much as possible,
|
||||
// considering that other reservations may have already been made.
|
||||
func (r *Reservation) CancelAt(now time.Time) {
|
||||
if !r.ok {
|
||||
return
|
||||
}
|
||||
|
||||
r.lim.mu.Lock()
|
||||
defer r.lim.mu.Unlock()
|
||||
|
||||
if r.lim.limit == Inf || r.tokens == 0 || r.timeToAct.Before(now) {
|
||||
return
|
||||
}
|
||||
|
||||
// calculate tokens to restore
|
||||
// The duration between lim.lastEvent and r.timeToAct tells us how many tokens were reserved
|
||||
// after r was obtained. These tokens should not be restored.
|
||||
restoreTokens := float64(r.tokens) - r.limit.tokensFromDuration(r.lim.lastEvent.Sub(r.timeToAct))
|
||||
if restoreTokens <= 0 {
|
||||
return
|
||||
}
|
||||
// advance time to now
|
||||
now, _, tokens := r.lim.advance(now)
|
||||
// calculate new number of tokens
|
||||
tokens += restoreTokens
|
||||
if burst := float64(r.lim.burst); tokens > burst {
|
||||
tokens = burst
|
||||
}
|
||||
// update state
|
||||
r.lim.last = now
|
||||
r.lim.tokens = tokens
|
||||
if r.timeToAct == r.lim.lastEvent {
|
||||
prevEvent := r.timeToAct.Add(r.limit.durationFromTokens(float64(-r.tokens)))
|
||||
if !prevEvent.Before(now) {
|
||||
r.lim.lastEvent = prevEvent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reserve is shorthand for ReserveN(time.Now(), 1).
|
||||
func (lim *Limiter) Reserve() *Reservation {
|
||||
return lim.ReserveN(time.Now(), 1)
|
||||
}
|
||||
|
||||
// ReserveN returns a Reservation that indicates how long the caller must wait before n events happen.
|
||||
// The Limiter takes this Reservation into account when allowing future events.
|
||||
// The returned Reservation’s OK() method returns false if n exceeds the Limiter's burst size.
|
||||
// Usage example:
|
||||
// r := lim.ReserveN(time.Now(), 1)
|
||||
// if !r.OK() {
|
||||
// // Not allowed to act! Did you remember to set lim.burst to be > 0 ?
|
||||
// return
|
||||
// }
|
||||
// time.Sleep(r.Delay())
|
||||
// Act()
|
||||
// Use this method if you wish to wait and slow down in accordance with the rate limit without dropping events.
|
||||
// If you need to respect a deadline or cancel the delay, use Wait instead.
|
||||
// To drop or skip events exceeding rate limit, use Allow instead.
|
||||
func (lim *Limiter) ReserveN(now time.Time, n int) *Reservation {
|
||||
r := lim.reserveN(now, n, InfDuration)
|
||||
return &r
|
||||
}
|
||||
|
||||
// Wait is shorthand for WaitN(ctx, 1).
|
||||
func (lim *Limiter) Wait(ctx context.Context) (err error) {
|
||||
return lim.WaitN(ctx, 1)
|
||||
}
|
||||
|
||||
// WaitN blocks until lim permits n events to happen.
|
||||
// It returns an error if n exceeds the Limiter's burst size, the Context is
|
||||
// canceled, or the expected wait time exceeds the Context's Deadline.
|
||||
// The burst limit is ignored if the rate limit is Inf.
|
||||
func (lim *Limiter) WaitN(ctx context.Context, n int) (err error) {
|
||||
lim.mu.Lock()
|
||||
burst := lim.burst
|
||||
limit := lim.limit
|
||||
lim.mu.Unlock()
|
||||
|
||||
if n > burst && limit != Inf {
|
||||
return fmt.Errorf("rate: Wait(n=%d) exceeds limiter's burst %d", n, burst)
|
||||
}
|
||||
// Check if ctx is already cancelled
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
// Determine wait limit
|
||||
now := time.Now()
|
||||
waitLimit := InfDuration
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
waitLimit = deadline.Sub(now)
|
||||
}
|
||||
// Reserve
|
||||
r := lim.reserveN(now, n, waitLimit)
|
||||
if !r.ok {
|
||||
return fmt.Errorf("rate: Wait(n=%d) would exceed context deadline", n)
|
||||
}
|
||||
// Wait if necessary
|
||||
delay := r.DelayFrom(now)
|
||||
if delay == 0 {
|
||||
return nil
|
||||
}
|
||||
t := time.NewTimer(delay)
|
||||
defer t.Stop()
|
||||
select {
|
||||
case <-t.C:
|
||||
// We can proceed.
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
// Context was canceled before we could proceed. Cancel the
|
||||
// reservation, which may permit other events to proceed sooner.
|
||||
r.Cancel()
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// SetLimit is shorthand for SetLimitAt(time.Now(), newLimit).
|
||||
func (lim *Limiter) SetLimit(newLimit Limit) {
|
||||
lim.SetLimitAt(time.Now(), newLimit)
|
||||
}
|
||||
|
||||
// SetLimitAt sets a new Limit for the limiter. The new Limit, and Burst, may be violated
|
||||
// or underutilized by those which reserved (using Reserve or Wait) but did not yet act
|
||||
// before SetLimitAt was called.
|
||||
func (lim *Limiter) SetLimitAt(now time.Time, newLimit Limit) {
|
||||
lim.mu.Lock()
|
||||
defer lim.mu.Unlock()
|
||||
|
||||
now, _, tokens := lim.advance(now)
|
||||
|
||||
lim.last = now
|
||||
lim.tokens = tokens
|
||||
lim.limit = newLimit
|
||||
}
|
||||
|
||||
// SetBurst is shorthand for SetBurstAt(time.Now(), newBurst).
|
||||
func (lim *Limiter) SetBurst(newBurst int) {
|
||||
lim.SetBurstAt(time.Now(), newBurst)
|
||||
}
|
||||
|
||||
// SetBurstAt sets a new burst size for the limiter.
|
||||
func (lim *Limiter) SetBurstAt(now time.Time, newBurst int) {
|
||||
lim.mu.Lock()
|
||||
defer lim.mu.Unlock()
|
||||
|
||||
now, _, tokens := lim.advance(now)
|
||||
|
||||
lim.last = now
|
||||
lim.tokens = tokens
|
||||
lim.burst = newBurst
|
||||
}
|
||||
|
||||
// reserveN is a helper method for AllowN, ReserveN, and WaitN.
|
||||
// maxFutureReserve specifies the maximum reservation wait duration allowed.
|
||||
// reserveN returns Reservation, not *Reservation, to avoid allocation in AllowN and WaitN.
|
||||
func (lim *Limiter) reserveN(now time.Time, n int, maxFutureReserve time.Duration) Reservation {
|
||||
lim.mu.Lock()
|
||||
|
||||
if lim.limit == Inf {
|
||||
lim.mu.Unlock()
|
||||
return Reservation{
|
||||
ok: true,
|
||||
lim: lim,
|
||||
tokens: n,
|
||||
timeToAct: now,
|
||||
}
|
||||
}
|
||||
|
||||
now, last, tokens := lim.advance(now)
|
||||
|
||||
// Calculate the remaining number of tokens resulting from the request.
|
||||
tokens -= float64(n)
|
||||
|
||||
// Calculate the wait duration
|
||||
var waitDuration time.Duration
|
||||
if tokens < 0 {
|
||||
waitDuration = lim.limit.durationFromTokens(-tokens)
|
||||
}
|
||||
|
||||
// Decide result
|
||||
ok := n <= lim.burst && waitDuration <= maxFutureReserve
|
||||
|
||||
// Prepare reservation
|
||||
r := Reservation{
|
||||
ok: ok,
|
||||
lim: lim,
|
||||
limit: lim.limit,
|
||||
}
|
||||
if ok {
|
||||
r.tokens = n
|
||||
r.timeToAct = now.Add(waitDuration)
|
||||
}
|
||||
|
||||
// Update state
|
||||
if ok {
|
||||
lim.last = now
|
||||
lim.tokens = tokens
|
||||
lim.lastEvent = r.timeToAct
|
||||
} else {
|
||||
lim.last = last
|
||||
}
|
||||
|
||||
lim.mu.Unlock()
|
||||
return r
|
||||
}
|
||||
|
||||
// advance calculates and returns an updated state for lim resulting from the passage of time.
|
||||
// lim is not changed.
|
||||
// advance requires that lim.mu is held.
|
||||
func (lim *Limiter) advance(now time.Time) (newNow time.Time, newLast time.Time, newTokens float64) {
|
||||
last := lim.last
|
||||
if now.Before(last) {
|
||||
last = now
|
||||
}
|
||||
|
||||
// Calculate the new number of tokens, due to time that passed.
|
||||
elapsed := now.Sub(last)
|
||||
delta := lim.limit.tokensFromDuration(elapsed)
|
||||
tokens := lim.tokens + delta
|
||||
if burst := float64(lim.burst); tokens > burst {
|
||||
tokens = burst
|
||||
}
|
||||
return now, last, tokens
|
||||
}
|
||||
|
||||
// durationFromTokens is a unit conversion function from the number of tokens to the duration
|
||||
// of time it takes to accumulate them at a rate of limit tokens per second.
|
||||
func (limit Limit) durationFromTokens(tokens float64) time.Duration {
|
||||
seconds := tokens / float64(limit)
|
||||
return time.Duration(float64(time.Second) * seconds)
|
||||
}
|
||||
|
||||
// tokensFromDuration is a unit conversion function from a time duration to the number of tokens
|
||||
// which could be accumulated during that duration at a rate of limit tokens per second.
|
||||
func (limit Limit) tokensFromDuration(d time.Duration) float64 {
|
||||
return d.Seconds() * float64(limit)
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
// Package libstring provides various string related functions.
|
||||
package libstring
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/didip/tollbooth/v8/limiter"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
return ip
|
||||
|
||||
case "X-Forwarded-For", "X-Real-IP", "CF-Connecting-IP":
|
||||
ipAddrListCommaSeparated := r.Header.Values(ipLookup.Name)
|
||||
|
||||
ipAddrCommaSeparated := strings.Join(ipAddrListCommaSeparated, ",")
|
||||
|
||||
ips := strings.Split(ipAddrCommaSeparated, ",")
|
||||
for i, p := range ips {
|
||||
ips[i] = strings.TrimSpace(p)
|
||||
}
|
||||
|
||||
ipIndex := len(ips) - 1 - ipLookup.IndexFromRight
|
||||
if ipIndex < 0 {
|
||||
ipIndex = 0
|
||||
}
|
||||
|
||||
return ips[ipIndex]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// CanonicalizeIP returns a form of ip suitable for comparison to other IPs.
|
||||
// For IPv4 addresses, this is simply the whole string.
|
||||
// For IPv6 addresses, this is the /64 prefix.
|
||||
func CanonicalizeIP(ip string) string {
|
||||
isIPv6 := false
|
||||
// This is how net.ParseIP decides if an address is IPv6
|
||||
// https://cs.opensource.google/go/go/+/refs/tags/go1.17.7:src/net/ip.go;l=704
|
||||
for i := 0; !isIPv6 && i < len(ip); i++ {
|
||||
switch ip[i] {
|
||||
case '.':
|
||||
// IPv4
|
||||
return ip
|
||||
case ':':
|
||||
// IPv6
|
||||
isIPv6 = true
|
||||
}
|
||||
}
|
||||
if !isIPv6 {
|
||||
// Not an IP address at all
|
||||
return ip
|
||||
}
|
||||
|
||||
// By default, the string representation of a net.IPNet (masked IP address) is just
|
||||
// "full_address/mask_bits". But using that will result in different addresses with
|
||||
// the same /64 prefix comparing differently. So we need to zero out the last 64 bits
|
||||
// so that all IPs in the same prefix will be the same.
|
||||
//
|
||||
// Note: When 1.18 is the minimum Go version, this can be written more cleanly like:
|
||||
// netip.PrefixFrom(netip.MustParseAddr(ipv6), 64).Masked().Addr().String()
|
||||
// (With appropriate error checking.)
|
||||
|
||||
ipv6 := net.ParseIP(ip)
|
||||
if ipv6 == nil {
|
||||
return ip
|
||||
}
|
||||
|
||||
const bytesToZero = (128 - 64) / 8
|
||||
for i := len(ipv6) - bytesToZero; i < len(ipv6); i++ {
|
||||
ipv6[i] = 0
|
||||
}
|
||||
|
||||
// Note that this doesn't have the "/64" suffix customary with a CIDR representation,
|
||||
// but those three bytes add nothing for us.
|
||||
return ipv6.String()
|
||||
}
|
||||
+624
@@ -0,0 +1,624 @@
|
||||
// Package limiter provides data structure to configure rate-limiter.
|
||||
package limiter
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cache "github.com/go-pkgz/expirable-cache/v3"
|
||||
|
||||
"github.com/didip/tollbooth/v8/internal/time/rate"
|
||||
)
|
||||
|
||||
// New is a constructor for Limiter.
|
||||
func New(generalExpirableOptions *ExpirableOptions) *Limiter {
|
||||
lmt := &Limiter{}
|
||||
|
||||
lmt.SetMessageContentType("text/plain; charset=utf-8").
|
||||
SetMessage("You have reached maximum request limit.").
|
||||
SetStatusCode(429).
|
||||
SetOnLimitReached(nil).
|
||||
SetForwardedForIndexFromBehind(0).
|
||||
SetHeaders(make(map[string][]string)).
|
||||
SetContextValues(make(map[string][]string)).
|
||||
SetIgnoreURL(false)
|
||||
|
||||
if generalExpirableOptions != nil {
|
||||
lmt.generalExpirableOptions = generalExpirableOptions
|
||||
} else {
|
||||
lmt.generalExpirableOptions = &ExpirableOptions{}
|
||||
}
|
||||
|
||||
// Default for DefaultExpirationTTL is 10 years.
|
||||
if lmt.generalExpirableOptions.DefaultExpirationTTL <= 0 {
|
||||
lmt.generalExpirableOptions.DefaultExpirationTTL = 87600 * time.Hour
|
||||
}
|
||||
|
||||
lmt.tokenBuckets = cache.NewCache[string, *rate.Limiter]().WithTTL(lmt.generalExpirableOptions.DefaultExpirationTTL)
|
||||
|
||||
lmt.basicAuthUsers = cache.NewCache[string, bool]().WithTTL(lmt.generalExpirableOptions.DefaultExpirationTTL)
|
||||
|
||||
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.
|
||||
max float64
|
||||
|
||||
// Limiter burst size
|
||||
burst int
|
||||
|
||||
// HTTP message when limit is reached.
|
||||
message string
|
||||
|
||||
// Content-Type for Message
|
||||
messageContentType string
|
||||
|
||||
// HTTP status code when limit is reached.
|
||||
statusCode int
|
||||
|
||||
// A function to call when a request is rejected.
|
||||
onLimitReached func(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// An option to write back what you want upon reaching a limit.
|
||||
overrideDefaultResponseWriter bool
|
||||
|
||||
// Explicitly define how to look up IP address.
|
||||
// This is intended to replace ipLookups
|
||||
explicitIPLookup IPLookup
|
||||
|
||||
forwardedForIndex int
|
||||
|
||||
// List of HTTP Methods to limit (GET, POST, PUT, etc.).
|
||||
// Empty means limit all methods.
|
||||
methods []string
|
||||
|
||||
// Able to configure token bucket expirations.
|
||||
generalExpirableOptions *ExpirableOptions
|
||||
|
||||
// List of basic auth usernames to limit.
|
||||
basicAuthUsers cache.Cache[string, bool]
|
||||
|
||||
// Map of HTTP headers to limit.
|
||||
// Empty means skip headers checking.
|
||||
headers map[string]cache.Cache[string, bool]
|
||||
|
||||
// Map of Context values to limit.
|
||||
contextValues map[string]cache.Cache[string, bool]
|
||||
|
||||
// Map of limiters with TTL
|
||||
tokenBuckets cache.Cache[string, *rate.Limiter]
|
||||
|
||||
// Ignore URL on the rate limiter keys
|
||||
ignoreURL bool
|
||||
|
||||
tokenBucketExpirationTTL time.Duration
|
||||
basicAuthExpirationTTL time.Duration
|
||||
headerEntryExpirationTTL time.Duration
|
||||
contextEntryExpirationTTL time.Duration
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// SetTokenBucketExpirationTTL is thread-safe way of setting custom token bucket expiration TTL.
|
||||
func (l *Limiter) SetTokenBucketExpirationTTL(ttl time.Duration) *Limiter {
|
||||
l.Lock()
|
||||
l.tokenBucketExpirationTTL = ttl
|
||||
l.Unlock()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// GetTokenBucketExpirationTTL is thread-safe way of getting custom token bucket expiration TTL.
|
||||
func (l *Limiter) GetTokenBucketExpirationTTL() time.Duration {
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
return l.tokenBucketExpirationTTL
|
||||
}
|
||||
|
||||
// SetBasicAuthExpirationTTL is thread-safe way of setting custom basic auth expiration TTL.
|
||||
func (l *Limiter) SetBasicAuthExpirationTTL(ttl time.Duration) *Limiter {
|
||||
l.Lock()
|
||||
l.basicAuthExpirationTTL = ttl
|
||||
l.Unlock()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// GetBasicAuthExpirationTTL is thread-safe way of getting custom basic auth expiration TTL.
|
||||
func (l *Limiter) GetBasicAuthExpirationTTL() time.Duration {
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
return l.basicAuthExpirationTTL
|
||||
}
|
||||
|
||||
// SetHeaderEntryExpirationTTL is thread-safe way of setting custom basic auth expiration TTL.
|
||||
func (l *Limiter) SetHeaderEntryExpirationTTL(ttl time.Duration) *Limiter {
|
||||
l.Lock()
|
||||
l.headerEntryExpirationTTL = ttl
|
||||
l.Unlock()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// GetHeaderEntryExpirationTTL is thread-safe way of getting custom basic auth expiration TTL.
|
||||
func (l *Limiter) GetHeaderEntryExpirationTTL() time.Duration {
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
return l.headerEntryExpirationTTL
|
||||
}
|
||||
|
||||
// SetContextValueEntryExpirationTTL is thread-safe way of setting custom Context value expiration TTL.
|
||||
func (l *Limiter) SetContextValueEntryExpirationTTL(ttl time.Duration) *Limiter {
|
||||
l.Lock()
|
||||
l.contextEntryExpirationTTL = ttl
|
||||
l.Unlock()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// GetContextValueEntryExpirationTTL is thread-safe way of getting custom Context value expiration TTL.
|
||||
func (l *Limiter) GetContextValueEntryExpirationTTL() time.Duration {
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
return l.contextEntryExpirationTTL
|
||||
}
|
||||
|
||||
// SetMax is thread-safe way of setting maximum number of requests to limit per second.
|
||||
func (l *Limiter) SetMax(max float64) *Limiter {
|
||||
l.Lock()
|
||||
l.max = max
|
||||
l.Unlock()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// GetMax is thread-safe way of getting maximum number of requests to limit per second.
|
||||
func (l *Limiter) GetMax() float64 {
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
return l.max
|
||||
}
|
||||
|
||||
// SetBurst is thread-safe way of setting maximum burst size.
|
||||
func (l *Limiter) SetBurst(burst int) *Limiter {
|
||||
l.Lock()
|
||||
l.burst = burst
|
||||
l.Unlock()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// GetBurst is thread-safe way of setting maximum burst size.
|
||||
func (l *Limiter) GetBurst() int {
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
|
||||
return l.burst
|
||||
}
|
||||
|
||||
// SetMessage is thread-safe way of setting HTTP message when limit is reached.
|
||||
func (l *Limiter) SetMessage(msg string) *Limiter {
|
||||
l.Lock()
|
||||
l.message = msg
|
||||
l.Unlock()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// GetMessage is thread-safe way of getting HTTP message when limit is reached.
|
||||
func (l *Limiter) GetMessage() string {
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
return l.message
|
||||
}
|
||||
|
||||
// SetMessageContentType is thread-safe way of setting HTTP message Content-Type when limit is reached.
|
||||
func (l *Limiter) SetMessageContentType(contentType string) *Limiter {
|
||||
l.Lock()
|
||||
l.messageContentType = contentType
|
||||
l.Unlock()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// GetMessageContentType is thread-safe way of getting HTTP message Content-Type when limit is reached.
|
||||
func (l *Limiter) GetMessageContentType() string {
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
return l.messageContentType
|
||||
}
|
||||
|
||||
// SetStatusCode is thread-safe way of setting HTTP status code when limit is reached.
|
||||
func (l *Limiter) SetStatusCode(statusCode int) *Limiter {
|
||||
l.Lock()
|
||||
l.statusCode = statusCode
|
||||
l.Unlock()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// GetStatusCode is thread-safe way of getting HTTP status code when limit is reached.
|
||||
func (l *Limiter) GetStatusCode() int {
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
return l.statusCode
|
||||
}
|
||||
|
||||
// SetOnLimitReached is thread-safe way of setting after-rejection function when limit is reached.
|
||||
func (l *Limiter) SetOnLimitReached(fn func(w http.ResponseWriter, r *http.Request)) *Limiter {
|
||||
l.Lock()
|
||||
l.onLimitReached = fn
|
||||
l.Unlock()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// ExecOnLimitReached is thread-safe way of executing after-rejection function when limit is reached.
|
||||
func (l *Limiter) ExecOnLimitReached(w http.ResponseWriter, r *http.Request) {
|
||||
l.RLock()
|
||||
fn := l.onLimitReached
|
||||
l.RUnlock()
|
||||
|
||||
if fn != nil {
|
||||
fn(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// SetOverrideDefaultResponseWriter is a thread-safe way of setting the response writer override variable.
|
||||
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.
|
||||
func (l *Limiter) GetOverrideDefaultResponseWriter() bool {
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
return l.overrideDefaultResponseWriter
|
||||
}
|
||||
|
||||
// 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.explicitIPLookup = lookup
|
||||
l.Unlock()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// 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.explicitIPLookup
|
||||
}
|
||||
|
||||
// SetIgnoreURL is thread-safe way of setting whenever ignore the URL on rate limit keys
|
||||
func (l *Limiter) SetIgnoreURL(enabled bool) *Limiter {
|
||||
l.Lock()
|
||||
l.ignoreURL = enabled
|
||||
l.Unlock()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// GetIgnoreURL returns whether the URL is ignored in the rate limit key set
|
||||
func (l *Limiter) GetIgnoreURL() bool {
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
return l.ignoreURL
|
||||
}
|
||||
|
||||
// SetForwardedForIndexFromBehind is thread-safe way of setting which X-Forwarded-For index to choose.
|
||||
func (l *Limiter) SetForwardedForIndexFromBehind(forwardedForIndex int) *Limiter {
|
||||
l.Lock()
|
||||
l.forwardedForIndex = forwardedForIndex
|
||||
l.Unlock()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// GetForwardedForIndexFromBehind is thread-safe way of getting which X-Forwarded-For index to choose.
|
||||
func (l *Limiter) GetForwardedForIndexFromBehind() int {
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
return l.forwardedForIndex
|
||||
}
|
||||
|
||||
// SetMethods is thread-safe way of setting list of HTTP Methods to limit (GET, POST, PUT, etc.).
|
||||
func (l *Limiter) SetMethods(methods []string) *Limiter {
|
||||
l.Lock()
|
||||
l.methods = methods
|
||||
l.Unlock()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// GetMethods is thread-safe way of getting list of HTTP Methods to limit (GET, POST, PUT, etc.).
|
||||
func (l *Limiter) GetMethods() []string {
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
return l.methods
|
||||
}
|
||||
|
||||
// SetBasicAuthUsers is thread-safe way of setting list of basic auth usernames to limit.
|
||||
func (l *Limiter) SetBasicAuthUsers(basicAuthUsers []string) *Limiter {
|
||||
ttl := l.GetBasicAuthExpirationTTL()
|
||||
if ttl <= 0 {
|
||||
ttl = l.generalExpirableOptions.DefaultExpirationTTL
|
||||
}
|
||||
|
||||
for _, basicAuthUser := range basicAuthUsers {
|
||||
l.basicAuthUsers.Set(basicAuthUser, true, ttl)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// GetBasicAuthUsers is thread-safe way of getting list of basic auth usernames to limit.
|
||||
func (l *Limiter) GetBasicAuthUsers() []string {
|
||||
return l.basicAuthUsers.Keys()
|
||||
}
|
||||
|
||||
// RemoveBasicAuthUsers is thread-safe way of removing basic auth usernames from existing list.
|
||||
func (l *Limiter) RemoveBasicAuthUsers(basicAuthUsers []string) *Limiter {
|
||||
for _, toBeRemoved := range basicAuthUsers {
|
||||
l.basicAuthUsers.Invalidate(toBeRemoved)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// DeleteExpiredTokenBuckets is thread-safe way of deleting expired token buckets
|
||||
func (l *Limiter) DeleteExpiredTokenBuckets() {
|
||||
l.tokenBuckets.DeleteExpired()
|
||||
}
|
||||
|
||||
// SetHeaders is thread-safe way of setting map of HTTP headers to limit.
|
||||
func (l *Limiter) SetHeaders(headers map[string][]string) *Limiter {
|
||||
if l.headers == nil {
|
||||
l.headers = make(map[string]cache.Cache[string, bool])
|
||||
}
|
||||
|
||||
for header, entries := range headers {
|
||||
l.SetHeader(header, entries)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// GetHeaders is thread-safe way of getting map of HTTP headers to limit.
|
||||
func (l *Limiter) GetHeaders() map[string][]string {
|
||||
results := make(map[string][]string)
|
||||
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
|
||||
for header, entriesAsGoCache := range l.headers {
|
||||
results[header] = entriesAsGoCache.Keys()
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// SetHeader is thread-safe way of setting entries of 1 HTTP header.
|
||||
func (l *Limiter) SetHeader(header string, entries []string) *Limiter {
|
||||
l.RLock()
|
||||
existing, found := l.headers[header]
|
||||
l.RUnlock()
|
||||
|
||||
ttl := l.GetHeaderEntryExpirationTTL()
|
||||
if ttl <= 0 {
|
||||
ttl = l.generalExpirableOptions.DefaultExpirationTTL
|
||||
}
|
||||
|
||||
if !found {
|
||||
existing = cache.NewCache[string, bool]().WithTTL(ttl)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
existing.Set(entry, true, ttl)
|
||||
}
|
||||
|
||||
l.Lock()
|
||||
l.headers[header] = existing
|
||||
l.Unlock()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// GetHeader is thread-safe way of getting entries of 1 HTTP header.
|
||||
func (l *Limiter) GetHeader(header string) []string {
|
||||
l.RLock()
|
||||
entriesAsGoCache := l.headers[header]
|
||||
l.RUnlock()
|
||||
|
||||
return entriesAsGoCache.Keys()
|
||||
}
|
||||
|
||||
// RemoveHeader is thread-safe way of removing entries of 1 HTTP header.
|
||||
func (l *Limiter) RemoveHeader(header string) *Limiter {
|
||||
ttl := l.GetHeaderEntryExpirationTTL()
|
||||
if ttl <= 0 {
|
||||
ttl = l.generalExpirableOptions.DefaultExpirationTTL
|
||||
}
|
||||
|
||||
l.Lock()
|
||||
l.headers[header] = cache.NewCache[string, bool]().WithTTL(ttl)
|
||||
l.Unlock()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// RemoveHeaderEntries is thread-safe way of removing new entries to 1 HTTP header rule.
|
||||
func (l *Limiter) RemoveHeaderEntries(header string, entriesForRemoval []string) *Limiter {
|
||||
l.RLock()
|
||||
entries, found := l.headers[header]
|
||||
l.RUnlock()
|
||||
|
||||
if !found {
|
||||
return l
|
||||
}
|
||||
|
||||
for _, toBeRemoved := range entriesForRemoval {
|
||||
entries.Invalidate(toBeRemoved)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// SetContextValues is thread-safe way of setting map of HTTP headers to limit.
|
||||
func (l *Limiter) SetContextValues(contextValues map[string][]string) *Limiter {
|
||||
if l.contextValues == nil {
|
||||
l.contextValues = make(map[string]cache.Cache[string, bool])
|
||||
}
|
||||
|
||||
for contextValue, entries := range contextValues {
|
||||
l.SetContextValue(contextValue, entries)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// GetContextValues is thread-safe way of getting a map of Context values to limit.
|
||||
func (l *Limiter) GetContextValues() map[string][]string {
|
||||
results := make(map[string][]string)
|
||||
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
|
||||
for contextValue, entriesAsGoCache := range l.contextValues {
|
||||
results[contextValue] = entriesAsGoCache.Keys()
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// SetContextValue is thread-safe way of setting entries of 1 Context value.
|
||||
func (l *Limiter) SetContextValue(contextValue string, entries []string) *Limiter {
|
||||
l.RLock()
|
||||
existing, found := l.contextValues[contextValue]
|
||||
l.RUnlock()
|
||||
|
||||
ttl := l.GetContextValueEntryExpirationTTL()
|
||||
if ttl <= 0 {
|
||||
ttl = l.generalExpirableOptions.DefaultExpirationTTL
|
||||
}
|
||||
|
||||
if !found {
|
||||
existing = cache.NewCache[string, bool]().WithTTL(ttl)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
existing.Set(entry, true, ttl)
|
||||
}
|
||||
|
||||
l.Lock()
|
||||
l.contextValues[contextValue] = existing
|
||||
l.Unlock()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// GetContextValue is thread-safe way of getting 1 Context value entry.
|
||||
func (l *Limiter) GetContextValue(contextValue string) []string {
|
||||
l.RLock()
|
||||
entriesAsGoCache := l.contextValues[contextValue]
|
||||
l.RUnlock()
|
||||
|
||||
return entriesAsGoCache.Keys()
|
||||
}
|
||||
|
||||
// RemoveContextValue is thread-safe way of removing entries of 1 Context value.
|
||||
func (l *Limiter) RemoveContextValue(contextValue string) *Limiter {
|
||||
ttl := l.GetContextValueEntryExpirationTTL()
|
||||
if ttl <= 0 {
|
||||
ttl = l.generalExpirableOptions.DefaultExpirationTTL
|
||||
}
|
||||
|
||||
l.Lock()
|
||||
l.contextValues[contextValue] = cache.NewCache[string, bool]().WithTTL(ttl)
|
||||
l.Unlock()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// RemoveContextValuesEntries is thread-safe way of removing entries to a ContextValue.
|
||||
func (l *Limiter) RemoveContextValuesEntries(contextValue string, entriesForRemoval []string) *Limiter {
|
||||
l.RLock()
|
||||
entries, found := l.contextValues[contextValue]
|
||||
l.RUnlock()
|
||||
|
||||
if !found {
|
||||
return l
|
||||
}
|
||||
|
||||
for _, toBeRemoved := range entriesForRemoval {
|
||||
entries.Invalidate(toBeRemoved)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *Limiter) limitReachedWithTokenBucketTTL(key string, tokenBucketTTL time.Duration) bool {
|
||||
lmtMax := l.GetMax()
|
||||
lmtBurst := l.GetBurst()
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
|
||||
if _, found := l.tokenBuckets.Get(key); !found {
|
||||
l.tokenBuckets.Set(
|
||||
key,
|
||||
rate.NewLimiter(rate.Limit(lmtMax), lmtBurst),
|
||||
tokenBucketTTL,
|
||||
)
|
||||
}
|
||||
|
||||
expiringMap, found := l.tokenBuckets.Get(key)
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
|
||||
return !expiringMap.Allow()
|
||||
}
|
||||
|
||||
// LimitReached returns a bool indicating if the Bucket identified by key ran out of tokens.
|
||||
func (l *Limiter) LimitReached(key string) bool {
|
||||
ttl := l.GetTokenBucketExpirationTTL()
|
||||
|
||||
if ttl <= 0 {
|
||||
ttl = l.generalExpirableOptions.DefaultExpirationTTL
|
||||
}
|
||||
|
||||
return l.limitReachedWithTokenBucketTTL(key, ttl)
|
||||
}
|
||||
|
||||
// Tokens returns current amount of tokens left in the Bucket identified by key.
|
||||
func (l *Limiter) Tokens(key string) int {
|
||||
expiringMap, found := l.tokenBuckets.Get(key)
|
||||
if !found {
|
||||
return 0
|
||||
}
|
||||
|
||||
return int(expiringMap.TokensAt(time.Now()))
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package limiter
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ExpirableOptions are options used for new limiter creation
|
||||
type ExpirableOptions struct {
|
||||
DefaultExpirationTTL time.Duration
|
||||
|
||||
// How frequently expire job triggers
|
||||
// Deprecated: not used anymore
|
||||
ExpireJobInterval time.Duration
|
||||
}
|
||||
+375
@@ -0,0 +1,375 @@
|
||||
// Package tollbooth provides rate-limiting logic to HTTP request handler.
|
||||
package tollbooth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"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
|
||||
func setResponseHeaders(lmt *limiter.Limiter, w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("X-Rate-Limit-Limit", fmt.Sprintf("%.2f", lmt.GetMax()))
|
||||
w.Header().Add("X-Rate-Limit-Duration", "1")
|
||||
|
||||
xForwardedFor := r.Header.Get("X-Forwarded-For")
|
||||
if strings.TrimSpace(xForwardedFor) != "" {
|
||||
w.Header().Add("X-Rate-Limit-Request-Forwarded-For", xForwardedFor)
|
||||
}
|
||||
|
||||
w.Header().Add("X-Rate-Limit-Request-Remote-Addr", r.RemoteAddr)
|
||||
}
|
||||
|
||||
// setRateLimitResponseHeaders configures RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset
|
||||
// as seen at https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-ratelimit-headers
|
||||
func setRateLimitResponseHeaders(lmt *limiter.Limiter, w http.ResponseWriter, tokensLeft int) {
|
||||
w.Header().Add("RateLimit-Limit", fmt.Sprintf("%d", int(math.Round(lmt.GetMax()))))
|
||||
w.Header().Add("RateLimit-Reset", "1")
|
||||
w.Header().Add("RateLimit-Remaining", fmt.Sprintf("%d", tokensLeft))
|
||||
}
|
||||
|
||||
// NewLimiter is a convenience function to limiter.New.
|
||||
func NewLimiter(max float64, tbOptions *limiter.ExpirableOptions) *limiter.Limiter {
|
||||
return limiter.New(tbOptions).
|
||||
SetMax(max).
|
||||
SetBurst(int(math.Max(1, max)))
|
||||
}
|
||||
|
||||
// LimitByKeys keeps track number of request made by keys separated by pipe.
|
||||
// It returns HTTPError when limit is exceeded.
|
||||
func LimitByKeys(lmt *limiter.Limiter, keys []string) *errors.HTTPError {
|
||||
err, _ := LimitByKeysAndReturn(lmt, keys)
|
||||
return err
|
||||
}
|
||||
|
||||
// LimitByKeysAndReturn keeps track number of request made by keys separated by pipe.
|
||||
// It returns HTTPError when limit is exceeded, and also returns the current limit value.
|
||||
func LimitByKeysAndReturn(lmt *limiter.Limiter, keys []string) (*errors.HTTPError, int) {
|
||||
if lmt.LimitReached(strings.Join(keys, "|")) {
|
||||
return &errors.HTTPError{Message: lmt.GetMessage(), StatusCode: lmt.GetStatusCode()}, 0
|
||||
}
|
||||
|
||||
return nil, lmt.Tokens(strings.Join(keys, "|"))
|
||||
}
|
||||
|
||||
// ShouldSkipLimiter is a series of filter that decides if request should be limited or not.
|
||||
func ShouldSkipLimiter(lmt *limiter.Limiter, r *http.Request) bool {
|
||||
// ---------------------------------
|
||||
// Filter by remote ip
|
||||
// If we are unable to find remoteIP, skip limiter
|
||||
remoteIP := libstring.RemoteIPFromIPLookup(lmt.GetIPLookup(), r)
|
||||
remoteIP = libstring.CanonicalizeIP(remoteIP)
|
||||
if remoteIP == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------
|
||||
// Filter by request method
|
||||
lmtMethods := lmt.GetMethods()
|
||||
lmtMethodsIsSet := len(lmtMethods) > 0
|
||||
|
||||
if lmtMethodsIsSet {
|
||||
// If request does not contain all of the methods in limiter,
|
||||
// skip limiter
|
||||
requestMethodDefinedInLimiter := libstring.StringInSlice(lmtMethods, r.Method)
|
||||
|
||||
if !requestMethodDefinedInLimiter {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------
|
||||
// Filter by request headers
|
||||
lmtHeaders := lmt.GetHeaders()
|
||||
lmtHeadersIsSet := len(lmtHeaders) > 0
|
||||
|
||||
if lmtHeadersIsSet {
|
||||
// If request does not contain all of the headers in limiter,
|
||||
// skip limiter
|
||||
requestHeadersDefinedInLimiter := false
|
||||
|
||||
for headerKey := range lmtHeaders {
|
||||
reqHeaderValue := r.Header.Get(headerKey)
|
||||
if reqHeaderValue != "" {
|
||||
requestHeadersDefinedInLimiter = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !requestHeadersDefinedInLimiter {
|
||||
return true
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// If request contains the header key but not the values,
|
||||
// skip limiter
|
||||
requestHeadersDefinedInLimiter = false
|
||||
|
||||
for headerKey, headerValues := range lmtHeaders {
|
||||
if len(headerValues) == 0 {
|
||||
requestHeadersDefinedInLimiter = true
|
||||
continue
|
||||
}
|
||||
for _, headerValue := range headerValues {
|
||||
if r.Header.Get(headerKey) == headerValue {
|
||||
requestHeadersDefinedInLimiter = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !requestHeadersDefinedInLimiter {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------
|
||||
// Filter by context values
|
||||
lmtContextValues := lmt.GetContextValues()
|
||||
lmtContextValuesIsSet := len(lmtContextValues) > 0
|
||||
|
||||
if lmtContextValuesIsSet {
|
||||
// If request does not contain all of the contexts in limiter,
|
||||
// skip limiter
|
||||
requestContextValuesDefinedInLimiter := false
|
||||
|
||||
for contextKey := range lmtContextValues {
|
||||
reqContextValue := fmt.Sprintf("%v", r.Context().Value(contextKey))
|
||||
if reqContextValue != "" {
|
||||
requestContextValuesDefinedInLimiter = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !requestContextValuesDefinedInLimiter {
|
||||
return true
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// If request contains the context key but not the values,
|
||||
// skip limiter
|
||||
requestContextValuesDefinedInLimiter = false
|
||||
|
||||
for contextKey, contextValues := range lmtContextValues {
|
||||
for _, contextValue := range contextValues {
|
||||
if r.Header.Get(contextKey) == contextValue {
|
||||
requestContextValuesDefinedInLimiter = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !requestContextValuesDefinedInLimiter {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------
|
||||
// Filter by basic auth usernames
|
||||
lmtBasicAuthUsers := lmt.GetBasicAuthUsers()
|
||||
lmtBasicAuthUsersIsSet := len(lmtBasicAuthUsers) > 0
|
||||
|
||||
if lmtBasicAuthUsersIsSet {
|
||||
// If request does not contain all of the basic auth users in limiter,
|
||||
// skip limiter
|
||||
requestAuthUsernameDefinedInLimiter := false
|
||||
|
||||
username, _, ok := r.BasicAuth()
|
||||
if ok && libstring.StringInSlice(lmtBasicAuthUsers, username) {
|
||||
requestAuthUsernameDefinedInLimiter = true
|
||||
}
|
||||
|
||||
if !requestAuthUsernameDefinedInLimiter {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 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.RemoteIPFromIPLookup(lmt.GetIPLookup(), r)
|
||||
remoteIP = libstring.CanonicalizeIP(remoteIP)
|
||||
path := r.URL.Path
|
||||
sliceKeys := make([][]string, 0)
|
||||
|
||||
lmtMethods := lmt.GetMethods()
|
||||
lmtHeaders := lmt.GetHeaders()
|
||||
lmtContextValues := lmt.GetContextValues()
|
||||
lmtBasicAuthUsers := lmt.GetBasicAuthUsers()
|
||||
lmtIgnoreURL := lmt.GetIgnoreURL()
|
||||
|
||||
lmtHeadersIsSet := len(lmtHeaders) > 0
|
||||
lmtContextValuesIsSet := len(lmtContextValues) > 0
|
||||
lmtBasicAuthUsersIsSet := len(lmtBasicAuthUsers) > 0
|
||||
|
||||
usernameToLimit := ""
|
||||
if lmtBasicAuthUsersIsSet {
|
||||
username, _, ok := r.BasicAuth()
|
||||
if ok && libstring.StringInSlice(lmtBasicAuthUsers, username) {
|
||||
usernameToLimit = username
|
||||
}
|
||||
}
|
||||
|
||||
headerValuesToLimit := [][]string{}
|
||||
if lmtHeadersIsSet {
|
||||
for headerKey, headerValues := range lmtHeaders {
|
||||
reqHeaderValue := r.Header.Get(headerKey)
|
||||
if reqHeaderValue == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(headerValues) == 0 {
|
||||
// If header values are empty, rate-limit all request containing headerKey.
|
||||
headerValuesToLimit = append(headerValuesToLimit, []string{headerKey, reqHeaderValue})
|
||||
|
||||
} else {
|
||||
// If header values are not empty, rate-limit all request with headerKey and headerValues.
|
||||
for _, headerValue := range headerValues {
|
||||
if r.Header.Get(headerKey) == headerValue {
|
||||
headerValuesToLimit = append(headerValuesToLimit, []string{headerKey, headerValue})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contextValuesToLimit := [][]string{}
|
||||
if lmtContextValuesIsSet {
|
||||
for contextKey, contextValues := range lmtContextValues {
|
||||
reqContextValue := fmt.Sprintf("%v", r.Context().Value(contextKey))
|
||||
if reqContextValue == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(contextValues) == 0 {
|
||||
// If context values are empty, rate-limit all request containing contextKey.
|
||||
contextValuesToLimit = append(contextValuesToLimit, []string{contextKey, reqContextValue})
|
||||
|
||||
} else {
|
||||
// If context values are not empty, rate-limit all request with contextKey and contextValues.
|
||||
for _, contextValue := range contextValues {
|
||||
if reqContextValue == contextValue {
|
||||
contextValuesToLimit = append(contextValuesToLimit, []string{contextKey, contextValue})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sliceKey := []string{remoteIP}
|
||||
if !lmtIgnoreURL {
|
||||
sliceKey = append(sliceKey, path)
|
||||
}
|
||||
|
||||
sliceKey = append(sliceKey, lmtMethods...)
|
||||
|
||||
for _, header := range headerValuesToLimit {
|
||||
sliceKey = append(sliceKey, header[0], header[1])
|
||||
}
|
||||
|
||||
for _, contextValue := range contextValuesToLimit {
|
||||
sliceKey = append(sliceKey, contextValue[0], contextValue[1])
|
||||
}
|
||||
|
||||
sliceKey = append(sliceKey, usernameToLimit)
|
||||
|
||||
sliceKeys = append(sliceKeys, sliceKey)
|
||||
|
||||
return sliceKeys
|
||||
}
|
||||
|
||||
// LimitByRequest builds keys based on http.Request struct,
|
||||
// loops through all the keys, and check if any one of them returns HTTPError.
|
||||
func LimitByRequest(lmt *limiter.Limiter, w http.ResponseWriter, r *http.Request) *errors.HTTPError {
|
||||
setResponseHeaders(lmt, w, r)
|
||||
|
||||
shouldSkip := ShouldSkipLimiter(lmt, r)
|
||||
if shouldSkip {
|
||||
return nil
|
||||
}
|
||||
|
||||
sliceKeys := BuildKeys(lmt, r)
|
||||
|
||||
// Get the lowest value over all keys to return in headers.
|
||||
// Start with high arbitrary number so that any limit returned would be lower and would
|
||||
// overwrite the value we start with.
|
||||
var tokensLeft = math.MaxInt32
|
||||
|
||||
// Loop sliceKeys and check if one of them has error.
|
||||
for _, keys := range sliceKeys {
|
||||
httpError, keysLimit := LimitByKeysAndReturn(lmt, keys)
|
||||
if tokensLeft > keysLimit {
|
||||
tokensLeft = keysLimit
|
||||
}
|
||||
if httpError != nil {
|
||||
setRateLimitResponseHeaders(lmt, w, tokensLeft)
|
||||
return httpError
|
||||
}
|
||||
}
|
||||
|
||||
setRateLimitResponseHeaders(lmt, w, tokensLeft)
|
||||
return nil
|
||||
}
|
||||
|
||||
// LimitHandler is a middleware that performs rate-limiting given http.Handler struct.
|
||||
func LimitHandler(lmt *limiter.Limiter, next http.Handler) http.Handler {
|
||||
middle := func(w http.ResponseWriter, r *http.Request) {
|
||||
httpError := LimitByRequest(lmt, w, r)
|
||||
if httpError != nil {
|
||||
lmt.ExecOnLimitReached(w, r)
|
||||
if lmt.GetOverrideDefaultResponseWriter() {
|
||||
return
|
||||
}
|
||||
w.Header().Add("Content-Type", lmt.GetMessageContentType())
|
||||
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(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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user