Documentation
¶
Overview ¶
Package tokenrate limits how fast each Caller may make requests, using the Rate Capabilities carried by the Caller's authenticated Token.
tokenrate never sees the Token. After authenticating a request, a service passes its principal and capability strings to Limiter.Check, or lets Middleware do so, and applies the Decision it gets back. A Resolver lets the service adjust the rates first, for ceilings, overrides and the like. See CONTEXT.md for the terms used here and docs/design.md for the behavior.
Check must run only after the request's credentials have been validated. The Limiter keeps state per principal, so an attacker who can choose the principal could fill memory with one request per name.
Index ¶
Examples ¶
Constants ¶
const DefaultWarningHeader = "X-Webpa-Capability-Warning"
DefaultWarningHeader is the header Middleware writes Capability Warnings to unless Middleware.WarningHeader says otherwise.
const WarningMalformed = "malformed"
WarningMalformed is the Reason of a Warning about a Rate Capability whose rate cannot be parsed.
Variables ¶
var ( // ErrNoPrefixes is returned by New when no Capability Prefix was // configured. ErrNoPrefixes = errors.New("at least one capability prefix is required") // ErrEmptyPrefix is returned by New when a Capability Prefix is empty. ErrEmptyPrefix = errors.New("a capability prefix cannot be empty") // ErrInvalidRate is the Decision.Err when a Resolver returns a rate that // is neither valid nor the zero Rate, which denies the request. ErrInvalidRate = errors.New("resolver returned an invalid rate") )
Errors from New and Check.
var ErrMalformedRate = errors.New("malformed rate")
ErrMalformedRate is returned by ParseRate when a rate cannot be parsed.
Functions ¶
This section is empty.
Types ¶
type Decision ¶
type Decision struct {
// Allowed is whether the request may proceed.
Allowed bool
// Reason is why the check failed, or None.
Reason Reason
// Limits are the rates applied, shortest window first. The request had
// to fit every one. It is empty when the Token is Unrestricted or the
// Resolver denied the request. It holds zero Rates, each reading back
// as the client wrote it, when the Token counts as carrying them, e.g.
// its only Rate Capabilities were malformed. Otherwise the rates are
// the Caller's allowances, which read back formatted.
Limits []Rate
// Limit is the rate that refused the request: when Reason is
// RateExceeded, the one of Limits with the longest wait, or the zero
// Rate when that is what refused. It is the zero Rate otherwise.
Limit Rate
// RetryAfter is how long until the Caller has allowance for another
// call at Limit. It is set only when Reason is RateExceeded and Limit
// is not zero; a zero limit never recovers by waiting.
RetryAfter time.Duration
// Warnings are Capability Warnings for the Caller. They are returned
// whether or not the request is Allowed.
Warnings []Warning
// Err is the Resolver's error when Reason is Denied, or ErrInvalidRate
// if it returned an invalid rate. It is for the service to log, not
// for the Caller.
Err error
}
Decision is the result of one Check.
type Extractor ¶
Extractor returns the principal and capabilities of an authenticated request, however the service stores them, e.g. from the Token in the request's context. The principal must come from validated credentials, never from anything the client can choose freely. Returning an empty principal and no capabilities for an unauthenticated request leaves it Unrestricted, unless the Resolver returns rates for it or denies it.
type Limiter ¶
type Limiter struct {
// contains filtered or unexported fields
}
Limiter decides, for each request, whether its Caller is within their Caller Rates. It is safe for concurrent use.
It keeps state for each Caller whose Token carries a rate, until twice the longest window after their last request: at most 48 hours under DefaultResolver. There is no cap on how many Callers it tracks, since forgetting a Caller early would give them a fresh allowance.
It needs no starting or stopping. A check that finds expired state starts a goroutine to sweep it, unless one is running, which exits when done.
func (*Limiter) Check ¶
Check decides one request from principal, whose Token carries capabilities, and spends one call from each of the Caller's allowances. ctx is passed to the Resolver.
Example ¶
package main
import (
"context"
"fmt"
"github.com/xmidt-org/tokenrate"
)
func main() {
limiter, err := tokenrate.New(tokenrate.WithPrefixes("prefix:rate:"))
if err != nil {
panic(err)
}
// Without HTTP, apply the Decision yourself. Every rate in the Token
// applies: this Caller may burst 1000, and make at most 100000 a day.
d := limiter.Check(context.Background(), "abc", []string{"prefix:rate:1000/1m", "prefix:rate:100000/24h"})
fmt.Println(d.Allowed, d.Limits, d.Reason)
// A Token with no rate is Unrestricted, unless a Resolver says otherwise.
d = limiter.Check(context.Background(), "def", nil)
fmt.Println(d.Allowed, d.Limits, d.Reason)
}
Output: true [1000/1m 100000/24h] none true [] none
type Middleware ¶
type Middleware struct {
// Limiter decides each request. Required.
Limiter *Limiter
// Extract finds the principal and capabilities in a request. Required.
Extract Extractor
// WarningHeader is the header Capability Warnings are written to. Empty
// means DefaultWarningHeader.
WarningHeader string
// OmitWarnings, if set, writes no Capability Warnings. Observe still
// sees them in the Decision.
OmitWarnings bool
// Observe, if set, is called with every Decision, for metrics or
// logging. It runs before the response is written.
Observe func(*http.Request, Decision)
}
Middleware applies a Limiter to HTTP requests. Place it after the middleware that validates the request's credentials. It needs the Token's principal and capabilities, and the Limiter keeps state per principal, so running it before authentication would let an attacker fill memory with one request per made-up principal.
For each request it checks the Caller and, unless OmitWarnings is set, adds one WarningHeader per Capability Warning, whether or not the request goes on. An allowed request is passed to the next handler. A rejected one gets 429 Too Many Requests, with Retry-After when waiting would help, or 403 Forbidden when the Resolver denied it, which Observe can log from Decision.Err.
Example ¶
package main
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"github.com/xmidt-org/tokenrate"
)
// A service's authentication middleware normally leaves the Token in the
// request's context. This stand-in reads it from headers instead.
func principalAndCapabilities(r *http.Request) (string, []string) {
return r.Header.Get("Principal"), strings.Split(r.Header.Get("Capabilities"), ",")
}
func main() {
// Every Token must carry a rate; the Resolver denies the ones that don't.
requireRate := func(ctx context.Context, principal string, provided []tokenrate.Rate) ([]tokenrate.Rate, error) {
rates, _ := tokenrate.DefaultResolver(ctx, principal, provided)
if len(rates) == 0 {
return nil, errors.New("no rate capability")
}
return rates, nil
}
limiter, err := tokenrate.New(
tokenrate.WithPrefixes("prefix:rate:"),
tokenrate.WithResolver(requireRate),
)
if err != nil {
panic(err)
}
handler := tokenrate.Middleware{
Limiter: limiter,
Extract: principalAndCapabilities,
}.Wrap(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
// The Token allows 2 calls per minute, so the third is refused. Its
// second capability is malformed, which every response warns about.
for range 3 {
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.Header.Set("Principal", "abc")
r.Header.Set("Capabilities", "prefix:rate:2/1m,prefix:rate:5/0s")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
fmt.Println(w.Code, w.Header().Get("Retry-After"), w.Header().Get(tokenrate.DefaultWarningHeader))
}
// A Token with no rate is denied by the Resolver.
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.Header.Set("Principal", "def")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
fmt.Println(w.Code)
}
Output: 204 malformed; kind=rate; cap="prefix:rate:5/0s" 204 malformed; kind=rate; cap="prefix:rate:5/0s" 429 30 malformed; kind=rate; cap="prefix:rate:5/0s" 403
type Option ¶
type Option interface {
// contains filtered or unexported methods
}
Option configures a Limiter.
func WithPrefixes ¶
WithPrefixes adds Capability Prefixes, which select the Rate Capabilities among a Token's capabilities. At least one is required.
A prefix is a plain string, matched exactly at the start of the capability, and whatever follows it is the rate. A capability that starts with no prefix is ignored; one that starts with several is read using the first configured. A prefix cannot be empty, since every capability would then count as a rate.
func WithResolver ¶
WithResolver sets the Resolver, replacing DefaultResolver.
Example ¶
package main
import (
"context"
"fmt"
"time"
"github.com/xmidt-org/tokenrate"
)
func main() {
ceiling := tokenrate.Rate{Count: 500, Window: time.Minute}
overrides := map[string][]tokenrate.Rate{
"partner": {{Count: 10000, Window: time.Minute}},
}
// The Resolver has the final say over the rates a Token counts as
// carrying, so the deployment's special cases live in one place.
resolve := func(ctx context.Context, principal string, provided []tokenrate.Rate) ([]tokenrate.Rate, error) {
if override, ok := overrides[principal]; ok {
return override, nil // replaces whatever the Token says
}
rates, _ := tokenrate.DefaultResolver(ctx, principal, provided)
if len(rates) == 0 {
return nil, nil // a Token with no rate stays Unrestricted
}
return append(rates, ceiling), nil // nobody else exceeds the ceiling
}
limiter, err := tokenrate.New(
tokenrate.WithPrefixes("prefix:rate:"),
tokenrate.WithResolver(resolve),
)
if err != nil {
panic(err)
}
ctx := context.Background()
fmt.Println(limiter.Check(ctx, "abc", []string{"prefix:rate:1000/1m"}).Limits)
fmt.Println(limiter.Check(ctx, "partner", []string{"prefix:rate:1000/1m"}).Limits)
fmt.Println(limiter.Check(ctx, "def", nil).Limits)
}
Output: [500/1m 1000/1m] [10000/1m] []
type Rate ¶
Rate is a number of calls allowed per window. Its Burst is always Count.
A zero Rate, with no Count and no Window, allows nothing. It is how a Malformed Capability reaches a Resolver, and how a Resolver refuses a Token without denying the Caller.
func DefaultResolver ¶
DefaultResolver is the Resolver unless WithResolver says otherwise. It ignores a Malformed Capability when the Token has valid rates, so a typo in one rate doesn't void the others, and holds a Token whose rates are all malformed to those zero Rates, so a typo never means no limit.
It also holds each valid rate within the Window Bounds, one minute to 24 hours. A rate outside them is rescaled to the nearer bound at the same calls per second, rounded to the nearest call but at least one, so 100/1s is held as 6000/1m and 100/48h as 50/24h. This keeps a tiny window from churning state and a long one from allowing a huge Burst at once. A rescaled rate is no longer what the client wrote, so it reads back formatted.
It never returns an error.
func ParseRate ¶
ParseRate parses a rate written <count>/<window>, e.g. 50/1m or 100/24h.
The count is a positive base-10 integer. The window is a positive Go duration (see time.ParseDuration). ParseRate checks only the grammar; a Limiter rescales rates whose window is outside its Window Bounds.
The Rate returned reads back as s from String. On failure it is zero.
type Resolver ¶
A Resolver has the final say over the rates a Token counts as carrying. It is called on every check, outside any lock, with the request's context, the Token's principal and the rates of its Rate Capabilities, in Token order and as parsed. Each reads back as the client wrote it from String. A Malformed Capability arrives as a zero Rate, which allows nothing.
Whatever the Resolver returns is used instead, as if the Token had carried those rates: they are remembered for the Caller, and they are trusted, so they are used as returned. Returning no rates leaves the Token Unrestricted. Returning a zero Rate refuses the request as RateExceeded, with nothing remembered. Returning an error denies the request, whatever the reason: the error is returned in the Decision for the service to log, and is not shown to the Caller.
That covers the policy a deployment needs, without options for each case:
- A ceiling for every Caller: return append(provided, ceiling).
- An Override for one Caller: ignore provided and return the Override.
- Requiring a rate: return an error when provided is empty.
- Vouching for a Caller whose Token has no rate: return rates for it.
- Blocking a Caller: return an error.
The Limiter keeps state for every principal the Resolver returns rates for. Returning a rate that is neither valid nor zero denies the request with ErrInvalidRate.
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
client
command
Command client makes a burst of requests to the example server and prints what came back, one line per request, so the limiter's answers can be seen: the status, Retry-After when it is refused, and any Capability Warning.
|
Command client makes a burst of requests to the example server and prints what came back, one line per request, so the limiter's answers can be seen: the status, Retry-After when it is refused, and any Capability Warning. |
|
server
command
Command server is a small HTTP service limited by tokenrate.
|
Command server is a small HTTP service limited by tokenrate. |
|
internal
|
|
|
gcra
Package gcra implements the Generic Cell Rate Algorithm: a leaky bucket whose whole state is a single theoretical arrival time (TAT).
|
Package gcra implements the Generic Cell Rate Algorithm: a leaky bucket whose whole state is a single theoretical arrival time (TAT). |