check / check (push) Failing after 6s
A new ClientIP middleware sets request.RemoteAddr to the client's address before the request is logged. X-Forwarded-For and X-Real-IP count only when the peer is in TRUSTED_PROXIES, which defaults to the RFC 1918 ranges. The login rate limit, the session's IP address and hostname, and the request log all use that address. Model: opus-5-5
287 lines
7.1 KiB
Go
287 lines
7.1 KiB
Go
package config_test
|
|
|
|
import (
|
|
"net/netip"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/neoirc/internal/config"
|
|
"sneak.berlin/go/neoirc/internal/globals"
|
|
"sneak.berlin/go/neoirc/internal/logger"
|
|
)
|
|
|
|
// loadConfig runs config.New and returns its result.
|
|
func loadConfig(t *testing.T) (*config.Config, error) {
|
|
t.Helper()
|
|
|
|
globs := &globals.Globals{
|
|
Appname: "neoirc-test",
|
|
Version: "test",
|
|
StartTime: time.Now(),
|
|
}
|
|
|
|
log, err := logger.New(nil, logger.Params{Globals: globs})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return config.New(
|
|
nil, config.Params{Globals: globs, Logger: log},
|
|
)
|
|
}
|
|
|
|
func newConfig(t *testing.T) *config.Config {
|
|
t.Helper()
|
|
|
|
cfg, err := loadConfig(t)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return cfg
|
|
}
|
|
|
|
func TestUnsetSettingsKeepDefaults(t *testing.T) {
|
|
// Viper counts an empty environment variable as unset.
|
|
t.Setenv("PORT", "")
|
|
t.Setenv("MAX_MESSAGE_SIZE", "")
|
|
t.Setenv("NEOIRC_HASHCASH_BITS", "")
|
|
t.Setenv("LOGIN_RATE_BURST", "")
|
|
t.Setenv("LOGIN_RATE_LIMIT", "")
|
|
t.Setenv("DEBUG", "")
|
|
t.Setenv("MAINTENANCE_MODE", "")
|
|
t.Setenv("MESSAGE_MAX_AGE", "")
|
|
t.Setenv("QUEUE_MAX_AGE", "")
|
|
t.Setenv("SESSION_IDLE_TIMEOUT", "")
|
|
|
|
// Each duration setting defaults to 30 days.
|
|
const thirtyDays = "720h"
|
|
|
|
want := settings{
|
|
Port: 8080,
|
|
MaxMessageSize: 4096,
|
|
HashcashBits: 20,
|
|
LoginRateBurst: 5,
|
|
LoginRateLimit: 1,
|
|
Debug: false,
|
|
MaintenanceMode: false,
|
|
MessageMaxAge: thirtyDays,
|
|
QueueMaxAge: thirtyDays,
|
|
SessionIdleTimeout: thirtyDays,
|
|
}
|
|
|
|
got := settingsOf(newConfig(t))
|
|
if got != want {
|
|
t.Errorf("settings = %+v, want %+v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestValidSettingsAreUsed(t *testing.T) {
|
|
// Each whole number is the lowest value its setting allows, and 1ns
|
|
// is the shortest duration allowed.
|
|
t.Setenv("PORT", "0")
|
|
t.Setenv("MAX_MESSAGE_SIZE", "1")
|
|
t.Setenv("NEOIRC_HASHCASH_BITS", "0")
|
|
t.Setenv("LOGIN_RATE_BURST", "1")
|
|
t.Setenv("LOGIN_RATE_LIMIT", "0.5")
|
|
t.Setenv("DEBUG", "true")
|
|
t.Setenv("MAINTENANCE_MODE", "true")
|
|
t.Setenv("MESSAGE_MAX_AGE", "1ns")
|
|
t.Setenv("QUEUE_MAX_AGE", "1h30m")
|
|
t.Setenv("SESSION_IDLE_TIMEOUT", "45m")
|
|
|
|
want := settings{
|
|
Port: 0,
|
|
MaxMessageSize: 1,
|
|
HashcashBits: 0,
|
|
LoginRateBurst: 1,
|
|
LoginRateLimit: 0.5,
|
|
Debug: true,
|
|
MaintenanceMode: true,
|
|
MessageMaxAge: "1ns",
|
|
QueueMaxAge: "1h30m",
|
|
SessionIdleTimeout: "45m",
|
|
}
|
|
|
|
got := settingsOf(newConfig(t))
|
|
if got != want {
|
|
t.Errorf("settings = %+v, want %+v", got, want)
|
|
}
|
|
}
|
|
|
|
// settings holds the Config fields whose settings config.New parses.
|
|
type settings struct {
|
|
Port int
|
|
MaxMessageSize int
|
|
HashcashBits int
|
|
LoginRateBurst int
|
|
LoginRateLimit float64
|
|
Debug bool
|
|
MaintenanceMode bool
|
|
MessageMaxAge string
|
|
QueueMaxAge string
|
|
SessionIdleTimeout string
|
|
}
|
|
|
|
func settingsOf(cfg *config.Config) settings {
|
|
return settings{
|
|
Port: cfg.Port,
|
|
MaxMessageSize: cfg.MaxMessageSize,
|
|
HashcashBits: cfg.HashcashBits,
|
|
LoginRateBurst: cfg.LoginRateBurst,
|
|
LoginRateLimit: cfg.LoginRateLimit,
|
|
Debug: cfg.Debug,
|
|
MaintenanceMode: cfg.MaintenanceMode,
|
|
MessageMaxAge: cfg.MessageMaxAge,
|
|
QueueMaxAge: cfg.QueueMaxAge,
|
|
SessionIdleTimeout: cfg.SessionIdleTimeout,
|
|
}
|
|
}
|
|
|
|
func TestBadSettingStopsStartup(t *testing.T) {
|
|
// For each setting, the first value does not parse and the others
|
|
// parse but are out of range.
|
|
tests := []struct {
|
|
key string
|
|
values []string
|
|
}{
|
|
{key: "PORT", values: []string{"80a", "-1"}},
|
|
{key: "MAX_MESSAGE_SIZE", values: []string{"4k", "0", "-4096"}},
|
|
{key: "NEOIRC_HASHCASH_BITS", values: []string{"twenty", "-1"}},
|
|
{key: "LOGIN_RATE_BURST", values: []string{"five", "0", "-5"}},
|
|
{key: "LOGIN_RATE_LIMIT", values: []string{
|
|
"fast", "0", "-1", "NaN", "Inf", "-Inf",
|
|
}},
|
|
{key: "DEBUG", values: []string{"yes"}},
|
|
{key: "MAINTENANCE_MODE", values: []string{"on"}},
|
|
{key: "MESSAGE_MAX_AGE", values: []string{"30d", "0", "-1h"}},
|
|
{key: "QUEUE_MAX_AGE", values: []string{"720", "0s", "-24h"}},
|
|
{key: "SESSION_IDLE_TIMEOUT", values: []string{"a month", "0", "-1m"}},
|
|
{key: "TRUSTED_PROXIES", values: []string{
|
|
"proxy.example.com", "10.0.0.0/33", "192.0.2.1:8080",
|
|
}},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
for _, value := range test.values {
|
|
t.Run(test.key+"="+value, func(t *testing.T) {
|
|
t.Setenv(test.key, value)
|
|
|
|
_, err := loadConfig(t)
|
|
if err == nil {
|
|
t.Fatalf("%s=%q: got no error", test.key, value)
|
|
}
|
|
|
|
msg := err.Error()
|
|
if !strings.Contains(msg, test.key) ||
|
|
!strings.Contains(msg, value) {
|
|
t.Errorf("error %q does not name %s and %q",
|
|
msg, test.key, value)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIRCListenAddrUnsetKeepsDefault(t *testing.T) {
|
|
// t.Setenv puts back whatever value was there after the test.
|
|
t.Setenv("IRC_LISTEN_ADDR", "")
|
|
|
|
err := os.Unsetenv("IRC_LISTEN_ADDR")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
const want = ":6667"
|
|
|
|
cfg := newConfig(t)
|
|
if cfg.IRCListenAddr != want {
|
|
t.Errorf("IRCListenAddr = %q, want %q", cfg.IRCListenAddr, want)
|
|
}
|
|
}
|
|
|
|
func TestIRCListenAddrEmptyTurnsListenerOff(t *testing.T) {
|
|
t.Setenv("IRC_LISTEN_ADDR", "")
|
|
|
|
cfg := newConfig(t)
|
|
if cfg.IRCListenAddr != "" {
|
|
t.Errorf("IRCListenAddr = %q, want empty", cfg.IRCListenAddr)
|
|
}
|
|
}
|
|
|
|
func TestTrustedProxies(t *testing.T) {
|
|
// Each case lists addresses that must be in the trusted set and
|
|
// addresses that must not.
|
|
tests := []struct {
|
|
name string
|
|
set bool
|
|
value string
|
|
trusted []string
|
|
untrusted []string
|
|
}{
|
|
{
|
|
name: "unset trusts the RFC 1918 ranges",
|
|
set: false,
|
|
value: "",
|
|
trusted: []string{
|
|
"10.1.2.3", "172.16.0.1", "172.31.255.254", "192.168.1.1",
|
|
},
|
|
untrusted: []string{
|
|
"127.0.0.1", "172.32.0.1", "192.0.2.1", "2001:db8::1",
|
|
},
|
|
},
|
|
{
|
|
name: "a value replaces the default",
|
|
set: true,
|
|
value: "198.51.100.0/24, 192.0.2.7,2001:db8::/32",
|
|
trusted: []string{"198.51.100.200", "192.0.2.7", "2001:db8::5"},
|
|
untrusted: []string{"10.1.2.3", "192.168.1.1", "192.0.2.8"},
|
|
},
|
|
{
|
|
name: "a mapped IPv6 address is trusted as IPv4",
|
|
set: true,
|
|
value: "::ffff:192.0.2.7",
|
|
trusted: []string{"192.0.2.7"},
|
|
untrusted: []string{"10.1.2.3"},
|
|
},
|
|
{
|
|
name: "the empty string trusts nothing",
|
|
set: true,
|
|
value: "",
|
|
trusted: nil,
|
|
untrusted: []string{"10.1.2.3", "192.168.1.1", "127.0.0.1"},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
// t.Setenv puts back whatever value was there after the
|
|
// test, including when Unsetenv removes it.
|
|
t.Setenv("TRUSTED_PROXIES", test.value)
|
|
|
|
if !test.set {
|
|
err := os.Unsetenv("TRUSTED_PROXIES")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
proxies := newConfig(t).TrustedProxies
|
|
|
|
for _, addr := range test.trusted {
|
|
if !proxies.Contains(netip.MustParseAddr(addr)) {
|
|
t.Errorf("%s is not trusted", addr)
|
|
}
|
|
}
|
|
|
|
for _, addr := range test.untrusted {
|
|
if proxies.Contains(netip.MustParseAddr(addr)) {
|
|
t.Errorf("%s is trusted", addr)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|