Files
sower/config/sowerd_test.go
wweir b42b2738e7 test(sowerd): stop the authority checks from passing for the wrong reason
Review of the previous commit found the validation test green for a reason it
did not claim: SowerdConfig with an empty FakeSite fails Validate on
"fake site is required" before validateSiteRoutes runs, so the bogus authority
was never exercised. Set a valid FakeSite and assert the error names the
authority field, which the assertion can no longer satisfy by accident.

Also fold the router mapping case into the end-to-end test and drop the
standalone TestSiteRouterPreserveHost: the positive mapping is already implied
by the wire assertion, and the default case costs one extra route and two
checks there. Both directions were verified by mutation — inverting the mapping
and letting Validate accept the bogus value each turn the covering test red.
2026-10-12 16:09:19 +08:00

624 lines
15 KiB
Go

package config
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/cristalhq/aconfig"
"github.com/cristalhq/aconfig/aconfigtoml"
)
func TestSowerdConfigPasswordVerbatim(t *testing.T) {
t.Parallel()
path := t.TempDir() + "/sowerd.toml"
if err := os.WriteFile(path, []byte(`
password = "fnhkwfnh"
fake_site = "127.0.0.1:18080"
`), 0o600); err != nil {
t.Fatalf("write config: %v", err)
}
var cfg SowerdConfig
if err := aconfig.LoaderFor(&cfg, aconfig.Config{
SkipEnv: true,
SkipFlags: true,
Files: []string{path},
FileDecoders: map[string]aconfig.FileDecoder{
".toml": aconfigtoml.New(),
},
}).Load(); err != nil {
t.Fatalf("load config: %v", err)
}
// "fnhkwfnh" is valid canonical base64; it must stay verbatim or client
// auth (byte-for-byte checksum over the password) silently breaks.
if got := cfg.Password; got != "fnhkwfnh" {
t.Fatalf("sowerd password = %q, want verbatim fnhkwfnh", got)
}
}
func TestSowerdConfigSiteRouteBasicAuthFromTOML(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "sowerd.toml")
if err := os.WriteFile(path, []byte(`
password = "secret"
fake_site = "127.0.0.1:18080"
[[site_routes]]
domains = ["nas.example.com"]
upstream = "http://127.0.0.1:9000"
[site_routes.auth]
username = "alice"
password = "s3cret"
[site_routes.routes]
"/ws" = "http://127.0.0.1:9002"
[[site_routes]]
domains = ["open.example.com"]
upstream = "http://127.0.0.1:9001"
`), 0o600); err != nil {
t.Fatalf("write config: %v", err)
}
var cfg SowerdConfig
if err := loadConfigFileForTest(path, &cfg); err != nil {
t.Fatalf("load config: %v", err)
}
// The gate lives in a nested table of an array-of-tables element; a
// decoder that silently drops it would leave the route open, so the
// parsed values (not just the load result) are the assertion.
if len(cfg.SiteRoutes) != 2 {
t.Fatalf("site routes = %d, want 2", len(cfg.SiteRoutes))
}
if got := cfg.SiteRoutes[0].Auth; got.Username != "alice" || got.Password != "s3cret" {
t.Fatalf("basic auth = %#v, want alice/s3cret", got)
}
if cfg.SiteRoutes[0].Routes["/ws"] != "http://127.0.0.1:9002" {
t.Fatalf("path routes = %#v, want the /ws override alongside auth", cfg.SiteRoutes[0].Routes)
}
if got := cfg.SiteRoutes[1].Auth; got.Username != "" || got.Password != "" {
t.Fatalf("ungated route basic auth = %#v, want empty", got)
}
if err := cfg.Validate(); err != nil {
t.Fatalf("validate config: %v", err)
}
}
func TestSowerdConfigSiteRouteAuthorityFromTOML(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "sowerd.toml")
if err := os.WriteFile(path, []byte(`
password = "secret"
fake_site = "127.0.0.1:18080"
[[site_routes]]
domains = ["web.example.com"]
upstream = "http://192.0.2.10:30141"
authority = "client"
[[site_routes]]
domains = ["plain.example.com"]
upstream = "http://127.0.0.1:9001"
`), 0o600); err != nil {
t.Fatalf("write config: %v", err)
}
var cfg SowerdConfig
if err := loadConfigFileForTest(path, &cfg); err != nil {
t.Fatalf("load config: %v", err)
}
if len(cfg.SiteRoutes) != 2 {
t.Fatalf("site routes = %d, want 2", len(cfg.SiteRoutes))
}
// An upstream that validates the visited authority itself only works when the
// client's name survives the proxy, so the value must round-trip from TOML and
// must stay off when the route does not ask for it.
if got := cfg.SiteRoutes[0].Authority; got != AuthorityClient {
t.Errorf("authority = %q, want %q", got, AuthorityClient)
}
if !cfg.SiteRoutes[0].PreserveHost() {
t.Error("PreserveHost() = false for an authority = client route")
}
if cfg.SiteRoutes[1].Authority != "" {
t.Errorf("authority = %q, want the empty default", cfg.SiteRoutes[1].Authority)
}
if cfg.SiteRoutes[1].PreserveHost() {
t.Error("PreserveHost() = true for a route that did not ask for it")
}
if err := cfg.Validate(); err != nil {
t.Fatalf("validate config: %v", err)
}
}
func TestSowerdConfigSiteRouteAuthorityRejectsUnknownValue(t *testing.T) {
t.Parallel()
// FakeSite is set so Validate reaches the route check: an earlier field
// failing first would make this test green without exercising the authority.
cfg := SowerdConfig{Password: "secret", FakeSite: "127.0.0.1:18080"}
cfg.SiteRoutes = []SiteRoute{
{Domains: []string{"a.example.com"}, Upstream: "http://127.0.0.1:9000", Authority: "keep"},
}
err := cfg.Validate()
if err == nil {
t.Fatal("Validate accepted an unknown authority, want an error")
}
if !strings.Contains(err.Error(), "authority") {
t.Fatalf("Validate error = %v, want it to name the authority field", err)
}
}
func TestSowerdConfigValidate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
cfg SowerdConfig
wantErr bool
}{
{
name: "valid remote fake site",
cfg: SowerdConfig{
ServeIP: "0.0.0.0",
Password: "secret",
FakeSite: "127.0.0.1:8080",
},
},
{
name: "invalid serve ip",
cfg: SowerdConfig{
ServeIP: "bad-ip",
Password: "secret",
FakeSite: "127.0.0.1:8080",
},
wantErr: true,
},
{
name: "missing password",
cfg: SowerdConfig{
FakeSite: "127.0.0.1:8080",
},
wantErr: true,
},
{
name: "invalid fake site address",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "not-an-address",
},
wantErr: true,
},
{
name: "partial cert config",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
Cert: struct {
Email string
Cert string
Key string
Domains []string
}{
Cert: "/tmp/cert.pem",
},
},
wantErr: true,
},
{
name: "valid site routes",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
SiteRoutes: []SiteRoute{
{Domains: []string{"a.example.com", "b.example.com"}, Upstream: "http://127.0.0.1:9000"},
{Domains: []string{"c.example.com"}, Upstream: "https://backend.example.com"},
},
},
},
{
name: "valid path routes",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
SiteRoutes: []SiteRoute{
{
Domains: []string{"a.example.com"},
Upstream: "http://127.0.0.1:9000",
Routes: map[string]string{
"/ws": "http://127.0.0.1:9002",
"/admin/": "http://127.0.0.1:9003",
},
},
},
},
},
{
name: "path route without leading slash",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
SiteRoutes: []SiteRoute{
{
Domains: []string{"a.example.com"},
Upstream: "http://127.0.0.1:9000",
Routes: map[string]string{"ws": "http://127.0.0.1:9002"},
},
},
},
wantErr: true,
},
{
name: "basic auth visitor gate",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
SiteRoutes: []SiteRoute{
{
Domains: []string{"nas.example.com"},
Upstream: "http://127.0.0.1:9000",
Auth: SiteRouteAuth{Username: "alice", Password: "s3cret"},
},
},
},
},
{
name: "basic auth username without password",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
SiteRoutes: []SiteRoute{
{
Domains: []string{"nas.example.com"},
Upstream: "http://127.0.0.1:9000",
Auth: SiteRouteAuth{Username: "alice"},
},
},
},
wantErr: true,
},
{
name: "basic auth password without username",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
SiteRoutes: []SiteRoute{
{
Domains: []string{"nas.example.com"},
Upstream: "http://127.0.0.1:9000",
Auth: SiteRouteAuth{Password: "s3cret"},
},
},
},
wantErr: true,
},
{
name: "basic auth username with colon",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
SiteRoutes: []SiteRoute{
{
Domains: []string{"nas.example.com"},
Upstream: "http://127.0.0.1:9000",
Auth: SiteRouteAuth{Username: "ali:ce", Password: "s3cret"},
},
},
},
wantErr: true,
},
{
name: "path route with query",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
SiteRoutes: []SiteRoute{
{
Domains: []string{"a.example.com"},
Upstream: "http://127.0.0.1:9000",
Routes: map[string]string{"/ws?x=1": "http://127.0.0.1:9002"},
},
},
},
wantErr: true,
},
{
name: "path route invalid upstream scheme",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
SiteRoutes: []SiteRoute{
{
Domains: []string{"a.example.com"},
Upstream: "http://127.0.0.1:9000",
Routes: map[string]string{"/ws": "ftp://127.0.0.1:21"},
},
},
},
wantErr: true,
},
{
name: "empty domains",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
SiteRoutes: []SiteRoute{
{Domains: []string{}, Upstream: "http://127.0.0.1:9000"},
},
},
wantErr: true,
},
{
name: "empty upstream",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
SiteRoutes: []SiteRoute{
{Domains: []string{"a.example.com"}, Upstream: ""},
},
},
wantErr: true,
},
{
name: "invalid upstream scheme",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
SiteRoutes: []SiteRoute{
{Domains: []string{"a.example.com"}, Upstream: "ftp://127.0.0.1:21"},
},
},
wantErr: true,
},
{
name: "upstream missing host",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
SiteRoutes: []SiteRoute{
{Domains: []string{"a.example.com"}, Upstream: "http://"},
},
},
wantErr: true,
},
{
name: "wildcard domain rejected",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
SiteRoutes: []SiteRoute{
{Domains: []string{"*.example.com"}, Upstream: "http://127.0.0.1:9000"},
},
},
wantErr: true,
},
{
name: "duplicate domain across routes",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
SiteRoutes: []SiteRoute{
{Domains: []string{"a.example.com"}, Upstream: "http://127.0.0.1:9000"},
{Domains: []string{"a.example.com"}, Upstream: "http://127.0.0.1:9001"},
},
},
wantErr: true,
},
{
name: "empty domain in list",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
SiteRoutes: []SiteRoute{
{Domains: []string{"a.example.com", ""}, Upstream: "http://127.0.0.1:9000"},
},
},
wantErr: true,
},
{
name: "domain with whitespace rejected",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
SiteRoutes: []SiteRoute{
{Domains: []string{" a.example.com"}, Upstream: "http://127.0.0.1:9000"},
},
},
wantErr: true,
},
{
name: "domain as url rejected",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
SiteRoutes: []SiteRoute{
{Domains: []string{"https://a.example.com"}, Upstream: "http://127.0.0.1:9000"},
},
},
wantErr: true,
},
{
name: "domain with port rejected",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
SiteRoutes: []SiteRoute{
{Domains: []string{"a.example.com:443"}, Upstream: "http://127.0.0.1:9000"},
},
},
wantErr: true,
},
{
name: "domain with trailing dot rejected",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
SiteRoutes: []SiteRoute{
{Domains: []string{"a.example.com."}, Upstream: "http://127.0.0.1:9000"},
},
},
wantErr: true,
},
{
name: "no site routes is valid",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
},
},
{
name: "valid cert domains",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
Cert: struct {
Email string
Cert string
Key string
Domains []string
}{
Domains: []string{"proxy.example.com"},
},
},
},
{
name: "invalid cert domain rejected",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
Cert: struct {
Email string
Cert string
Key string
Domains []string
}{
Domains: []string{"not a domain"},
},
},
wantErr: true,
},
{
name: "duplicate cert domain rejected",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
Cert: struct {
Email string
Cert string
Key string
Domains []string
}{
Domains: []string{"proxy.example.com", "proxy.example.com"},
},
},
wantErr: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.cfg.Validate()
if tt.wantErr && err == nil {
t.Fatal("expected error")
}
if !tt.wantErr && err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
}
func TestSowerdConfigValidateSiteRouteCertificate(t *testing.T) {
t.Parallel()
certPath, keyPath := writeTestCertificate(t, []string{"a.example.com", "b.example.com"})
cfg := SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
SiteRoutes: []SiteRoute{
{Domains: []string{"a.example.com", "b.example.com"}, Upstream: "http://127.0.0.1:9000"},
},
}
cfg.Cert.Cert = certPath
cfg.Cert.Key = keyPath
if err := cfg.Validate(); err != nil {
t.Fatalf("unexpected validation error: %v", err)
}
cfg.SiteRoutes = []SiteRoute{
{Domains: []string{"missing.example.com"}, Upstream: "http://127.0.0.1:9000"},
}
if err := cfg.Validate(); err == nil {
t.Fatal("expected validation error for uncovered site route domain")
}
}
func writeTestCertificate(t *testing.T, domains []string) (string, string) {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("generate key: %v", err)
}
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: domains[0],
},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
DNSNames: domains,
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
if err != nil {
t.Fatalf("create cert: %v", err)
}
dir := t.TempDir()
certPath := filepath.Join(dir, "cert.pem")
keyPath := filepath.Join(dir, "key.pem")
certFile, err := os.Create(certPath)
if err != nil {
t.Fatalf("create cert file: %v", err)
}
if err := pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: der}); err != nil {
t.Fatalf("write cert file: %v", err)
}
if err := certFile.Close(); err != nil {
t.Fatalf("close cert file: %v", err)
}
keyFile, err := os.Create(keyPath)
if err != nil {
t.Fatalf("create key file: %v", err)
}
if err := pem.Encode(keyFile, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}); err != nil {
t.Fatalf("write key file: %v", err)
}
if err := keyFile.Close(); err != nil {
t.Fatalf("close key file: %v", err)
}
return certPath, keyPath
}