63 lines
2.1 KiB
Go
63 lines
2.1 KiB
Go
// Package ratelimit enforces per-key and per-end-user request ceilings.
|
|
//
|
|
// Redis is authoritative for the live window; api.usage_daily holds completed
|
|
// days. A Redis flush therefore loses at most the window in progress, which for
|
|
// abuse detection is an acceptable trade.
|
|
package ratelimit
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
)
|
|
|
|
// Result is what a limiter says about one request.
|
|
type Result struct {
|
|
Allowed bool
|
|
Limit int
|
|
Remaining int
|
|
RetryAfter time.Duration
|
|
}
|
|
|
|
// Limiter is deliberately narrow so the middleware can be tested without Redis,
|
|
// and so a self-hoster running a single instance could drop in an in-process
|
|
// implementation instead of standing up Redis at all.
|
|
type Limiter interface {
|
|
// Allow records one request against key and reports whether it may
|
|
// proceed. Implementations must be atomic: a check-then-increment race
|
|
// under load is exactly when the limit matters most.
|
|
Allow(ctx context.Context, key string, limit int, window time.Duration) (Result, error)
|
|
}
|
|
|
|
// Quota is the ceiling for one tier, from api.keys.quota_tier.
|
|
type Quota struct {
|
|
// Requests allowed per window for the key as a whole.
|
|
KeyLimit int
|
|
|
|
// Requests allowed per window for a single end user within that key. One
|
|
// Discord user should not be able to consume a whole self-hosted
|
|
// instance's allowance, and a key whose traffic is 90% one user is the
|
|
// enumeration signal worth alerting on.
|
|
UserLimit int
|
|
|
|
Window time.Duration
|
|
}
|
|
|
|
// Tiers maps api.keys.quota_tier to its ceiling. Values are a starting point,
|
|
// not a considered policy: watch real usage before hardening them.
|
|
var Tiers = map[string]Quota{
|
|
"default": {KeyLimit: 600, UserLimit: 120, Window: time.Minute},
|
|
"verified": {KeyLimit: 3000, UserLimit: 600, Window: time.Minute},
|
|
"trusted": {KeyLimit: 12000, UserLimit: 2400, Window: time.Minute},
|
|
"internal": {KeyLimit: 0, UserLimit: 0, Window: time.Minute}, // 0 = unlimited
|
|
}
|
|
|
|
// QuotaFor falls back to the tightest tier for an unknown name, so a typo in
|
|
// the database cannot accidentally grant unlimited access.
|
|
func QuotaFor(tier string) Quota {
|
|
if q, ok := Tiers[tier]; ok {
|
|
return q
|
|
}
|
|
|
|
return Tiers["default"]
|
|
}
|