summaryrefslogtreecommitdiffstats
path: root/internal/config
diff options
context:
space:
mode:
authorGab Virebent <gabriel1@virebent.art>2026-08-03 17:13:50 +0200
committerGab Virebent <gabriel1@virebent.art>2026-08-03 17:13:50 +0200
commitcd58d593789c6facb1d05b7f255bc2c5f2e46010 (patch)
tree87a226313e49e2c6f7b7028d77d4957db416d1c1 /internal/config
downloadn2usenet-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.go209
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
+}