139 lines
3.7 KiB
Go
139 lines
3.7 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type Config struct {
|
|
Env string `yaml:"env"`
|
|
Port string `yaml:"port"`
|
|
DSN string `yaml:"dsn"`
|
|
StorageDir string `yaml:"storage_dir"`
|
|
MaxUploadBytes int64 `yaml:"max_upload_bytes"`
|
|
AuthSecret string `yaml:"auth_secret"`
|
|
SystemAIKey string `yaml:"system_ai_key"`
|
|
AIKeyEncryptionSecret string `yaml:"ai_key_encryption_secret"`
|
|
AllowedOrigins []string `yaml:"allowed_origins"`
|
|
}
|
|
|
|
func Load() Config {
|
|
cfg, err := LoadFromDir("etc")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
func LoadFromDir(configDir string) (Config, error) {
|
|
mode := strings.TrimSpace(os.Getenv("SENLIN_APP_MODE"))
|
|
if mode == "" {
|
|
mode = "dev"
|
|
}
|
|
path := filepath.Join(configDir, "agent."+strings.ToLower(mode)+".yaml")
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
var cfg Config
|
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
return Config{}, err
|
|
}
|
|
selectedEnvironment, err := canonicalEnvironment(mode)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
configuredEnvironment, err := canonicalEnvironment(cfg.Env)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
if selectedEnvironment != configuredEnvironment {
|
|
return Config{}, fmt.Errorf("SENLIN_APP_MODE %s does not match config env %s", mode, cfg.Env)
|
|
}
|
|
if cfg.MaxUploadBytes <= 0 {
|
|
cfg.MaxUploadBytes = 32 << 20
|
|
}
|
|
if strings.TrimSpace(cfg.StorageDir) == "" {
|
|
return Config{}, fmt.Errorf("storage_dir must not be empty")
|
|
}
|
|
hasAllowedOrigin := false
|
|
for _, origin := range cfg.AllowedOrigins {
|
|
if strings.TrimSpace(origin) != "" {
|
|
hasAllowedOrigin = true
|
|
break
|
|
}
|
|
}
|
|
if !hasAllowedOrigin {
|
|
return Config{}, fmt.Errorf("allowed_origins must include at least one origin")
|
|
}
|
|
production := selectedEnvironment == "production"
|
|
if err := validateSecret(production, "auth_secret", cfg.AuthSecret); err != nil {
|
|
return Config{}, err
|
|
}
|
|
if err := validateSecret(production, "ai_key_encryption_secret", cfg.AIKeyEncryptionSecret); err != nil {
|
|
return Config{}, err
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func canonicalEnvironment(value string) (string, error) {
|
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
|
case "dev", "development":
|
|
return "development", nil
|
|
case "prod", "production":
|
|
return "production", nil
|
|
case "test":
|
|
return "test", nil
|
|
default:
|
|
return "", fmt.Errorf("unsupported environment %q", value)
|
|
}
|
|
}
|
|
|
|
func validateSecret(production bool, field, value string) error {
|
|
secret := strings.TrimSpace(value)
|
|
if secret == "" {
|
|
return fmt.Errorf("%s must not be empty", field)
|
|
}
|
|
if production {
|
|
if len([]byte(secret)) < 32 || estimatedEntropyBits(secret) < 128 || isCommonSecret(secret) {
|
|
return fmt.Errorf("%s must contain at least 32 bytes and an estimated 128 bits of entropy, without development sentinels, in production", field)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func estimatedEntropyBits(value string) float64 {
|
|
data := []byte(value)
|
|
counts := make(map[byte]int, len(data))
|
|
for _, item := range data {
|
|
counts[item]++
|
|
}
|
|
length := float64(len(data))
|
|
entropyPerByte := 0.0
|
|
for _, count := range counts {
|
|
probability := float64(count) / length
|
|
entropyPerByte -= probability * math.Log2(probability)
|
|
}
|
|
return entropyPerByte * length
|
|
}
|
|
|
|
func isCommonSecret(value string) bool {
|
|
normalized := strings.ToLower(strings.TrimSpace(value))
|
|
for _, marker := range []string{"change-me", "changeme", "development", "dev-secret", "local-secret", "test-secret", "placeholder"} {
|
|
if strings.Contains(normalized, marker) {
|
|
return true
|
|
}
|
|
}
|
|
switch normalized {
|
|
case "secret", "password", "default", "admin":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|