This commit is contained in:
ponzischeme89
2026-08-28 23:00:02 +12:00
parent 3e89036f7b
commit d5632e844a
66 changed files with 2870 additions and 689 deletions
+72
View File
@@ -4,6 +4,7 @@ package config
import (
"encoding/json"
"fmt"
"net/netip"
"os"
"strconv"
"strings"
@@ -85,6 +86,17 @@ type Config struct {
UpstreamTimeout time.Duration
// TrustedProxies are the reverse proxies whose X-Forwarded-For and X-Real-IP
// headers the gateway will believe when working out where a request originated.
// A forwarded header is only honoured when the immediate peer is one of these, so
// a client reaching the gateway directly cannot spoof its address by setting one.
//
// Empty (MEMBY_TRUSTED_PROXIES unset) trusts loopback and the private/unique-local
// ranges — every reverse proxy a home deployment puts in front of Memby sits in one
// of those. Set it to a comma-separated list of addresses or CIDR ranges to narrow
// or widen that, or to "none" to trust no proxy at all.
TrustedProxies []netip.Prefix
// SlowRequestThreshold is how long a request has to take before its log line
// carries a stage breakdown. Fast requests deliberately carry none: it is the one
// field on the line that varies in width, and a column of them on every /v1/status
@@ -225,6 +237,10 @@ func Load() (Config, error) {
if err != nil {
return Config{}, err
}
trusted, err := trustedProxies(os.Getenv("MEMBY_TRUSTED_PROXIES"))
if err != nil {
return Config{}, fmt.Errorf("MEMBY_TRUSTED_PROXIES: %w", err)
}
c := Config{
ListenAddr: env("MEMBY_LISTEN_ADDR", ":8080"),
EmbyURL: strings.TrimRight(os.Getenv("MEMBY_EMBY_URL"), "/"),
@@ -261,6 +277,7 @@ func Load() (Config, error) {
SyncAPIKey: strings.TrimSpace(os.Getenv("MEMBY_SYNC_API_KEY")),
AnalyticsRetention: duration("MEMBY_ANALYTICS_RETENTION", 90*24*time.Hour),
UpstreamTimeout: duration("MEMBY_UPSTREAM_TIMEOUT", 20*time.Second),
TrustedProxies: trusted,
SlowRequestThreshold: duration("MEMBY_SLOW_REQUEST_THRESHOLD", 500*time.Millisecond),
EmbyHealthInterval: duration("MEMBY_EMBY_HEALTH_INTERVAL", 60*time.Second),
SonarrURL: strings.TrimRight(strings.TrimSpace(os.Getenv("MEMBY_SONARR_URL")), "/"),
@@ -408,6 +425,61 @@ func duration(key string, fallback time.Duration) time.Duration {
return fallback
}
// DefaultTrustedProxyRanges is what MEMBY_TRUSTED_PROXIES falls back to: loopback and
// the private and unique-local ranges. It is exported so the request layer can use the
// same set when it is handed a nil list.
func DefaultTrustedProxyRanges() []netip.Prefix {
return parsePrefixes(
"127.0.0.0/8", "::1/128",
"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"169.254.0.0/16", "fe80::/10", "fc00::/7",
)
}
func parsePrefixes(values ...string) []netip.Prefix {
out := make([]netip.Prefix, 0, len(values))
for _, value := range values {
if prefix, err := netip.ParsePrefix(value); err == nil {
out = append(out, prefix.Masked())
}
}
return out
}
// trustedProxies reads the MEMBY_TRUSTED_PROXIES list. Blank means the defaults, the
// literal "none" means an empty (but non-nil) set, and every other token is an address
// or a CIDR range — with "private" as a shorthand for the default ranges.
func trustedProxies(raw string) ([]netip.Prefix, error) {
raw = strings.TrimSpace(raw)
switch {
case raw == "":
return DefaultTrustedProxyRanges(), nil
case strings.EqualFold(raw, "none"):
return []netip.Prefix{}, nil
}
out := []netip.Prefix{}
for _, token := range strings.Split(raw, ",") {
token = strings.TrimSpace(token)
if token == "" {
continue
}
if strings.EqualFold(token, "private") {
out = append(out, DefaultTrustedProxyRanges()...)
continue
}
if prefix, err := netip.ParsePrefix(token); err == nil {
out = append(out, prefix.Masked())
continue
}
addr, err := netip.ParseAddr(token)
if err != nil {
return nil, fmt.Errorf("%q is not an IP address or CIDR range", token)
}
out = append(out, netip.PrefixFrom(addr, addr.BitLen()))
}
return out, nil
}
func integer(key string, fallback int) int {
raw := strings.TrimSpace(os.Getenv(key))
if raw == "" {