436 lines
16 KiB
Go
436 lines
16 KiB
Go
package ratelimits
|
|
|
|
import (
|
|
"net/netip"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/letsencrypt/boulder/config"
|
|
"github.com/letsencrypt/boulder/identifier"
|
|
"github.com/letsencrypt/boulder/test"
|
|
)
|
|
|
|
// loadAndParseDefaultLimits is a helper that calls both loadDefaults and
|
|
// parseDefaultLimits to handle a YAML file.
|
|
//
|
|
// TODO(#7901): Update the tests to test these functions individually.
|
|
func loadAndParseDefaultLimits(path string) (Limits, error) {
|
|
fromFile, err := loadDefaults(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return parseDefaultLimits(fromFile)
|
|
}
|
|
|
|
// loadAndParseOverrideLimits is a helper that calls both loadOverrides and
|
|
// parseOverrideLimits to handle a YAML file.
|
|
//
|
|
// TODO(#7901): Update the tests to test these functions individually.
|
|
func loadAndParseOverrideLimits(path string) (Limits, error) {
|
|
fromFile, err := loadOverrides(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return parseOverrideLimits(fromFile)
|
|
}
|
|
|
|
func TestParseOverrideNameId(t *testing.T) {
|
|
// 'enum:ipv4'
|
|
// Valid IPv4 address.
|
|
name, id, err := parseOverrideNameId(NewRegistrationsPerIPAddress.String() + ":10.0.0.1")
|
|
test.AssertNotError(t, err, "should not error")
|
|
test.AssertEquals(t, name, NewRegistrationsPerIPAddress)
|
|
test.AssertEquals(t, id, "10.0.0.1")
|
|
|
|
// 'enum:ipv6range'
|
|
// Valid IPv6 address range.
|
|
name, id, err = parseOverrideNameId(NewRegistrationsPerIPv6Range.String() + ":2602:80a:6000::/48")
|
|
test.AssertNotError(t, err, "should not error")
|
|
test.AssertEquals(t, name, NewRegistrationsPerIPv6Range)
|
|
test.AssertEquals(t, id, "2602:80a:6000::/48")
|
|
|
|
// Missing colon (this should never happen but we should avoid panicking).
|
|
_, _, err = parseOverrideNameId(NewRegistrationsPerIPAddress.String() + "10.0.0.1")
|
|
test.AssertError(t, err, "missing colon")
|
|
|
|
// Empty string.
|
|
_, _, err = parseOverrideNameId("")
|
|
test.AssertError(t, err, "empty string")
|
|
|
|
// Only a colon.
|
|
_, _, err = parseOverrideNameId(NewRegistrationsPerIPAddress.String() + ":")
|
|
test.AssertError(t, err, "only a colon")
|
|
|
|
// Invalid enum.
|
|
_, _, err = parseOverrideNameId("lol:noexist")
|
|
test.AssertError(t, err, "invalid enum")
|
|
}
|
|
|
|
func TestParseOverrideNameEnumId(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
input string
|
|
wantName Name
|
|
wantId string
|
|
expectError bool
|
|
desc string
|
|
}{
|
|
{
|
|
input: NewRegistrationsPerIPAddress.EnumString() + ":10.0.0.1",
|
|
wantName: NewRegistrationsPerIPAddress,
|
|
wantId: "10.0.0.1",
|
|
expectError: false,
|
|
desc: "valid IPv4 address",
|
|
},
|
|
{
|
|
input: NewRegistrationsPerIPv6Range.EnumString() + ":2001:0db8:0000::/48",
|
|
wantName: NewRegistrationsPerIPv6Range,
|
|
wantId: "2001:0db8:0000::/48",
|
|
expectError: false,
|
|
desc: "valid IPv6 address range",
|
|
},
|
|
{
|
|
input: NewRegistrationsPerIPAddress.EnumString() + "10.0.0.1",
|
|
expectError: true,
|
|
desc: "missing colon",
|
|
},
|
|
{
|
|
input: "",
|
|
expectError: true,
|
|
desc: "empty string",
|
|
},
|
|
{
|
|
input: NewRegistrationsPerIPAddress.EnumString() + ":",
|
|
expectError: true,
|
|
desc: "only a colon",
|
|
},
|
|
{
|
|
input: "lol:noexist",
|
|
expectError: true,
|
|
desc: "invalid enum",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
name, id, err := parseOverrideNameEnumId(tc.input)
|
|
if tc.expectError {
|
|
test.AssertError(t, err, tc.desc)
|
|
} else {
|
|
test.AssertNotError(t, err, tc.desc)
|
|
test.AssertEquals(t, name, tc.wantName)
|
|
test.AssertEquals(t, id, tc.wantId)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateLimit(t *testing.T) {
|
|
err := ValidateLimit(&Limit{Burst: 1, Count: 1, Period: config.Duration{Duration: time.Second}})
|
|
test.AssertNotError(t, err, "valid limit")
|
|
|
|
// All of the following are invalid.
|
|
for _, l := range []*Limit{
|
|
{Burst: 0, Count: 1, Period: config.Duration{Duration: time.Second}},
|
|
{Burst: 1, Count: 0, Period: config.Duration{Duration: time.Second}},
|
|
{Burst: 1, Count: 1, Period: config.Duration{Duration: 0}},
|
|
} {
|
|
err = ValidateLimit(l)
|
|
test.AssertError(t, err, "limit should be invalid")
|
|
}
|
|
}
|
|
|
|
func TestLoadAndParseOverrideLimits(t *testing.T) {
|
|
// Load a single valid override limit with Id formatted as 'enum:RegId'.
|
|
l, err := loadAndParseOverrideLimits("testdata/working_override.yml")
|
|
test.AssertNotError(t, err, "valid single override limit")
|
|
expectKey := joinWithColon(NewRegistrationsPerIPAddress.EnumString(), "64.112.117.1")
|
|
test.AssertEquals(t, l[expectKey].Burst, int64(40))
|
|
test.AssertEquals(t, l[expectKey].Count, int64(40))
|
|
test.AssertEquals(t, l[expectKey].Period.Duration, time.Second)
|
|
|
|
// Load single valid override limit with a 'domainOrCIDR' Id.
|
|
l, err = loadAndParseOverrideLimits("testdata/working_override_regid_domainorcidr.yml")
|
|
test.AssertNotError(t, err, "valid single override limit with Id of regId:domainOrCIDR")
|
|
expectKey = joinWithColon(CertificatesPerDomain.EnumString(), "example.com")
|
|
test.AssertEquals(t, l[expectKey].Burst, int64(40))
|
|
test.AssertEquals(t, l[expectKey].Count, int64(40))
|
|
test.AssertEquals(t, l[expectKey].Period.Duration, time.Second)
|
|
|
|
// Load multiple valid override limits with 'regId' Ids.
|
|
l, err = loadAndParseOverrideLimits("testdata/working_overrides.yml")
|
|
test.AssertNotError(t, err, "multiple valid override limits")
|
|
expectKey1 := joinWithColon(NewRegistrationsPerIPAddress.EnumString(), "64.112.117.1")
|
|
test.AssertEquals(t, l[expectKey1].Burst, int64(40))
|
|
test.AssertEquals(t, l[expectKey1].Count, int64(40))
|
|
test.AssertEquals(t, l[expectKey1].Period.Duration, time.Second)
|
|
expectKey2 := joinWithColon(NewRegistrationsPerIPv6Range.EnumString(), "2602:80a:6000::/48")
|
|
test.AssertEquals(t, l[expectKey2].Burst, int64(50))
|
|
test.AssertEquals(t, l[expectKey2].Count, int64(50))
|
|
test.AssertEquals(t, l[expectKey2].Period.Duration, time.Second*2)
|
|
|
|
// Load multiple valid override limits with 'fqdnSet' Ids, as follows:
|
|
// - CertificatesPerFQDNSet:example.com
|
|
// - CertificatesPerFQDNSet:example.com,example.net
|
|
// - CertificatesPerFQDNSet:example.com,example.net,example.org
|
|
entryKey1 := newFQDNSetBucketKey(CertificatesPerFQDNSet, identifier.NewDNSSlice([]string{"example.com"}))
|
|
entryKey2 := newFQDNSetBucketKey(CertificatesPerFQDNSet, identifier.NewDNSSlice([]string{"example.com", "example.net"}))
|
|
entryKey3 := newFQDNSetBucketKey(CertificatesPerFQDNSet, identifier.NewDNSSlice([]string{"example.com", "example.net", "example.org"}))
|
|
entryKey4 := newFQDNSetBucketKey(CertificatesPerFQDNSet, identifier.ACMEIdentifiers{
|
|
identifier.NewIP(netip.MustParseAddr("2602:80a:6000::1")),
|
|
identifier.NewIP(netip.MustParseAddr("9.9.9.9")),
|
|
identifier.NewDNS("example.com"),
|
|
})
|
|
|
|
l, err = loadAndParseOverrideLimits("testdata/working_overrides_regid_fqdnset.yml")
|
|
test.AssertNotError(t, err, "multiple valid override limits with 'fqdnSet' Ids")
|
|
test.AssertEquals(t, l[entryKey1].Burst, int64(40))
|
|
test.AssertEquals(t, l[entryKey1].Count, int64(40))
|
|
test.AssertEquals(t, l[entryKey1].Period.Duration, time.Second)
|
|
test.AssertEquals(t, l[entryKey2].Burst, int64(50))
|
|
test.AssertEquals(t, l[entryKey2].Count, int64(50))
|
|
test.AssertEquals(t, l[entryKey2].Period.Duration, time.Second*2)
|
|
test.AssertEquals(t, l[entryKey3].Burst, int64(60))
|
|
test.AssertEquals(t, l[entryKey3].Count, int64(60))
|
|
test.AssertEquals(t, l[entryKey3].Period.Duration, time.Second*3)
|
|
test.AssertEquals(t, l[entryKey4].Burst, int64(60))
|
|
test.AssertEquals(t, l[entryKey4].Count, int64(60))
|
|
test.AssertEquals(t, l[entryKey4].Period.Duration, time.Second*4)
|
|
|
|
// Path is empty string.
|
|
_, err = loadAndParseOverrideLimits("")
|
|
test.AssertError(t, err, "path is empty string")
|
|
test.Assert(t, os.IsNotExist(err), "path is empty string")
|
|
|
|
// Path to file which does not exist.
|
|
_, err = loadAndParseOverrideLimits("testdata/file_does_not_exist.yml")
|
|
test.AssertError(t, err, "a file that does not exist ")
|
|
test.Assert(t, os.IsNotExist(err), "test file should not exist")
|
|
|
|
// Burst cannot be 0.
|
|
_, err = loadAndParseOverrideLimits("testdata/busted_override_burst_0.yml")
|
|
test.AssertError(t, err, "single override limit with burst=0")
|
|
test.AssertContains(t, err.Error(), "invalid burst")
|
|
|
|
// Id cannot be empty.
|
|
_, err = loadAndParseOverrideLimits("testdata/busted_override_empty_id.yml")
|
|
test.AssertError(t, err, "single override limit with empty id")
|
|
test.Assert(t, !os.IsNotExist(err), "test file should exist")
|
|
|
|
// Name cannot be empty.
|
|
_, err = loadAndParseOverrideLimits("testdata/busted_override_empty_name.yml")
|
|
test.AssertError(t, err, "single override limit with empty name")
|
|
test.Assert(t, !os.IsNotExist(err), "test file should exist")
|
|
|
|
// Name must be a string representation of a valid Name enumeration.
|
|
_, err = loadAndParseOverrideLimits("testdata/busted_override_invalid_name.yml")
|
|
test.AssertError(t, err, "single override limit with invalid name")
|
|
test.Assert(t, !os.IsNotExist(err), "test file should exist")
|
|
|
|
// Multiple entries, second entry has a bad name.
|
|
_, err = loadAndParseOverrideLimits("testdata/busted_overrides_second_entry_bad_name.yml")
|
|
test.AssertError(t, err, "multiple override limits, second entry is bad")
|
|
test.Assert(t, !os.IsNotExist(err), "test file should exist")
|
|
|
|
// Multiple entries, third entry has id of "lol", instead of an IPv4 address.
|
|
_, err = loadAndParseOverrideLimits("testdata/busted_overrides_third_entry_bad_id.yml")
|
|
test.AssertError(t, err, "multiple override limits, third entry has bad Id value")
|
|
test.Assert(t, !os.IsNotExist(err), "test file should exist")
|
|
}
|
|
|
|
func TestLoadAndParseDefaultLimits(t *testing.T) {
|
|
// Load a single valid default limit.
|
|
l, err := loadAndParseDefaultLimits("testdata/working_default.yml")
|
|
test.AssertNotError(t, err, "valid single default limit")
|
|
test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Burst, int64(20))
|
|
test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Count, int64(20))
|
|
test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Period.Duration, time.Second)
|
|
|
|
// Load multiple valid default limits.
|
|
l, err = loadAndParseDefaultLimits("testdata/working_defaults.yml")
|
|
test.AssertNotError(t, err, "multiple valid default limits")
|
|
test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Burst, int64(20))
|
|
test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Count, int64(20))
|
|
test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Period.Duration, time.Second)
|
|
test.AssertEquals(t, l[NewRegistrationsPerIPv6Range.EnumString()].Burst, int64(30))
|
|
test.AssertEquals(t, l[NewRegistrationsPerIPv6Range.EnumString()].Count, int64(30))
|
|
test.AssertEquals(t, l[NewRegistrationsPerIPv6Range.EnumString()].Period.Duration, time.Second*2)
|
|
|
|
// Path is empty string.
|
|
_, err = loadAndParseDefaultLimits("")
|
|
test.AssertError(t, err, "path is empty string")
|
|
test.Assert(t, os.IsNotExist(err), "path is empty string")
|
|
|
|
// Path to file which does not exist.
|
|
_, err = loadAndParseDefaultLimits("testdata/file_does_not_exist.yml")
|
|
test.AssertError(t, err, "a file that does not exist")
|
|
test.Assert(t, os.IsNotExist(err), "test file should not exist")
|
|
|
|
// Burst cannot be 0.
|
|
_, err = loadAndParseDefaultLimits("testdata/busted_default_burst_0.yml")
|
|
test.AssertError(t, err, "single default limit with burst=0")
|
|
test.AssertContains(t, err.Error(), "invalid burst")
|
|
|
|
// Name cannot be empty.
|
|
_, err = loadAndParseDefaultLimits("testdata/busted_default_empty_name.yml")
|
|
test.AssertError(t, err, "single default limit with empty name")
|
|
test.Assert(t, !os.IsNotExist(err), "test file should exist")
|
|
|
|
// Name must be a string representation of a valid Name enumeration.
|
|
_, err = loadAndParseDefaultLimits("testdata/busted_default_invalid_name.yml")
|
|
test.AssertError(t, err, "single default limit with invalid name")
|
|
test.Assert(t, !os.IsNotExist(err), "test file should exist")
|
|
|
|
// Multiple entries, second entry has a bad name.
|
|
_, err = loadAndParseDefaultLimits("testdata/busted_defaults_second_entry_bad_name.yml")
|
|
test.AssertError(t, err, "multiple default limits, one is bad")
|
|
test.Assert(t, !os.IsNotExist(err), "test file should exist")
|
|
}
|
|
|
|
func TestLoadAndDumpOverrides(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
input := `
|
|
- CertificatesPerDomain:
|
|
burst: 5000
|
|
count: 5000
|
|
period: 168h0m0s
|
|
ids:
|
|
- id: example.com
|
|
comment: IN-10057
|
|
- id: example.net
|
|
comment: IN-10057
|
|
- CertificatesPerDomain:
|
|
burst: 300
|
|
count: 300
|
|
period: 168h0m0s
|
|
ids:
|
|
- id: example.org
|
|
comment: IN-10057
|
|
- CertificatesPerDomainPerAccount:
|
|
burst: 12000
|
|
count: 12000
|
|
period: 168h0m0s
|
|
ids:
|
|
- id: "123456789"
|
|
comment: Affluent (IN-8322)
|
|
- CertificatesPerDomainPerAccount:
|
|
burst: 6000
|
|
count: 6000
|
|
period: 168h0m0s
|
|
ids:
|
|
- id: "543219876"
|
|
comment: Affluent (IN-8322)
|
|
- id: "987654321"
|
|
comment: Affluent (IN-8322)
|
|
- CertificatesPerFQDNSet:
|
|
burst: 50
|
|
count: 50
|
|
period: 168h0m0s
|
|
ids:
|
|
- id: example.co.uk,example.cn
|
|
comment: IN-6843
|
|
- CertificatesPerFQDNSet:
|
|
burst: 24
|
|
count: 24
|
|
period: 168h0m0s
|
|
ids:
|
|
- id: example.org,example.com,example.net
|
|
comment: IN-6006
|
|
- FailedAuthorizationsPerDomainPerAccount:
|
|
burst: 250
|
|
count: 250
|
|
period: 1h0m0s
|
|
ids:
|
|
- id: "123456789"
|
|
comment: Digital Lake (IN-6736)
|
|
- FailedAuthorizationsPerDomainPerAccount:
|
|
burst: 50
|
|
count: 50
|
|
period: 1h0m0s
|
|
ids:
|
|
- id: "987654321"
|
|
comment: Digital Lake (IN-6856)
|
|
- FailedAuthorizationsPerDomainPerAccount:
|
|
burst: 10
|
|
count: 10
|
|
period: 1h0m0s
|
|
ids:
|
|
- id: "543219876"
|
|
comment: Big Mart (IN-6949)
|
|
- NewOrdersPerAccount:
|
|
burst: 3000
|
|
count: 3000
|
|
period: 3h0m0s
|
|
ids:
|
|
- id: "123456789"
|
|
comment: Galaxy Hoster (IN-8180)
|
|
- NewOrdersPerAccount:
|
|
burst: 1000
|
|
count: 1000
|
|
period: 3h0m0s
|
|
ids:
|
|
- id: "543219876"
|
|
comment: Big Mart (IN-8180)
|
|
- id: "987654321"
|
|
comment: Buy More (IN-10057)
|
|
- NewRegistrationsPerIPAddress:
|
|
burst: 100000
|
|
count: 100000
|
|
period: 3h0m0s
|
|
ids:
|
|
- id: 2600:1f1c:5e0:e702:ca06:d2a3:c7ce:a02e
|
|
comment: example.org IN-2395
|
|
- id: 55.66.77.88
|
|
comment: example.org IN-2395
|
|
- NewRegistrationsPerIPAddress:
|
|
burst: 200
|
|
count: 200
|
|
period: 3h0m0s
|
|
ids:
|
|
- id: 11.22.33.44
|
|
comment: example.net (IN-1583)`
|
|
|
|
expectCSV := `
|
|
name,id,count,burst,period,comment
|
|
CertificatesPerDomain,example.com,5000,5000,168h0m0s,IN-10057
|
|
CertificatesPerDomain,example.net,5000,5000,168h0m0s,IN-10057
|
|
CertificatesPerDomain,example.org,300,300,168h0m0s,IN-10057
|
|
CertificatesPerDomainPerAccount,123456789,12000,12000,168h0m0s,Affluent (IN-8322)
|
|
CertificatesPerDomainPerAccount,543219876,6000,6000,168h0m0s,Affluent (IN-8322)
|
|
CertificatesPerDomainPerAccount,987654321,6000,6000,168h0m0s,Affluent (IN-8322)
|
|
CertificatesPerFQDNSet,7c956936126b492845ddb48f4d220034509e7c0ad54ed2c1ba2650406846d9c3,50,50,168h0m0s,IN-6843
|
|
CertificatesPerFQDNSet,394e82811f52e2da38b970afdb21c9bc9af81060939c690183c00fce37408738,24,24,168h0m0s,IN-6006
|
|
FailedAuthorizationsPerDomainPerAccount,123456789,250,250,1h0m0s,Digital Lake (IN-6736)
|
|
FailedAuthorizationsPerDomainPerAccount,987654321,50,50,1h0m0s,Digital Lake (IN-6856)
|
|
FailedAuthorizationsPerDomainPerAccount,543219876,10,10,1h0m0s,Big Mart (IN-6949)
|
|
NewOrdersPerAccount,123456789,3000,3000,3h0m0s,Galaxy Hoster (IN-8180)
|
|
NewOrdersPerAccount,543219876,1000,1000,3h0m0s,Big Mart (IN-8180)
|
|
NewOrdersPerAccount,987654321,1000,1000,3h0m0s,Buy More (IN-10057)
|
|
NewRegistrationsPerIPAddress,2600:1f1c:5e0:e702:ca06:d2a3:c7ce:a02e,100000,100000,3h0m0s,example.org IN-2395
|
|
NewRegistrationsPerIPAddress,55.66.77.88,100000,100000,3h0m0s,example.org IN-2395
|
|
NewRegistrationsPerIPAddress,11.22.33.44,200,200,3h0m0s,example.net (IN-1583)
|
|
`
|
|
|
|
tempFile := filepath.Join(t.TempDir(), "overrides.yaml")
|
|
|
|
err := os.WriteFile(tempFile, []byte(input), 0644)
|
|
test.AssertNotError(t, err, "writing temp overrides.yaml")
|
|
|
|
original, err := LoadOverridesByBucketKey(tempFile)
|
|
test.AssertNotError(t, err, "loading overrides")
|
|
test.Assert(t, len(original) > 0, "expected at least one override loaded")
|
|
|
|
dumpFile := filepath.Join(t.TempDir(), "dumped.yaml")
|
|
err = DumpOverrides(dumpFile, original)
|
|
test.AssertNotError(t, err, "dumping overrides")
|
|
|
|
dumped, err := os.ReadFile(dumpFile)
|
|
test.AssertNotError(t, err, "reading dumped overrides file")
|
|
test.AssertEquals(t, strings.TrimLeft(string(dumped), "\n"), strings.TrimLeft(expectCSV, "\n"))
|
|
}
|