|
|
|
@@ -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
|
|
|
|
@@ -86,6 +95,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 +125,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 +270,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 +392,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 +421,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 +578,12 @@ func (c *Config) validate() error {
|
|
|
|
|
settingName(keyCacheMaxBytes), c.CacheMaxBytes, errMustNotBeNegative)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if c.UpstreamMaxResponseSize <= 0 {
|
|
|
|
|
return fmt.Errorf("%s: value %d %w",
|
|
|
|
|
settingName(keyUpstreamMaxResponseSize), c.UpstreamMaxResponseSize,
|
|
|
|
|
errMustBePositive)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, host := range c.AllowlistHosts {
|
|
|
|
|
err := validateAllowlistHost(host)
|
|
|
|
|
if err != nil {
|
|
|
|
@@ -556,6 +605,25 @@ func (c *Config) validate() error {
|
|
|
|
|
errMustBeSetTogether)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return c.validateAccessControlAllowOrigin()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// validateAccessControlAllowOrigin checks that access_control_allow_origin
|
|
|
|
|
// is "*" or one origin, a scheme and host with nothing after them, as
|
|
|
|
|
// browsers send it in the Origin header. Anything else, such as a bare
|
|
|
|
|
// hostname or a trailing slash, would match no request.
|
|
|
|
|
func (c *Config) validateAccessControlAllowOrigin() error {
|
|
|
|
|
origin := c.AccessControlAllowOrigin
|
|
|
|
|
if origin == "*" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parsed, err := url.Parse(origin)
|
|
|
|
|
if err != nil || parsed.Host == "" || parsed.Scheme+"://"+parsed.Host != origin {
|
|
|
|
|
return fmt.Errorf("%s: value %q is %w",
|
|
|
|
|
settingName(keyAccessControlAllowOrigin), origin, errNotAnOrigin)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -673,6 +741,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 +875,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
|
|
|
|
|