diff options
| author | Gab Virebent <gabriel1@virebent.art> | 2026-08-03 17:13:50 +0200 |
|---|---|---|
| committer | Gab Virebent <gabriel1@virebent.art> | 2026-08-03 17:13:50 +0200 |
| commit | cd58d593789c6facb1d05b7f255bc2c5f2e46010 (patch) | |
| tree | 87a226313e49e2c6f7b7028d77d4957db416d1c1 /internal/config | |
| download | n2usenet-cd58d593789c6facb1d05b7f255bc2c5f2e46010.tar.gz n2usenet-cd58d593789c6facb1d05b7f255bc2c5f2e46010.tar.xz n2usenet-cd58d593789c6facb1d05b7f255bc2c5f2e46010.zip | |
Initial import: n2usenet HTTPS/Nym Usenet gateway
Diffstat (limited to 'internal/config')
| -rw-r--r-- | internal/config/config.go | 209 |
1 files changed, 209 insertions, 0 deletions
diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..5ee60ea --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,209 @@ +package config + +import ( + "fmt" + "net" + "os" + "strconv" + "strings" + "time" +) + +type Config struct { + Listen string + PublicBaseURL string + + SMTP SMTPConfig + Nym NymConfig + Security SecurityConfig +} + +type SMTPConfig struct { + Host string + Port int + Recipient string + EnvelopeFrom string + HELO string + TLSServerName string + RequireTLS bool + ImplicitTLS bool + Timeout time.Duration + DryRun bool +} + +type NymConfig struct { + Enabled bool + Managed bool + Binary string + Provider string + HomeDir string + ClientID string + SocksAddr string + AnonymousReplies bool + StartupTimeout time.Duration +} + +type SecurityConfig struct { + MinHashcashBits int + MinMessageBytes int + MaxMessageBytes int + MaxNewsgroups int + RateLimitCount int + RateLimitWindow time.Duration + TrustProxy bool + SecureCookies bool + MessageIDDomain string + IdenticonsCLI string + RequireFace bool +} + +func Load() (Config, error) { + cfg := Config{ + Listen: env("N2U_LISTEN", "M2U_LISTEN", "127.0.0.1:8095"), + PublicBaseURL: env("N2U_PUBLIC_BASE_URL", "M2U_PUBLIC_BASE_URL", "https://n2usenet.virebent.art"), + SMTP: SMTPConfig{ + Host: env("N2U_SMTP_HOST", "M2U_SMTP_HOST", "mail2news.tcpreset.net"), + Port: envInt("N2U_SMTP_PORT", "M2U_SMTP_PORT", 25), + Recipient: env("N2U_SMTP_RECIPIENT", "M2U_SMTP_RECIPIENT", "mail2news@mail2news.tcpreset.net"), + EnvelopeFrom: env("N2U_SMTP_ENVELOPE_FROM", "M2U_SMTP_ENVELOPE_FROM", "n2usenet@virebent.art"), + HELO: env("N2U_SMTP_HELO", "M2U_SMTP_HELO", "n2usenet.virebent.art"), + TLSServerName: env("N2U_SMTP_TLS_SERVER_NAME", "M2U_SMTP_TLS_SERVER_NAME", "mail.tcpreset.net"), + RequireTLS: envBool("N2U_SMTP_REQUIRE_TLS", "M2U_SMTP_REQUIRE_TLS", true), + ImplicitTLS: envBool("N2U_SMTP_IMPLICIT_TLS", "M2U_SMTP_IMPLICIT_TLS", false), + Timeout: envDuration("N2U_SMTP_TIMEOUT", "M2U_SMTP_TIMEOUT", 90*time.Second), + DryRun: envBool("N2U_DRY_RUN", "M2U_DRY_RUN", false), + }, + Nym: NymConfig{ + Enabled: envBool("N2U_NYM_ENABLED", "M2U_NYM_ENABLED", true), + Managed: envBool("N2U_NYM_MANAGED", "M2U_NYM_MANAGED", false), + Binary: env("N2U_NYM_BINARY", "M2U_NYM_BINARY", "nym-socks5-client"), + Provider: env("N2U_NYM_PROVIDER", "M2U_NYM_PROVIDER", ""), + HomeDir: env("N2U_NYM_HOME", "M2U_NYM_HOME", "./data/nym"), + ClientID: env("N2U_NYM_CLIENT_ID", "M2U_NYM_CLIENT_ID", "n2usenet"), + SocksAddr: env("N2U_NYM_SOCKS", "M2U_NYM_SOCKS", "127.0.0.1:11080"), + AnonymousReplies: envBool("N2U_NYM_ANONYMOUS_REPLIES", "M2U_NYM_ANONYMOUS_REPLIES", true), + StartupTimeout: envDuration("N2U_NYM_STARTUP_TIMEOUT", "M2U_NYM_STARTUP_TIMEOUT", 120*time.Second), + }, + Security: SecurityConfig{ + MinHashcashBits: envInt("N2U_HASHCASH_MIN_BITS", "M2U_HASHCASH_MIN_BITS", 20), + MinMessageBytes: envInt("N2U_MIN_MESSAGE_BYTES", "M2U_MIN_MESSAGE_BYTES", 10), + MaxMessageBytes: envInt("N2U_MAX_MESSAGE_BYTES", "M2U_MAX_MESSAGE_BYTES", 65536), + MaxNewsgroups: envInt("N2U_MAX_NEWSGROUPS", "M2U_MAX_NEWSGROUPS", 3), + RateLimitCount: envInt("N2U_RATE_LIMIT_COUNT", "M2U_RATE_LIMIT_COUNT", 10), + RateLimitWindow: envDuration("N2U_RATE_LIMIT_WINDOW", "M2U_RATE_LIMIT_WINDOW", time.Hour), + TrustProxy: envBool("N2U_TRUST_PROXY", "M2U_TRUST_PROXY", true), + SecureCookies: envBool("N2U_SECURE_COOKIES", "M2U_SECURE_COOKIES", true), + MessageIDDomain: env("N2U_MESSAGE_ID_DOMAIN", "M2U_MESSAGE_ID_DOMAIN", "n2usenet.virebent.art"), + IdenticonsCLI: env("N2U_IDENTICONS_CLI", "M2U_IDENTICONS_CLI", "/usr/local/bin/identicons-cli"), + RequireFace: envBool("N2U_REQUIRE_FACE", "M2U_REQUIRE_FACE", true), + }, + } + if err := cfg.Validate(); err != nil { + return Config{}, err + } + return cfg, nil +} + +func (c Config) Validate() error { + if _, _, err := net.SplitHostPort(c.Listen); err != nil { + return fmt.Errorf("invalid N2U_LISTEN: %w", err) + } + if c.SMTP.Host == "" { + return fmt.Errorf("N2U_SMTP_HOST is required") + } + if c.SMTP.Port < 1 || c.SMTP.Port > 65535 { + return fmt.Errorf("invalid N2U_SMTP_PORT") + } + if c.SMTP.Recipient == "" { + return fmt.Errorf("N2U_SMTP_RECIPIENT is required") + } + if c.SMTP.EnvelopeFrom == "" { + return fmt.Errorf("N2U_SMTP_ENVELOPE_FROM is required") + } + if c.SMTP.RequireTLS && c.SMTP.TLSServerName == "" { + return fmt.Errorf("N2U_SMTP_TLS_SERVER_NAME is required when TLS is required") + } + if c.Nym.Enabled { + if _, _, err := net.SplitHostPort(c.Nym.SocksAddr); err != nil { + return fmt.Errorf("invalid N2U_NYM_SOCKS: %w", err) + } + if c.Nym.Managed && c.Nym.Provider == "" { + return fmt.Errorf("N2U_NYM_PROVIDER is required when N2U_NYM_MANAGED=true") + } + } + if c.Security.MinHashcashBits < 1 || c.Security.MinHashcashBits > 32 { + return fmt.Errorf("invalid N2U_HASHCASH_MIN_BITS") + } + if c.Security.MinMessageBytes < 0 || c.Security.MaxMessageBytes <= c.Security.MinMessageBytes { + return fmt.Errorf("invalid message size limits") + } + if c.Security.MaxNewsgroups < 1 || c.Security.MaxNewsgroups > 10 { + return fmt.Errorf("invalid N2U_MAX_NEWSGROUPS") + } + if c.Security.RateLimitCount < 1 { + return fmt.Errorf("invalid N2U_RATE_LIMIT_COUNT") + } + if c.Security.RequireFace && c.Security.IdenticonsCLI == "" { + return fmt.Errorf("N2U_IDENTICONS_CLI is required when N2U_REQUIRE_FACE=true") + } + return nil +} + +func env(primary, legacy, fallback string) string { + v, ok := envValue(primary, legacy) + if !ok { + return fallback + } + return v +} + +func envBool(primary, legacy string, fallback bool) bool { + v, ok := envValue(primary, legacy) + if !ok { + return fallback + } + switch strings.ToLower(v) { + case "1", "true", "yes", "on": + return true + case "0", "false", "no", "off": + return false + default: + return fallback + } +} + +func envInt(primary, legacy string, fallback int) int { + v, ok := envValue(primary, legacy) + if !ok { + return fallback + } + n, err := strconv.Atoi(v) + if err != nil { + return fallback + } + return n +} + +func envDuration(primary, legacy string, fallback time.Duration) time.Duration { + v, ok := envValue(primary, legacy) + if !ok { + return fallback + } + if d, err := time.ParseDuration(v); err == nil { + return d + } + if n, err := strconv.Atoi(v); err == nil { + return time.Duration(n) * time.Second + } + return fallback +} + +func envValue(primary, legacy string) (string, bool) { + for _, key := range []string{primary, legacy} { + v := strings.TrimSpace(os.Getenv(key)) + if v != "" { + return v, true + } + } + return "", false +} |
