boulder/ratelimits/limit.go

302 lines
8.4 KiB
Go

package ratelimits
import (
"errors"
"fmt"
"os"
"strings"
"github.com/letsencrypt/boulder/config"
"github.com/letsencrypt/boulder/strictyaml"
)
// errLimitDisabled indicates that the limit name specified is valid but is not
// currently configured.
var errLimitDisabled = errors.New("limit disabled")
// LimitConfig defines the exportable configuration for a rate limit or a rate
// limit override, without a `limit`'s internal fields.
//
// The zero value of this struct is invalid, because some of the fields must be
// greater than zero.
type LimitConfig struct {
// Burst specifies maximum concurrent allowed requests at any given time. It
// must be greater than zero.
Burst int64
// Count is the number of requests allowed per period. It must be greater
// than zero.
Count int64
// Period is the duration of time in which the count (of requests) is
// allowed. It must be greater than zero.
Period config.Duration
}
type LimitConfigs map[string]*LimitConfig
// limit defines the configuration for a rate limit or a rate limit override.
//
// The zero value of this struct is invalid, because some of the fields must
// be greater than zero.
type limit struct {
// burst specifies maximum concurrent allowed requests at any given time. It
// must be greater than zero.
burst int64
// count is the number of requests allowed per period. It must be greater
// than zero.
count int64
// period is the duration of time in which the count (of requests) is
// allowed. It must be greater than zero.
period config.Duration
// name is the name of the limit. It must be one of the Name enums defined
// in this package.
name Name
// emissionInterval is the interval, in nanoseconds, at which tokens are
// added to a bucket (period / count). This is also the steady-state rate at
// which requests can be made without being denied even once the burst has
// been exhausted. This is precomputed to avoid doing the same calculation
// on every request.
emissionInterval int64
// burstOffset is the duration of time, in nanoseconds, it takes for a
// bucket to go from empty to full (burst * (period / count)). This is
// precomputed to avoid doing the same calculation on every request.
burstOffset int64
// isOverride is true if the limit is an override.
isOverride bool
}
// precompute calculates the emissionInterval and burstOffset for the limit.
func (l *limit) precompute() {
l.emissionInterval = l.period.Nanoseconds() / l.count
l.burstOffset = l.emissionInterval * l.burst
}
func validateLimit(l *limit) error {
if l.burst <= 0 {
return fmt.Errorf("invalid burst '%d', must be > 0", l.burst)
}
if l.count <= 0 {
return fmt.Errorf("invalid count '%d', must be > 0", l.count)
}
if l.period.Duration <= 0 {
return fmt.Errorf("invalid period '%s', must be > 0", l.period)
}
return nil
}
type limits map[string]*limit
// loadDefaults marshals the defaults YAML file at path into a map of limits.
func loadDefaults(path string) (LimitConfigs, error) {
lm := make(LimitConfigs)
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
err = strictyaml.Unmarshal(data, &lm)
if err != nil {
return nil, err
}
return lm, nil
}
type overrideYAML struct {
LimitConfig `yaml:",inline"`
// Ids is a list of ids that this override applies to.
Ids []struct {
Id string `yaml:"id"`
// Comment is an optional field that can be used to provide additional
// context for the override.
Comment string `yaml:"comment,omitempty"`
} `yaml:"ids"`
}
type overridesYAML []map[string]overrideYAML
// loadOverrides marshals the YAML file at path into a map of overrides.
func loadOverrides(path string) (overridesYAML, error) {
ov := overridesYAML{}
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
err = strictyaml.Unmarshal(data, &ov)
if err != nil {
return nil, err
}
return ov, nil
}
// parseOverrideNameId is broken out for ease of testing.
func parseOverrideNameId(key string) (Name, string, error) {
if !strings.Contains(key, ":") {
// Avoids a potential panic in strings.SplitN below.
return Unknown, "", fmt.Errorf("invalid override %q, must be formatted 'name:id'", key)
}
nameAndId := strings.SplitN(key, ":", 2)
nameStr := nameAndId[0]
if nameStr == "" {
return Unknown, "", fmt.Errorf("empty name in override %q, must be formatted 'name:id'", key)
}
name, ok := stringToName[nameStr]
if !ok {
return Unknown, "", fmt.Errorf("unrecognized name %q in override limit %q, must be one of %v", nameStr, key, limitNames)
}
id := nameAndId[1]
if id == "" {
return Unknown, "", fmt.Errorf("empty id in override %q, must be formatted 'name:id'", key)
}
return name, id, nil
}
// parseOverrideLimits validates a YAML list of override limits. It must be
// formatted as a list of maps, where each map has a single key representing the
// limit name and a value that is a map containing the limit fields and an
// additional 'ids' field that is a list of ids that this override applies to.
func parseOverrideLimits(newOverridesYAML overridesYAML) (limits, error) {
parsed := make(limits)
for _, ov := range newOverridesYAML {
for k, v := range ov {
name, ok := stringToName[k]
if !ok {
return nil, fmt.Errorf("unrecognized name %q in override limit, must be one of %v", k, limitNames)
}
lim := &limit{
burst: v.Burst,
count: v.Count,
period: v.Period,
name: name,
isOverride: true,
}
lim.precompute()
err := validateLimit(lim)
if err != nil {
return nil, fmt.Errorf("validating override limit %q: %w", k, err)
}
for _, entry := range v.Ids {
id := entry.Id
err = validateIdForName(name, id)
if err != nil {
return nil, fmt.Errorf(
"validating name %s and id %q for override limit %q: %w", name, id, k, err)
}
if name == CertificatesPerFQDNSet {
// FQDNSet hashes are not a nice thing to ask for in a
// config file, so we allow the user to specify a
// comma-separated list of FQDNs and compute the hash here.
id = fmt.Sprintf("%x", hashNames(strings.Split(id, ",")))
}
parsed[joinWithColon(name.EnumString(), id)] = lim
}
}
}
return parsed, nil
}
// parseDefaultLimits validates a map of default limits and rekeys it by 'Name'.
func parseDefaultLimits(newDefaultLimits LimitConfigs) (limits, error) {
parsed := make(limits)
for k, v := range newDefaultLimits {
name, ok := stringToName[k]
if !ok {
return nil, fmt.Errorf("unrecognized name %q in default limit, must be one of %v", k, limitNames)
}
lim := &limit{
burst: v.Burst,
count: v.Count,
period: v.Period,
name: name,
}
err := validateLimit(lim)
if err != nil {
return nil, fmt.Errorf("parsing default limit %q: %w", k, err)
}
lim.precompute()
parsed[name.EnumString()] = lim
}
return parsed, nil
}
type limitRegistry struct {
// defaults stores default limits by 'name'.
defaults limits
// overrides stores override limits by 'name:id'.
overrides limits
}
func newLimitRegistryFromFiles(defaults, overrides string) (*limitRegistry, error) {
defaultsData, err := loadDefaults(defaults)
if err != nil {
return nil, err
}
if overrides == "" {
return newLimitRegistry(defaultsData, nil)
}
overridesData, err := loadOverrides(overrides)
if err != nil {
return nil, err
}
return newLimitRegistry(defaultsData, overridesData)
}
func newLimitRegistry(defaults LimitConfigs, overrides overridesYAML) (*limitRegistry, error) {
regDefaults, err := parseDefaultLimits(defaults)
if err != nil {
return nil, err
}
regOverrides, err := parseOverrideLimits(overrides)
if err != nil {
return nil, err
}
return &limitRegistry{
defaults: regDefaults,
overrides: regOverrides,
}, nil
}
// getLimit returns the limit for the specified by name and bucketKey, name is
// required, bucketKey is optional. If bucketkey is empty, the default for the
// limit specified by name is returned. If no default limit exists for the
// specified name, errLimitDisabled is returned.
func (l *limitRegistry) getLimit(name Name, bucketKey string) (*limit, error) {
if !name.isValid() {
// This should never happen. Callers should only be specifying the limit
// Name enums defined in this package.
return nil, fmt.Errorf("specified name enum %q, is invalid", name)
}
if bucketKey != "" {
// Check for override.
ol, ok := l.overrides[bucketKey]
if ok {
return ol, nil
}
}
dl, ok := l.defaults[name.EnumString()]
if ok {
return dl, nil
}
return nil, errLimitDisabled
}