...

Source file src/github.com/letsencrypt/boulder/ratelimits/limit.go

Documentation: github.com/letsencrypt/boulder/ratelimits

     1  package ratelimits
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"strings"
     7  
     8  	"github.com/letsencrypt/boulder/config"
     9  	"github.com/letsencrypt/boulder/core"
    10  	"github.com/letsencrypt/boulder/strictyaml"
    11  )
    12  
    13  type limit struct {
    14  	// Burst specifies maximum concurrent allowed requests at any given time. It
    15  	// must be greater than zero.
    16  	Burst int64
    17  
    18  	// Count is the number of requests allowed per period. It must be greater
    19  	// than zero.
    20  	Count int64
    21  
    22  	// Period is the duration of time in which the count (of requests) is
    23  	// allowed. It must be greater than zero.
    24  	Period config.Duration
    25  
    26  	// emissionInterval is the interval, in nanoseconds, at which tokens are
    27  	// added to a bucket (period / count). This is also the steady-state rate at
    28  	// which requests can be made without being denied even once the burst has
    29  	// been exhausted. This is precomputed to avoid doing the same calculation
    30  	// on every request.
    31  	emissionInterval int64
    32  
    33  	// burstOffset is the duration of time, in nanoseconds, it takes for a
    34  	// bucket to go from empty to full (burst * (period / count)). This is
    35  	// precomputed to avoid doing the same calculation on every request.
    36  	burstOffset int64
    37  
    38  	// isOverride is true if this limit is an override limit, false if it is a
    39  	// default limit.
    40  	isOverride bool
    41  }
    42  
    43  func precomputeLimit(l limit) limit {
    44  	l.emissionInterval = l.Period.Nanoseconds() / l.Count
    45  	l.burstOffset = l.emissionInterval * l.Burst
    46  	return l
    47  }
    48  
    49  func validateLimit(l limit) error {
    50  	if l.Burst <= 0 {
    51  		return fmt.Errorf("invalid burst '%d', must be > 0", l.Burst)
    52  	}
    53  	if l.Count <= 0 {
    54  		return fmt.Errorf("invalid count '%d', must be > 0", l.Count)
    55  	}
    56  	if l.Period.Duration <= 0 {
    57  		return fmt.Errorf("invalid period '%s', must be > 0", l.Period)
    58  	}
    59  	return nil
    60  }
    61  
    62  type limits map[string]limit
    63  
    64  // loadLimits marshals the YAML file at path into a map of limis.
    65  func loadLimits(path string) (limits, error) {
    66  	lm := make(limits)
    67  	data, err := os.ReadFile(path)
    68  	if err != nil {
    69  		return nil, err
    70  	}
    71  	err = strictyaml.Unmarshal(data, &lm)
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  	return lm, nil
    76  }
    77  
    78  // parseOverrideNameId is broken out for ease of testing.
    79  func parseOverrideNameId(key string) (Name, string, error) {
    80  	if !strings.Contains(key, ":") {
    81  		// Avoids a potential panic in strings.SplitN below.
    82  		return Unknown, "", fmt.Errorf("invalid override %q, must be formatted 'name:id'", key)
    83  	}
    84  	nameAndId := strings.SplitN(key, ":", 2)
    85  	nameStr := nameAndId[0]
    86  	if nameStr == "" {
    87  		return Unknown, "", fmt.Errorf("empty name in override %q, must be formatted 'name:id'", key)
    88  	}
    89  
    90  	name, ok := stringToName[nameStr]
    91  	if !ok {
    92  		return Unknown, "", fmt.Errorf("unrecognized name %q in override limit %q, must be one of %v", nameStr, key, limitNames)
    93  	}
    94  	id := nameAndId[1]
    95  	if id == "" {
    96  		return Unknown, "", fmt.Errorf("empty id in override %q, must be formatted 'name:id'", key)
    97  	}
    98  	return name, id, nil
    99  }
   100  
   101  // loadAndParseOverrideLimits loads override limits from YAML, validates them,
   102  // and parses them into a map of limits keyed by 'Name:id'.
   103  func loadAndParseOverrideLimits(path string) (limits, error) {
   104  	fromFile, err := loadLimits(path)
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  	parsed := make(limits, len(fromFile))
   109  
   110  	for k, v := range fromFile {
   111  		err = validateLimit(v)
   112  		if err != nil {
   113  			return nil, fmt.Errorf("validating override limit %q: %w", k, err)
   114  		}
   115  		name, id, err := parseOverrideNameId(k)
   116  		if err != nil {
   117  			return nil, fmt.Errorf("parsing override limit %q: %w", k, err)
   118  		}
   119  		err = validateIdForName(name, id)
   120  		if err != nil {
   121  			return nil, fmt.Errorf(
   122  				"validating name %s and id %q for override limit %q: %w", name, id, k, err)
   123  		}
   124  		if name == CertificatesPerFQDNSetPerAccount {
   125  			// FQDNSet hashes are not a nice thing to ask for in a config file,
   126  			// so we allow the user to specify a comma-separated list of FQDNs
   127  			// and compute the hash here.
   128  			regIdDomains := strings.SplitN(id, ":", 2)
   129  			if len(regIdDomains) != 2 {
   130  				// Should never happen, the Id format was validated above.
   131  				return nil, fmt.Errorf("invalid override limit %q, must be formatted 'name:id'", k)
   132  			}
   133  			regId := regIdDomains[0]
   134  			domains := strings.Split(regIdDomains[1], ",")
   135  			fqdnSet := core.HashNames(domains)
   136  			id = fmt.Sprintf("%s:%s", regId, fqdnSet)
   137  		}
   138  		v.isOverride = true
   139  		parsed[bucketKey(name, id)] = precomputeLimit(v)
   140  	}
   141  	return parsed, nil
   142  }
   143  
   144  // loadAndParseDefaultLimits loads default limits from YAML, validates them, and
   145  // parses them into a map of limits keyed by 'Name'.
   146  func loadAndParseDefaultLimits(path string) (limits, error) {
   147  	fromFile, err := loadLimits(path)
   148  	if err != nil {
   149  		return nil, err
   150  	}
   151  	parsed := make(limits, len(fromFile))
   152  
   153  	for k, v := range fromFile {
   154  		err := validateLimit(v)
   155  		if err != nil {
   156  			return nil, fmt.Errorf("parsing default limit %q: %w", k, err)
   157  		}
   158  		name, ok := stringToName[k]
   159  		if !ok {
   160  			return nil, fmt.Errorf("unrecognized name %q in default limit, must be one of %v", k, limitNames)
   161  		}
   162  		parsed[nameToEnumString(name)] = precomputeLimit(v)
   163  	}
   164  	return parsed, nil
   165  }
   166  

View as plain text