Add the four settings the README documented but pixa lacked (closes #61) #142
@@ -249,8 +249,9 @@ Key settings in more detail:
|
|||||||
|
|
||||||
- `access_control_allow_origin` — the origin a browser lets read pixa's
|
- `access_control_allow_origin` — the origin a browser lets read pixa's
|
||||||
responses, sent as the CORS `Access-Control-Allow-Origin` header: `*`, the
|
responses, sent as the CORS `Access-Control-Allow-Origin` header: `*`, the
|
||||||
default, is any site; otherwise one origin, scheme and host only, such as
|
default, is any site; otherwise one origin: scheme, host and optional port,
|
||||||
`https://example.com`. Anything else aborts startup
|
exactly as the browser sends it, such as `https://example.com`. Anything
|
||||||
|
else aborts startup
|
||||||
- `allowlist_hosts` — list of allowed upstream hosts
|
- `allowlist_hosts` — list of allowed upstream hosts
|
||||||
- `blocked_networks` — list of CIDR ranges to refuse for SSRF protection,
|
- `blocked_networks` — list of CIDR ranges to refuse for SSRF protection,
|
||||||
added to the always-enforced built-in ranges (loopback, private,
|
added to the always-enforced built-in ranges (loopback, private,
|
||||||
|
|||||||
@@ -65,9 +65,9 @@ exhaustion
|
|||||||
server's write timeout and the per-request timeout); each has a
|
server's write timeout and the per-request timeout); each has a
|
||||||
`PIXA_` variable; durations are positive Go duration strings, the size a
|
`PIXA_` variable; durations are positive Go duration strings, the size a
|
||||||
whole number of bytes up to 1 GiB, the origin `*` or one scheme, host
|
whole number of bytes up to 1 GiB, the origin `*` or one scheme, host
|
||||||
name or IP address and optional port; an invalid value aborts startup
|
and optional port, exactly as the browser sends it; an invalid value
|
||||||
naming the key and the value; documented in `config.example.yml` and
|
aborts startup naming the key and the value; documented in
|
||||||
`README.md`.
|
`config.example.yml` and `README.md`.
|
||||||
- 2026-09-28 cache stats report real numbers (closes #56): `Cache.Stats`
|
- 2026-09-28 cache stats report real numbers (closes #56): `Cache.Stats`
|
||||||
counts the cached source images and processed variants (`source_content`
|
counts the cached source images and processed variants (`source_content`
|
||||||
plus `variant_content`) and takes their size from `Cache.UsageBytes`,
|
plus `variant_content`) and takes their size from `Cache.UsageBytes`,
|
||||||
|
|||||||
+2
-1
@@ -84,7 +84,8 @@ downstream_timeout: 60s
|
|||||||
|
|
||||||
# The origin a browser lets read pixa's responses, sent as the CORS
|
# The origin a browser lets read pixa's responses, sent as the CORS
|
||||||
# Access-Control-Allow-Origin header: "*" (the default) is any site;
|
# Access-Control-Allow-Origin header: "*" (the default) is any site;
|
||||||
# otherwise one origin, scheme and host only, such as https://example.com
|
# otherwise one origin: scheme, host and optional port, exactly as the
|
||||||
|
# browser sends it, such as https://example.com
|
||||||
access_control_allow_origin: "*"
|
access_control_allow_origin: "*"
|
||||||
|
|
||||||
# Maximum disk cache size in bytes. Explicit values are used exactly as
|
# Maximum disk cache size in bytes. Explicit values are used exactly as
|
||||||
|
|||||||
+35
-35
@@ -624,13 +624,9 @@ func (c *Config) validateUpstreamMaxResponseSize() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateAccessControlAllowOrigin checks that access_control_allow_origin
|
// validateAccessControlAllowOrigin accepts "*" or an origin exactly as a browser
|
||||||
// is "*" or one origin as browsers send it in the Origin header: a scheme,
|
// sends it: http or https, an IP address as netip writes it or a lowercase name
|
||||||
// a host name or IP address, and optionally a port from 1 to 65535, with
|
// with a letter in its last part, and an optional port 1-65535, not the default.
|
||||||
// nothing after them. Anything else, such as a bare hostname or a trailing
|
|
||||||
// slash, would match no request. A "*" is not allowed in a host name, so
|
|
||||||
// https://*example.com is refused; the CORS middleware would read that
|
|
||||||
// "*" as a pattern and let other sites read responses.
|
|
||||||
func (c *Config) validateAccessControlAllowOrigin() error {
|
func (c *Config) validateAccessControlAllowOrigin() error {
|
||||||
origin := c.AccessControlAllowOrigin
|
origin := c.AccessControlAllowOrigin
|
||||||
if origin == "*" {
|
if origin == "*" {
|
||||||
@@ -640,50 +636,54 @@ func (c *Config) validateAccessControlAllowOrigin() error {
|
|||||||
errOrigin := fmt.Errorf("%s: value %q is %w",
|
errOrigin := fmt.Errorf("%s: value %q is %w",
|
||||||
settingName(keyAccessControlAllowOrigin), origin, errNotAnOrigin)
|
settingName(keyAccessControlAllowOrigin), origin, errNotAnOrigin)
|
||||||
|
|
||||||
// url.Parse accepts an empty port, as in https://example.com:, and
|
|
||||||
// then reports no port, so a trailing colon is refused here.
|
|
||||||
parsed, err := url.Parse(origin)
|
parsed, err := url.Parse(origin)
|
||||||
if err != nil || parsed.Scheme+"://"+parsed.Host != origin ||
|
if err != nil {
|
||||||
strings.HasSuffix(origin, ":") {
|
|
||||||
return errOrigin
|
return errOrigin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defaultPort := map[string]string{"http": "80", "https": "443"}[parsed.Scheme]
|
||||||
|
if defaultPort == "" {
|
||||||
|
return errOrigin
|
||||||
|
}
|
||||||
|
|
||||||
|
const letters = "abcdefghijklmnopqrstuvwxyz"
|
||||||
|
|
||||||
host := parsed.Hostname()
|
host := parsed.Hostname()
|
||||||
|
lastPart := host[strings.LastIndex(host, ".")+1:]
|
||||||
|
|
||||||
_, err = netip.ParseAddr(host)
|
addr, err := netip.ParseAddr(host)
|
||||||
if err != nil && !isHostName(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
|
return errOrigin
|
||||||
}
|
}
|
||||||
|
|
||||||
// A port is a 16-bit number, and 0 is not a port a browser sends.
|
// The value must be exactly the origin rebuilt from its parts.
|
||||||
if parsed.Port() != "" {
|
rebuilt := parsed.Scheme + "://" + host
|
||||||
port, err := strconv.ParseUint(parsed.Port(), 10, 16)
|
|
||||||
if err != nil || port == 0 {
|
port := parsed.Port()
|
||||||
|
if port != "" {
|
||||||
|
_, err := strconv.ParseUint(port, 10, 16)
|
||||||
|
if err != nil || port[0] == '0' || port == defaultPort {
|
||||||
return errOrigin
|
return errOrigin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rebuilt += ":" + port
|
||||||
|
}
|
||||||
|
|
||||||
|
if rebuilt != origin {
|
||||||
|
return errOrigin
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isHostName reports whether host is not empty and has only ASCII
|
|
||||||
// letters, digits, hyphens and dots.
|
|
||||||
func isHostName(host string) bool {
|
|
||||||
if host == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, r := range host {
|
|
||||||
isLetterOrDigit := 'a' <= r && r <= 'z' || 'A' <= r && r <= 'Z' ||
|
|
||||||
'0' <= r && r <= '9'
|
|
||||||
if !isLetterOrDigit && r != '-' && r != '.' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateAllowlistHost checks that an allowlist_hosts entry is a bare
|
// validateAllowlistHost checks that an allowlist_hosts entry is a bare
|
||||||
// hostname, optionally with a leading dot for suffix matching. URLs,
|
// hostname, optionally with a leading dot for suffix matching. URLs,
|
||||||
// paths, and whitespace indicate a misconfigured entry. An entry with
|
// paths, and whitespace indicate a misconfigured entry. An entry with
|
||||||
|
|||||||
Reference in New Issue
Block a user