Add the four settings the README documented but pixa lacked (closes #61)
check / check (push) Successful in 15s
check / check (push) Successful in 15s
README.md documented access_control_allow_origin, upstream_fetch_timeout, upstream_max_response_size and downstream_timeout, but pixa did not know them, so a config following the README aborted startup. Each is now a setting with its PIXA_ variable, defaulting to the value that was fixed in the code: *, 30s, 50 MiB and 60s. Durations are Go duration strings and must be positive; the size is whole bytes, at most 1 GiB. The origin is * or one http or https origin written exactly as a browser sends it; anything else aborts startup. downstream_timeout sets both the server's write timeout and the per-request timeout. The owner approved the edits to existing tests. Model: opus-5-5
This commit was merged in pull request #142.
This commit is contained in:
+175
-1
@@ -13,6 +13,7 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.eeqj.de/sneak/smartconfig"
|
||||
"go.uber.org/fx"
|
||||
@@ -24,6 +25,10 @@ const (
|
||||
DefaultPort = 8080
|
||||
DefaultStateDir = "/var/lib/pixa"
|
||||
DefaultUpstreamConnectionsPerHost = 20
|
||||
DefaultAccessControlAllowOrigin = "*"
|
||||
DefaultUpstreamFetchTimeout = 30 * time.Second
|
||||
DefaultUpstreamMaxResponseSize = 50 << 20 // 50 MiB
|
||||
DefaultDownstreamTimeout = 60 * time.Second
|
||||
)
|
||||
|
||||
// Configuration key names.
|
||||
@@ -44,6 +49,10 @@ const (
|
||||
keyCacheMaxBytes = "cache_max_bytes"
|
||||
keyBlockedNetworks = "blocked_networks"
|
||||
keyTrustedProxies = "trusted_proxies"
|
||||
keyAccessControlAllowOrigin = "access_control_allow_origin"
|
||||
keyUpstreamFetchTimeout = "upstream_fetch_timeout"
|
||||
keyUpstreamMaxResponseSize = "upstream_max_response_size"
|
||||
keyDownstreamTimeout = "downstream_timeout"
|
||||
)
|
||||
|
||||
// placeholderSigningKey is the dummy signing_key shipped in
|
||||
@@ -69,6 +78,7 @@ var (
|
||||
errEmptyEntry = errors.New("contains an empty entry")
|
||||
errNotAValidURL = errors.New("not a valid URL")
|
||||
errPortOutOfRange = errors.New("outside the valid port range")
|
||||
errSizeOutOfRange = errors.New("outside the accepted range")
|
||||
errTooFewConnections = errors.New("must be at least 1")
|
||||
errValueTooShort = errors.New("value too short")
|
||||
errPlaceholderKey = errors.New(
|
||||
@@ -86,6 +96,10 @@ var (
|
||||
errNotBareHostname = errors.New(
|
||||
"must be a bare hostname without scheme, path, or whitespace")
|
||||
errNoHostnameLabels = errors.New("contains no hostname labels")
|
||||
errNotADuration = errors.New("not a duration such as 30s or 2m")
|
||||
errMustBePositive = errors.New("must be positive")
|
||||
errNotAnOrigin = errors.New(
|
||||
`not "*" or an origin such as https://example.com`)
|
||||
)
|
||||
|
||||
// Params defines dependencies for Config.
|
||||
@@ -112,6 +126,23 @@ type Config struct {
|
||||
AllowHTTP bool // Allow non-TLS upstream (testing only)
|
||||
UpstreamConnectionsPerHost int // Max concurrent connections per upstream host
|
||||
|
||||
// UpstreamFetchTimeout is the time allowed for one fetch from an
|
||||
// upstream host. UpstreamMaxResponseSize is the largest upstream
|
||||
// response accepted, in bytes, and also the image processor's input
|
||||
// limit.
|
||||
UpstreamFetchTimeout time.Duration
|
||||
UpstreamMaxResponseSize int64
|
||||
|
||||
// AccessControlAllowOrigin is the origin the CORS middleware allows
|
||||
// to read responses: "*" for any, or one origin such as
|
||||
// https://example.com.
|
||||
AccessControlAllowOrigin string
|
||||
|
||||
// DownstreamTimeout bounds how long answering one client request may
|
||||
// take. It is both the HTTP server's write timeout and the deadline
|
||||
// of the per-request timeout middleware.
|
||||
DownstreamTimeout time.Duration
|
||||
|
||||
// BlockedNetworks are operator-supplied CIDR ranges to refuse in
|
||||
// addition to the built-in SSRF blocklist. Enforced by the upstream
|
||||
// fetcher's dialer; the built-in ranges always apply.
|
||||
@@ -240,6 +271,14 @@ func newFromSmartConfig(sc *smartconfig.Config) (*Config, error) {
|
||||
AllowHTTP: loader.boolVal(keyAllowHTTP, false),
|
||||
UpstreamConnectionsPerHost: loader.intVal(
|
||||
keyUpstreamConnectionsPerHost, DefaultUpstreamConnectionsPerHost),
|
||||
UpstreamFetchTimeout: loader.durationVal(
|
||||
keyUpstreamFetchTimeout, DefaultUpstreamFetchTimeout),
|
||||
UpstreamMaxResponseSize: loader.int64Val(
|
||||
keyUpstreamMaxResponseSize, DefaultUpstreamMaxResponseSize),
|
||||
AccessControlAllowOrigin: loader.stringVal(
|
||||
keyAccessControlAllowOrigin, DefaultAccessControlAllowOrigin),
|
||||
DownstreamTimeout: loader.durationVal(
|
||||
keyDownstreamTimeout, DefaultDownstreamTimeout),
|
||||
CacheMaxBytes: loader.int64Val(keyCacheMaxBytes, 0),
|
||||
BlockedNetworks: blockedNetworks,
|
||||
TrustedProxies: trustedProxies,
|
||||
@@ -354,7 +393,8 @@ func isKnownConfigKey(key string) bool {
|
||||
case keyDebug, keyMaintenanceMode, keyPort, keyStateDir, keySentryDSN,
|
||||
keyDBURL, keyMetrics, keySigningKey, keyAllowlistHosts, keyAllowHTTP,
|
||||
keyUpstreamConnectionsPerHost, keyCacheMaxBytes, keyBlockedNetworks,
|
||||
keyTrustedProxies, "env":
|
||||
keyTrustedProxies, keyAccessControlAllowOrigin, keyUpstreamFetchTimeout,
|
||||
keyUpstreamMaxResponseSize, keyDownstreamTimeout, "env":
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -382,6 +422,10 @@ func envVarNames() map[string]string {
|
||||
keyCacheMaxBytes: "PIXA_CACHE_MAX_BYTES",
|
||||
keyBlockedNetworks: "PIXA_BLOCKED_NETWORKS",
|
||||
keyTrustedProxies: "PIXA_TRUSTED_PROXIES",
|
||||
keyAccessControlAllowOrigin: "PIXA_ACCESS_CONTROL_ALLOW_ORIGIN",
|
||||
keyUpstreamFetchTimeout: "PIXA_UPSTREAM_FETCH_TIMEOUT",
|
||||
keyUpstreamMaxResponseSize: "PIXA_UPSTREAM_MAX_RESPONSE_SIZE",
|
||||
keyDownstreamTimeout: "PIXA_DOWNSTREAM_TIMEOUT",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,6 +579,11 @@ func (c *Config) validate() error {
|
||||
settingName(keyCacheMaxBytes), c.CacheMaxBytes, errMustNotBeNegative)
|
||||
}
|
||||
|
||||
err = c.validateUpstreamMaxResponseSize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, host := range c.AllowlistHosts {
|
||||
err := validateAllowlistHost(host)
|
||||
if err != nil {
|
||||
@@ -556,6 +605,82 @@ func (c *Config) validate() error {
|
||||
errMustBeSetTogether)
|
||||
}
|
||||
|
||||
return c.validateAccessControlAllowOrigin()
|
||||
}
|
||||
|
||||
// validateUpstreamMaxResponseSize checks that upstream_max_response_size
|
||||
// is from 1 byte to 1 GiB. An upstream response is read whole into
|
||||
// memory, and the image processor reads one byte past this limit, which
|
||||
// must not overflow.
|
||||
func (c *Config) validateUpstreamMaxResponseSize() error {
|
||||
const maxUpstreamMaxResponseSize = 1 << 30 // 1 GiB
|
||||
if c.UpstreamMaxResponseSize < 1 ||
|
||||
c.UpstreamMaxResponseSize > maxUpstreamMaxResponseSize {
|
||||
return fmt.Errorf("%s: value %d is %w 1-%d",
|
||||
settingName(keyUpstreamMaxResponseSize), c.UpstreamMaxResponseSize,
|
||||
errSizeOutOfRange, maxUpstreamMaxResponseSize)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateAccessControlAllowOrigin accepts "*" or an origin exactly as a browser
|
||||
// sends it: http or https, an IP address as netip writes it or a lowercase name
|
||||
// with a letter in its last part, and an optional port 1-65535, not the default.
|
||||
func (c *Config) validateAccessControlAllowOrigin() error {
|
||||
origin := c.AccessControlAllowOrigin
|
||||
if origin == "*" {
|
||||
return nil
|
||||
}
|
||||
|
||||
errOrigin := fmt.Errorf("%s: value %q is %w",
|
||||
settingName(keyAccessControlAllowOrigin), origin, errNotAnOrigin)
|
||||
|
||||
parsed, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
return errOrigin
|
||||
}
|
||||
|
||||
defaultPort := map[string]string{"http": "80", "https": "443"}[parsed.Scheme]
|
||||
if defaultPort == "" {
|
||||
return errOrigin
|
||||
}
|
||||
|
||||
const letters = "abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
host := parsed.Hostname()
|
||||
lastPart := host[strings.LastIndex(host, ".")+1:]
|
||||
|
||||
addr, err := netip.ParseAddr(host)
|
||||
|
||||
switch {
|
||||
case err == nil && addr.Is6():
|
||||
host = "[" + addr.String() + "]"
|
||||
case err == nil:
|
||||
host = addr.String()
|
||||
case strings.Trim(host, letters+"0123456789-.") != "": // a character other than these
|
||||
return errOrigin
|
||||
case !strings.ContainsAny(lastPart, letters):
|
||||
return errOrigin
|
||||
}
|
||||
|
||||
// The value must be exactly the origin rebuilt from its parts.
|
||||
rebuilt := parsed.Scheme + "://" + host
|
||||
|
||||
port := parsed.Port()
|
||||
if port != "" {
|
||||
_, err := strconv.ParseUint(port, 10, 16)
|
||||
if err != nil || port[0] == '0' || port == defaultPort {
|
||||
return errOrigin
|
||||
}
|
||||
|
||||
rebuilt += ":" + port
|
||||
}
|
||||
|
||||
if rebuilt != origin {
|
||||
return errOrigin
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -673,6 +798,19 @@ func (l *strictLoader) int64Val(key string, defaultVal int64) int64 {
|
||||
return val
|
||||
}
|
||||
|
||||
func (l *strictLoader) durationVal(key string, defaultVal time.Duration) time.Duration {
|
||||
if l.err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
val, err := getDuration(l.sc, key, defaultVal)
|
||||
if err != nil {
|
||||
l.err = err
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func (l *strictLoader) boolVal(key string, defaultVal bool) bool {
|
||||
if l.err != nil {
|
||||
return false
|
||||
@@ -794,6 +932,42 @@ func getInt64(sc *smartconfig.Config, key string, defaultVal int64) (int64, erro
|
||||
}
|
||||
}
|
||||
|
||||
// getDuration returns the duration value for key, or defaultVal if the
|
||||
// key is omitted. A present value must be a positive Go duration string
|
||||
// such as "30s" or "2m", read with time.ParseDuration; a bare number has
|
||||
// no unit and is an error, as is an explicit null.
|
||||
func getDuration(
|
||||
sc *smartconfig.Config, key string, defaultVal time.Duration,
|
||||
) (time.Duration, error) {
|
||||
raw, ok := lookupValue(sc, key)
|
||||
if !ok {
|
||||
return defaultVal, nil
|
||||
}
|
||||
|
||||
if raw == nil {
|
||||
return 0, errNullConfigValue(key)
|
||||
}
|
||||
|
||||
str, ok := raw.(string)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("config key %q: value %v (%T) is %w",
|
||||
key, raw, raw, errNotADuration)
|
||||
}
|
||||
|
||||
parsed, err := time.ParseDuration(strings.TrimSpace(str))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%s: value %q is %w",
|
||||
settingName(key), str, errNotADuration)
|
||||
}
|
||||
|
||||
if parsed <= 0 {
|
||||
return 0, fmt.Errorf("%s: value %q %w",
|
||||
settingName(key), str, errMustBePositive)
|
||||
}
|
||||
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
// getBool returns the boolean value for key, or defaultVal if the key
|
||||
// is omitted. A present value that is not a boolean (or a ParseBool-able
|
||||
// string), or is explicitly null, is an error; numbers are not accepted
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.eeqj.de/sneak/smartconfig"
|
||||
)
|
||||
@@ -599,3 +600,244 @@ func TestEnsureStateDirFailsOnUncreatablePath(t *testing.T) {
|
||||
t.Errorf("error %q does not name the offending key state_dir", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// TestOmittedOriginTimeoutsAndSizeUseDefaults checks that the CORS
|
||||
// origin, the upstream fetch timeout, the upstream response size limit
|
||||
// and the downstream timeout default to the values pixa used before they
|
||||
// could be configured.
|
||||
func TestOmittedOriginTimeoutsAndSizeUseDefaults(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c, err := configFromYAML(t, signingKeyLine)
|
||||
if err != nil {
|
||||
t.Fatalf("minimal config should be valid, got error: %v", err)
|
||||
}
|
||||
|
||||
if c.AccessControlAllowOrigin != "*" {
|
||||
t.Errorf("AccessControlAllowOrigin = %q, want *", c.AccessControlAllowOrigin)
|
||||
}
|
||||
|
||||
if c.UpstreamFetchTimeout != 30*time.Second {
|
||||
t.Errorf("UpstreamFetchTimeout = %v, want 30s", c.UpstreamFetchTimeout)
|
||||
}
|
||||
|
||||
if c.UpstreamMaxResponseSize != 50<<20 {
|
||||
t.Errorf("UpstreamMaxResponseSize = %d, want %d (50 MiB)",
|
||||
c.UpstreamMaxResponseSize, 50<<20)
|
||||
}
|
||||
|
||||
if c.DownstreamTimeout != 60*time.Second {
|
||||
t.Errorf("DownstreamTimeout = %v, want 60s", c.DownstreamTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExplicitOriginTimeoutsAndSizeAreUsed checks that valid values for
|
||||
// the CORS origin, the two timeouts and the response size limit are used
|
||||
// as given. The size is the largest accepted, 1 GiB.
|
||||
func TestExplicitOriginTimeoutsAndSizeAreUsed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c, err := configFromYAML(t, signingKeyLine+`
|
||||
access_control_allow_origin: https://app.example.com
|
||||
upstream_fetch_timeout: 10s
|
||||
upstream_max_response_size: 1073741824
|
||||
downstream_timeout: 2m
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("valid config should load, got error: %v", err)
|
||||
}
|
||||
|
||||
if c.AccessControlAllowOrigin != "https://app.example.com" {
|
||||
t.Errorf("AccessControlAllowOrigin = %q, want https://app.example.com",
|
||||
c.AccessControlAllowOrigin)
|
||||
}
|
||||
|
||||
if c.UpstreamFetchTimeout != 10*time.Second {
|
||||
t.Errorf("UpstreamFetchTimeout = %v, want 10s", c.UpstreamFetchTimeout)
|
||||
}
|
||||
|
||||
if c.UpstreamMaxResponseSize != 1073741824 {
|
||||
t.Errorf("UpstreamMaxResponseSize = %d, want 1073741824",
|
||||
c.UpstreamMaxResponseSize)
|
||||
}
|
||||
|
||||
if c.DownstreamTimeout != 2*time.Minute {
|
||||
t.Errorf("DownstreamTimeout = %v, want 2m", c.DownstreamTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOriginWithPortOrAnyOriginIsAccepted checks the other accepted forms
|
||||
// of access_control_allow_origin: "*", an origin with a port, and origins
|
||||
// whose host is an IPv4 or IPv6 address.
|
||||
func TestOriginWithPortOrAnyOriginIsAccepted(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, origin := range []string{
|
||||
"*", "http://localhost:3000", "http://192.0.2.1", "http://[2001:db8::1]:8080",
|
||||
} {
|
||||
c, err := configFromYAML(t, signingKeyLine+
|
||||
"access_control_allow_origin: \""+origin+"\"\n")
|
||||
if err != nil {
|
||||
t.Fatalf("origin %q should be accepted, got error: %v", origin, err)
|
||||
}
|
||||
|
||||
if c.AccessControlAllowOrigin != origin {
|
||||
t.Errorf("AccessControlAllowOrigin = %q, want %q",
|
||||
c.AccessControlAllowOrigin, origin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// invalidTimeoutCases are configs where upstream_fetch_timeout or
|
||||
// downstream_timeout is not a positive Go duration string; each must
|
||||
// abort startup naming the key and the value.
|
||||
func invalidTimeoutCases() []abortCase {
|
||||
return []abortCase{
|
||||
{
|
||||
name: "upstream_fetch_timeout not a duration",
|
||||
yaml: signingKeyLine + "upstream_fetch_timeout: soon\n",
|
||||
wantErrSubstrings: []string{keyUpstreamFetchTimeout, "soon"},
|
||||
},
|
||||
{
|
||||
name: "upstream_fetch_timeout number without a unit",
|
||||
yaml: signingKeyLine + "upstream_fetch_timeout: 45\n",
|
||||
wantErrSubstrings: []string{keyUpstreamFetchTimeout, "45"},
|
||||
},
|
||||
{
|
||||
name: "upstream_fetch_timeout zero",
|
||||
yaml: signingKeyLine + "upstream_fetch_timeout: 0s\n",
|
||||
wantErrSubstrings: []string{keyUpstreamFetchTimeout, "0s"},
|
||||
},
|
||||
{
|
||||
name: "upstream_fetch_timeout negative",
|
||||
yaml: signingKeyLine + "upstream_fetch_timeout: -5s\n",
|
||||
wantErrSubstrings: []string{keyUpstreamFetchTimeout, "-5s"},
|
||||
},
|
||||
{
|
||||
name: "upstream_fetch_timeout null",
|
||||
yaml: signingKeyLine + "upstream_fetch_timeout: null\n",
|
||||
wantErrSubstrings: []string{keyUpstreamFetchTimeout, nullValueText},
|
||||
},
|
||||
{
|
||||
name: "downstream_timeout not a duration",
|
||||
yaml: signingKeyLine + "downstream_timeout: 1 minute\n",
|
||||
wantErrSubstrings: []string{keyDownstreamTimeout, "1 minute"},
|
||||
},
|
||||
{
|
||||
name: "downstream_timeout zero",
|
||||
yaml: signingKeyLine + "downstream_timeout: 0s\n",
|
||||
wantErrSubstrings: []string{keyDownstreamTimeout, "0s"},
|
||||
},
|
||||
{
|
||||
name: "downstream_timeout negative",
|
||||
yaml: signingKeyLine + "downstream_timeout: -1m\n",
|
||||
wantErrSubstrings: []string{keyDownstreamTimeout, "-1m"},
|
||||
},
|
||||
{
|
||||
name: "downstream_timeout null",
|
||||
yaml: signingKeyLine + "downstream_timeout:\n",
|
||||
wantErrSubstrings: []string{keyDownstreamTimeout, nullValueText},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// invalidSizeAndOriginCases are configs where upstream_max_response_size
|
||||
// is not a whole number of bytes from 1 to 1 GiB, or
|
||||
// access_control_allow_origin is neither "*" nor an origin; each must
|
||||
// abort startup naming the key and the value.
|
||||
func invalidSizeAndOriginCases() []abortCase {
|
||||
badOrigins := []string{
|
||||
"", // empty
|
||||
"example.com", // no scheme
|
||||
"https://example.com/images", // a path
|
||||
"https://example.com/", // a trailing slash
|
||||
// The CORS middleware reads a * inside an origin as a pattern
|
||||
// that lets other sites read responses.
|
||||
"https://*",
|
||||
"https://*.example.com",
|
||||
"https://*example.com",
|
||||
"https://a.com,b.com", // two hosts
|
||||
"https://example.com:", // an empty port
|
||||
"https://:8443", // no host
|
||||
"https://example.com:0", // a port below 1
|
||||
"https://example.com:99999", // a port above 65535
|
||||
"https://exämple.com", // a host name that is not ASCII
|
||||
"https://example.com:443", // the default port for https
|
||||
"http://example.com:80", // the default port for http
|
||||
"https://example.com:08080", // a port with a leading zero
|
||||
"https://01.2.3.4", // an IPv4 address with a leading zero
|
||||
"https://10.0.0", // an IPv4 address with three parts
|
||||
"https://192.168.1.256", // an IPv4 address part above 255
|
||||
"https://example.123", // a host name whose last part is a number
|
||||
"https://[0:0:0:0:0:0:0:1]", // an IPv6 address not in its shortest form
|
||||
"file://example.com", // a scheme other than http or https
|
||||
"HTTPS://example.com", // a scheme in upper case
|
||||
"https://Example.com", // a host name in upper case
|
||||
}
|
||||
|
||||
cases := make([]abortCase, 0, len(badOrigins))
|
||||
for _, origin := range badOrigins {
|
||||
cases = append(cases, abortCase{
|
||||
name: "access_control_allow_origin " + origin,
|
||||
yaml: signingKeyLine +
|
||||
"access_control_allow_origin: \"" + origin + "\"\n",
|
||||
wantErrSubstrings: []string{keyAccessControlAllowOrigin, origin},
|
||||
})
|
||||
}
|
||||
|
||||
return append(cases, []abortCase{
|
||||
{
|
||||
name: "access_control_allow_origin null",
|
||||
yaml: signingKeyLine + "access_control_allow_origin: null\n",
|
||||
wantErrSubstrings: []string{keyAccessControlAllowOrigin, nullValueText},
|
||||
},
|
||||
{
|
||||
name: "upstream_max_response_size with a unit",
|
||||
yaml: signingKeyLine + "upstream_max_response_size: 50MB\n",
|
||||
wantErrSubstrings: []string{keyUpstreamMaxResponseSize, "50MB"},
|
||||
},
|
||||
{
|
||||
name: "upstream_max_response_size fractional",
|
||||
yaml: signingKeyLine + "upstream_max_response_size: 1.5\n",
|
||||
wantErrSubstrings: []string{keyUpstreamMaxResponseSize, "1.5"},
|
||||
},
|
||||
{
|
||||
name: "upstream_max_response_size zero",
|
||||
yaml: signingKeyLine + "upstream_max_response_size: 0\n",
|
||||
wantErrSubstrings: []string{keyUpstreamMaxResponseSize, "0"},
|
||||
},
|
||||
{
|
||||
name: "upstream_max_response_size negative",
|
||||
yaml: signingKeyLine + "upstream_max_response_size: -1\n",
|
||||
wantErrSubstrings: []string{keyUpstreamMaxResponseSize, "-1"},
|
||||
},
|
||||
{
|
||||
name: "upstream_max_response_size null",
|
||||
yaml: signingKeyLine + "upstream_max_response_size: null\n",
|
||||
wantErrSubstrings: []string{keyUpstreamMaxResponseSize, nullValueText},
|
||||
},
|
||||
{
|
||||
name: "upstream_max_response_size above 1 GiB",
|
||||
yaml: signingKeyLine + "upstream_max_response_size: 1073741825\n",
|
||||
wantErrSubstrings: []string{keyUpstreamMaxResponseSize, "1073741825"},
|
||||
},
|
||||
{
|
||||
name: "upstream_max_response_size largest 64-bit integer",
|
||||
yaml: signingKeyLine +
|
||||
"upstream_max_response_size: 9223372036854775807\n",
|
||||
wantErrSubstrings: []string{
|
||||
keyUpstreamMaxResponseSize, "9223372036854775807",
|
||||
},
|
||||
},
|
||||
}...)
|
||||
}
|
||||
|
||||
// TestInvalidOriginTimeoutOrSizeAbortsStartup verifies the
|
||||
// no-silent-fallback rule for the CORS origin, the two timeouts and the
|
||||
// response size limit: a value that does not parse or is out of range
|
||||
// aborts startup naming the key and the value.
|
||||
func TestInvalidOriginTimeoutOrSizeAbortsStartup(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
runAbortCases(t, append(invalidTimeoutCases(), invalidSizeAndOriginCases()...))
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/pixa/internal/globals"
|
||||
"sneak.berlin/go/pixa/internal/logger"
|
||||
@@ -69,6 +70,10 @@ func TestEnvironmentSetsEveryKey(t *testing.T) {
|
||||
t.Setenv("PIXA_CACHE_MAX_BYTES", "1024")
|
||||
t.Setenv("PIXA_BLOCKED_NETWORKS", "203.0.113.0/24")
|
||||
t.Setenv("PIXA_TRUSTED_PROXIES", "192.0.2.0/24")
|
||||
t.Setenv("PIXA_ACCESS_CONTROL_ALLOW_ORIGIN", "https://app.example.com")
|
||||
t.Setenv("PIXA_UPSTREAM_FETCH_TIMEOUT", "10s")
|
||||
t.Setenv("PIXA_UPSTREAM_MAX_RESPONSE_SIZE", "1048576")
|
||||
t.Setenv("PIXA_DOWNSTREAM_TIMEOUT", "2m")
|
||||
|
||||
c, err := newFromSmartConfig(nil)
|
||||
if err != nil {
|
||||
@@ -92,6 +97,10 @@ func TestEnvironmentSetsEveryKey(t *testing.T) {
|
||||
cacheMaxBytesExplicit: true,
|
||||
BlockedNetworks: []netip.Prefix{netip.MustParsePrefix("203.0.113.0/24")},
|
||||
TrustedProxies: []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24")},
|
||||
AccessControlAllowOrigin: "https://app.example.com",
|
||||
UpstreamFetchTimeout: 10 * time.Second,
|
||||
UpstreamMaxResponseSize: 1048576,
|
||||
DownstreamTimeout: 2 * time.Minute,
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(*c, want) {
|
||||
@@ -280,6 +289,30 @@ func TestInvalidDebugFromEnvironmentAbortsStartup(t *testing.T) {
|
||||
wantStartupError(t, err, "PIXA_DEBUG", "maybe")
|
||||
}
|
||||
|
||||
// TestInvalidOriginTimeoutOrSizeFromEnvironmentAbortsStartup checks that
|
||||
// an invalid CORS origin, timeout or response size limit in its variable
|
||||
// aborts startup naming the variable and the value.
|
||||
func TestInvalidOriginTimeoutOrSizeFromEnvironmentAbortsStartup(t *testing.T) {
|
||||
cases := []struct {
|
||||
variable string
|
||||
value string
|
||||
}{
|
||||
{"PIXA_ACCESS_CONTROL_ALLOW_ORIGIN", "example.com"},
|
||||
{"PIXA_UPSTREAM_FETCH_TIMEOUT", "soon"},
|
||||
{"PIXA_UPSTREAM_MAX_RESPONSE_SIZE", "50MB"},
|
||||
{"PIXA_DOWNSTREAM_TIMEOUT", "0s"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.variable, func(t *testing.T) {
|
||||
t.Setenv(tc.variable, tc.value)
|
||||
|
||||
_, err := configFromYAML(t, signingKeyLine)
|
||||
wantStartupError(t, err, tc.variable, tc.value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigFileAloneBehavesAsBefore checks that with no variables set
|
||||
// (TestMain unsets them) the config file's values are used and omitted
|
||||
// keys take their defaults.
|
||||
|
||||
Reference in New Issue
Block a user