package config_test import ( "net/netip" "os" "path/filepath" "slices" "strings" "testing" "time" "sneak.berlin/go/smallwebwaf/internal/config" ) // rproxy's settings, by name, and their values in these tests. const ( wafBackend = "SWRP_WAF_BACKEND" rproxyTimeout = "SWRP_TIMEOUT_MS" rproxyDefault = "SWRP_DEFAULT_DISPOSITION" rproxyListen = "SWRP_LISTEN_ADDR" rproxyUpstream = "SWRP_UPSTREAM_URL" rproxyTrusted = "SWRP_TRUSTED_PROXIES" rproxyMaxRequest = "SWRP_MAX_REQUEST_SIZE" rproxyMaxResponse = "SWRP_MAX_RESPONSE_SIZE" rproxyRequestTime = "SWRP_MAX_REQUEST_TIME_MS" rproxyAnswerTime = "SWRP_MAX_RESPONSE_TIME_MS" backendURL = "http://servewaf:8080" rproxyDefaultAddr = ":8080" rproxyDefaultApp = "http://127.0.0.1:8081" notASize = "a lot" ) // rproxyConfig reads rproxy's settings from env. func rproxyConfig(env map[string]string) (*config.RProxy, error) { return config.RProxyFromEnvironment(func(name string) (string, bool) { value, ok := env[name] return value, ok }) } func TestRProxyDefaults(t *testing.T) { t.Parallel() cfg, err := rproxyConfig(map[string]string{wafBackend: backendURL}) if err != nil { t.Fatalf("settings: %v", err) } if cfg.ListenAddr != rproxyDefaultAddr || cfg.UpstreamURL.String() != rproxyDefaultApp || cfg.WAFBackend.String() != backendURL { t.Errorf("addresses %q, %v, %v", cfg.ListenAddr, cfg.UpstreamURL, cfg.WAFBackend) } if cfg.Timeout != 2*time.Second || cfg.DefaultDisposition != "allow" { t.Errorf("timeout %v, default disposition %q", cfg.Timeout, cfg.DefaultDisposition) } if cfg.MaxRequestSize != 100<<20 || cfg.MaxResponseSize != 1<<30 { t.Errorf("sizes %d and %d", cfg.MaxRequestSize, cfg.MaxResponseSize) } if cfg.MaxRequestTime != time.Minute || cfg.MaxResponseTime != 30*time.Minute { t.Errorf("times %v and %v", cfg.MaxRequestTime, cfg.MaxResponseTime) } want := []netip.Prefix{ netip.MustParsePrefix("10.0.0.0/8"), netip.MustParsePrefix("172.16.0.0/12"), netip.MustParsePrefix("192.168.0.0/16"), } if !slices.Equal(cfg.TrustedProxies, want) { t.Errorf("trusted proxies %v, want %v", cfg.TrustedProxies, want) } } func TestRProxySettingsAreRead(t *testing.T) { t.Parallel() cfg, err := rproxyConfig(map[string]string{ wafBackend: "https://waf.example:8443", rproxyListen: ":9000", rproxyUpstream: "http://app:3000", rproxyTrusted: "192.0.2.0/24", rproxyTimeout: "150", rproxyDefault: "deny", rproxyMaxRequest: "1K", rproxyMaxResponse: off, rproxyRequestTime: off, rproxyAnswerTime: "250", }) if err != nil { t.Fatalf("settings: %v", err) } if cfg.ListenAddr != ":9000" || cfg.UpstreamURL.Host != "app:3000" || cfg.WAFBackend.Host != "waf.example:8443" || cfg.TrustedProxies[0] != netip.MustParsePrefix("192.0.2.0/24") { t.Errorf("addresses %+v", cfg) } if cfg.Timeout != 150*time.Millisecond || cfg.DefaultDisposition != "deny" || cfg.MaxRequestSize != 1024 || cfg.MaxResponseSize != 0 || cfg.MaxRequestTime != 0 || cfg.MaxResponseTime != 250*time.Millisecond { t.Errorf("limits %+v", cfg) } } func TestRProxyInvalidSettingsAreRefused(t *testing.T) { t.Parallel() for _, tc := range []struct { name, value string }{ {wafBackend, ""}, {wafBackend, "servewaf:8080"}, {wafBackend, "http://servewaf:8080/path"}, {rproxyTimeout, "0"}, {rproxyTimeout, "-1"}, {rproxyTimeout, off}, {rproxyTimeout, "2s"}, {rproxyTimeout, "1.25"}, {rproxyTimeout, "99999999999999999"}, {rproxyDefault, "maybe"}, {rproxyListen, "8080"}, {rproxyUpstream, "app"}, {rproxyUpstream, "127.0.0.1:8081"}, {rproxyUpstream, "ftp://127.0.0.1:8081"}, {rproxyUpstream, "http://"}, {rproxyUpstream, "http://:8081"}, {rproxyUpstream, "http://127.0.0.1:0"}, {rproxyUpstream, "http://127.0.0.1:99999"}, {rproxyUpstream, "http://127.0.0.1:8081/app"}, {rproxyUpstream, "http://127.0.0.1:8081/?a=1"}, {rproxyUpstream, "http://user:secret@127.0.0.1:8081"}, {rproxyTrusted, "not a netblock"}, {rproxyTrusted, "10.0.0.0/33"}, {rproxyTrusted, "10.0.0.0/8,,192.168.0.0/16"}, {rproxyTrusted, "fe80::1%eth0"}, {rproxyMaxRequest, notASize}, {rproxyMaxResponse, notASize}, {rproxyRequestTime, "0"}, {rproxyRequestTime, "1m"}, {rproxyAnswerTime, "0"}, } { t.Run(tc.name+"="+tc.value, func(t *testing.T) { t.Parallel() _, err := rproxyConfig(map[string]string{wafBackend: backendURL, tc.name: tc.value}) if err == nil || !strings.HasPrefix(err.Error(), tc.name+": ") { t.Errorf("error %v, want one naming %s", err, tc.name) } }) } } func TestRProxyTrustedProxiesSetButEmptyTrustNothing(t *testing.T) { t.Parallel() cfg, err := rproxyConfig(map[string]string{wafBackend: backendURL, rproxyTrusted: ""}) if err != nil { t.Fatalf("settings: %v", err) } if len(cfg.TrustedProxies) != 0 { t.Errorf("trusted proxies %v, want none", cfg.TrustedProxies) } } func TestRProxyNeedsItsBackend(t *testing.T) { t.Parallel() _, err := rproxyConfig(nil) if err == nil || !strings.HasPrefix(err.Error(), wafBackend+": is required") { t.Errorf("error %v, want one saying %s is required", err, wafBackend) } } func TestRProxyReadsNoFile(t *testing.T) { t.Parallel() path := filepath.Join(t.TempDir(), "backend") err := os.WriteFile(path, []byte("http://from-a-file:8080\n"), 0o600) if err != nil { t.Fatalf("write: %v", err) } _, err = rproxyConfig(map[string]string{wafBackend + "_FILE": path}) if err == nil || !strings.HasPrefix(err.Error(), wafBackend+": is required") { t.Errorf("error %v, want %s still required", err, wafBackend) } cfg, err := rproxyConfig(map[string]string{ wafBackend: backendURL, rproxyTimeout + "_FILE": "/nonexistent/timeout", }) if err != nil { t.Fatalf("settings: %v, with the file left unread", err) } if cfg.Timeout != 2*time.Second { t.Errorf("timeout %v, want the default", cfg.Timeout) } } func TestRProxySettingsAreLoggedByName(t *testing.T) { t.Parallel() cfg, err := rproxyConfig(map[string]string{wafBackend: backendURL}) if err != nil { t.Fatalf("settings: %v", err) } group := cfg.LogValue().Group() names := make([]string, 0, len(group)) for _, attr := range group { names = append(names, attr.Key) } for _, name := range []string{ rproxyListen, rproxyUpstream, rproxyTrusted, wafBackend, rproxyTimeout, rproxyDefault, rproxyMaxRequest, rproxyMaxResponse, rproxyRequestTime, rproxyAnswerTime, } { if !slices.Contains(names, name) { t.Errorf("%s is not logged, only %v", name, names) } } }