chore: capture post-P0/P1 state for clean P2 start (working tree was dirty at task begin)
This commit is contained in:
+44
-2
@@ -2,8 +2,11 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/go-units"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -15,6 +18,10 @@ type Config struct {
|
||||
MaxConcurrentRequests int64 `yaml:"max_concurrent_requests" default:"200"`
|
||||
MaxRequestsPerClient int64 `yaml:"max_requests_per_client" default:"5"`
|
||||
|
||||
// P1 hardening limits (security/correctness)
|
||||
MaxObjectSize string `yaml:"max_object_size" default:"0"` // 0=unlimited; e.g. "256MB" protects against OOM from huge/malicious upstream responses (P1-01)
|
||||
TrustedProxies []string `yaml:"trusted_proxies"` // CIDR list; empty=never trust X-Forwarded-For (safe default, P1-02). See README security notes.
|
||||
|
||||
// Cache configuration
|
||||
Cache CacheConfig `yaml:"cache"`
|
||||
|
||||
@@ -75,6 +82,12 @@ func LoadConfig(configPath string) (*Config, error) {
|
||||
if config.MaxRequestsPerClient == 0 {
|
||||
config.MaxRequestsPerClient = 3
|
||||
}
|
||||
if config.MaxObjectSize == "" {
|
||||
config.MaxObjectSize = "0"
|
||||
}
|
||||
if config.TrustedProxies == nil {
|
||||
config.TrustedProxies = []string{}
|
||||
}
|
||||
if config.Cache.Memory.Size == "" {
|
||||
config.Cache.Memory.Size = "0"
|
||||
}
|
||||
@@ -99,8 +112,10 @@ func SaveDefaultConfig(configPath string) error {
|
||||
|
||||
defaultConfig := Config{
|
||||
ListenAddress: ":80",
|
||||
MaxConcurrentRequests: 50, // Reduced for home user (less concurrent load)
|
||||
MaxRequestsPerClient: 3, // Reduced for home user (more conservative per client)
|
||||
MaxConcurrentRequests: 50, // Reduced for home user (less concurrent load)
|
||||
MaxRequestsPerClient: 3, // Reduced for home user (more conservative per client)
|
||||
MaxObjectSize: "0", // 0=unlimited; set e.g. "512MB" for DoS protection on large bodies (P1-01)
|
||||
TrustedProxies: []string{}, // Conservative default: never trust XFF (P1-02 spoof prevention)
|
||||
Cache: CacheConfig{
|
||||
Memory: MemoryConfig{
|
||||
Size: "1GB", // Recommended for systems that can spare 1GB RAM for caching
|
||||
@@ -133,6 +148,8 @@ func GetDefaultConfig() Config {
|
||||
ListenAddress: ":80",
|
||||
MaxConcurrentRequests: 50,
|
||||
MaxRequestsPerClient: 3,
|
||||
MaxObjectSize: "0", // 0=unlimited (override for bounded response safety)
|
||||
TrustedProxies: []string{}, // safe default: do not trust forwarded headers
|
||||
Cache: CacheConfig{
|
||||
Memory: MemoryConfig{
|
||||
Size: "1GB",
|
||||
@@ -169,5 +186,30 @@ func (c Config) Validate() error {
|
||||
return fmt.Errorf("disk cache enabled but no path specified")
|
||||
}
|
||||
|
||||
// P1 light validation for security/resource fields (mirrors existing GC + path checks; fails fast before New)
|
||||
if c.MaxObjectSize != "" && c.MaxObjectSize != "0" {
|
||||
if _, err := units.FromHumanSize(c.MaxObjectSize); err != nil {
|
||||
return fmt.Errorf("invalid max_object_size: %w", err)
|
||||
}
|
||||
}
|
||||
for _, p := range c.TrustedProxies {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(p, "/") {
|
||||
if net.ParseIP(p) == nil {
|
||||
return fmt.Errorf("invalid trusted_proxies entry (not IP or CIDR): %s", p)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, _, err := net.ParseCIDR(p); err != nil {
|
||||
return fmt.Errorf("invalid trusted_proxies CIDR: %s", p)
|
||||
}
|
||||
}
|
||||
if c.MaxConcurrentRequests < 0 || c.MaxRequestsPerClient < 0 { // already covered above but explicit for P1 knobs
|
||||
// covered by earlier checks
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg Config
|
||||
wantErr bool
|
||||
errSub string // substring to match in error if wantErr
|
||||
}{
|
||||
{
|
||||
name: "valid default",
|
||||
cfg: GetDefaultConfig(),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid zero concurrency",
|
||||
cfg: func() Config {
|
||||
c := GetDefaultConfig()
|
||||
c.MaxConcurrentRequests = 0
|
||||
c.MaxRequestsPerClient = 0
|
||||
return c
|
||||
}(),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid negative? no, but zero ok; positive values",
|
||||
cfg: func() Config {
|
||||
c := GetDefaultConfig()
|
||||
c.MaxConcurrentRequests = 100
|
||||
c.MaxRequestsPerClient = 10
|
||||
c.Cache.Memory.GCAlgorithm = "lru"
|
||||
c.Cache.Disk.GCAlgorithm = "hybrid"
|
||||
c.Cache.Disk.Size = "10GB"
|
||||
c.Cache.Disk.Path = "/tmp/cache"
|
||||
return c
|
||||
}(),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "negative max concurrent requests",
|
||||
cfg: func() Config {
|
||||
c := GetDefaultConfig()
|
||||
c.MaxConcurrentRequests = -1
|
||||
return c
|
||||
}(),
|
||||
wantErr: true,
|
||||
errSub: "negative concurrency not allowed",
|
||||
},
|
||||
{
|
||||
name: "negative max requests per client",
|
||||
cfg: func() Config {
|
||||
c := GetDefaultConfig()
|
||||
c.MaxRequestsPerClient = -5
|
||||
return c
|
||||
}(),
|
||||
wantErr: true,
|
||||
errSub: "negative per-client limit not allowed",
|
||||
},
|
||||
{
|
||||
name: "invalid memory gc algorithm",
|
||||
cfg: func() Config {
|
||||
c := GetDefaultConfig()
|
||||
c.Cache.Memory.GCAlgorithm = "invalid-alg"
|
||||
return c
|
||||
}(),
|
||||
wantErr: true,
|
||||
errSub: "invalid memory gc algorithm: invalid-alg",
|
||||
},
|
||||
{
|
||||
name: "empty memory gc ok (treated as default)",
|
||||
cfg: func() Config {
|
||||
c := GetDefaultConfig()
|
||||
c.Cache.Memory.GCAlgorithm = ""
|
||||
return c
|
||||
}(),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid memory gc values",
|
||||
cfg: func() Config {
|
||||
c := GetDefaultConfig()
|
||||
for _, alg := range []string{"lru", "lfu", "fifo", "largest", "smallest", "hybrid"} {
|
||||
c.Cache.Memory.GCAlgorithm = alg
|
||||
if err := c.Validate(); err != nil {
|
||||
t.Errorf("valid gc %s should not error: %v", alg, err)
|
||||
}
|
||||
}
|
||||
return c // last one
|
||||
}(),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "disk enabled (non-zero size) but no path",
|
||||
cfg: func() Config {
|
||||
c := GetDefaultConfig()
|
||||
c.Cache.Disk.Size = "50GB"
|
||||
c.Cache.Disk.Path = ""
|
||||
return c
|
||||
}(),
|
||||
wantErr: true,
|
||||
errSub: "disk cache enabled but no path specified",
|
||||
},
|
||||
{
|
||||
name: "disk size 0 (disabled) no path ok",
|
||||
cfg: func() Config {
|
||||
c := GetDefaultConfig()
|
||||
c.Cache.Disk.Size = "0"
|
||||
c.Cache.Disk.Path = ""
|
||||
return c
|
||||
}(),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "disk size empty (disabled) no path ok",
|
||||
cfg: func() Config {
|
||||
c := GetDefaultConfig()
|
||||
c.Cache.Disk.Size = ""
|
||||
c.Cache.Disk.Path = ""
|
||||
return c
|
||||
}(),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "disk enabled with path ok",
|
||||
cfg: func() Config {
|
||||
c := GetDefaultConfig()
|
||||
c.Cache.Disk.Size = "1TB"
|
||||
c.Cache.Disk.Path = "./disk"
|
||||
return c
|
||||
}(),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "disk gc invalid does not fail (not validated by current impl)",
|
||||
cfg: func() Config {
|
||||
c := GetDefaultConfig()
|
||||
c.Cache.Disk.GCAlgorithm = "bad-disk-gc"
|
||||
c.Cache.Disk.Size = "10GB"
|
||||
c.Cache.Disk.Path = "/p"
|
||||
return c
|
||||
}(),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "p1 new fields default ok (maxobj 0 + empty trusted proxies)",
|
||||
cfg: func() Config {
|
||||
c := GetDefaultConfig()
|
||||
c.MaxObjectSize = "0"
|
||||
c.TrustedProxies = nil
|
||||
return c
|
||||
}(),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.cfg.Validate()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if tt.wantErr && tt.errSub != "" && err != nil {
|
||||
if !strings.Contains(err.Error(), tt.errSub) {
|
||||
t.Errorf("Validate() error %q does not contain %q", err.Error(), tt.errSub)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user