check / check (push) Failing after 8m17s
`smallwebwaf` with no command now prints the commands and exits with status 2. The image runs rproxy, or servewaf with SMALLWEBWAF_COMMAND=servewaf. The passing-on code, its settings and its log fields and metric go. internal/proxy keeps the checks, answers, reports and own endpoints; its tests run through the real rproxy. The documents describe the pair. Judgement call: clients.json keeps token_refused, since state files refuse unknown fields. Deviation: script/gitea-requests sets SWWAF_LOG_REQUEST_HEADERS, to label requests. Model: opus-5-5
232 lines
6.4 KiB
Go
232 lines
6.4 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|