rss email digests over ssh because you're a cool kid
herald.dunkirk.sh
go
rss
rss-reader
ssh
charm
1package config
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strconv"
8 "strings"
9
10 "github.com/joho/godotenv"
11 "gopkg.in/yaml.v3"
12)
13
14type AppConfig struct {
15 Host string `yaml:"host"`
16 SSHPort int `yaml:"ssh_port"`
17 ExternalSSHPort int `yaml:"external_ssh_port"`
18 HTTPPort int `yaml:"http_port"`
19 HostKeyPath string `yaml:"host_key_path"`
20 DBPath string `yaml:"db_path"`
21 Origin string `yaml:"origin"`
22 LogLevel string `yaml:"log_level"`
23 SMTP SMTPConfig `yaml:"smtp"`
24 AllowAllKeys bool `yaml:"allow_all_keys"`
25 AllowedKeys []string `yaml:"allowed_keys"`
26}
27
28type SMTPConfig struct {
29 Host string `yaml:"host"`
30 Port int `yaml:"port"`
31 User string `yaml:"user"`
32 Pass string `yaml:"pass"`
33 From string `yaml:"from"`
34 DKIMPrivateKey string `yaml:"dkim_private_key"`
35 DKIMPrivateKeyFile string `yaml:"dkim_private_key_file"`
36 DKIMSelector string `yaml:"dkim_selector"`
37 DKIMDomain string `yaml:"dkim_domain"`
38}
39
40func DefaultAppConfig() *AppConfig {
41 return &AppConfig{
42 Host: "0.0.0.0",
43 SSHPort: 2222,
44 HTTPPort: 8080,
45 HostKeyPath: "./host_key",
46 DBPath: "./herald.db",
47 Origin: "http://localhost:8080",
48 LogLevel: "info",
49 SMTP: SMTPConfig{
50 Host: "localhost",
51 Port: 587,
52 From: "herald@localhost",
53 },
54 AllowAllKeys: true,
55 }
56}
57
58func LoadAppConfig(path string) (*AppConfig, error) {
59 cfg := DefaultAppConfig()
60
61 // Load .env file if it exists (silently ignore if not found)
62 if envPath := findEnvFile(path); envPath != "" {
63 _ = godotenv.Load(envPath)
64 }
65
66 if path != "" {
67 data, err := os.ReadFile(path) //nolint:gosec // Config file path from CLI flag
68 if err != nil {
69 return nil, fmt.Errorf("failed to read config file: %w", err)
70 }
71
72 expanded := os.Expand(string(data), func(key string) string {
73 return os.Getenv(key)
74 })
75
76 if err := yaml.Unmarshal([]byte(expanded), cfg); err != nil {
77 return nil, fmt.Errorf("failed to parse config file: %w", err)
78 }
79 }
80
81 applyEnvOverrides(cfg)
82
83 // Default external_ssh_port to ssh_port if not set
84 if cfg.ExternalSSHPort == 0 {
85 cfg.ExternalSSHPort = cfg.SSHPort
86 }
87
88 return cfg, nil
89}
90
91// findEnvFile looks for .env file in the config file's directory or current directory
92func findEnvFile(configPath string) string {
93 // If config path provided, look in its directory
94 if configPath != "" {
95 dir := filepath.Dir(configPath)
96 envPath := filepath.Join(dir, ".env")
97 if _, err := os.Stat(envPath); err == nil {
98 return envPath
99 }
100 }
101
102 // Otherwise check current directory
103 if _, err := os.Stat(".env"); err == nil {
104 return ".env"
105 }
106
107 return ""
108}
109
110func applyEnvOverrides(cfg *AppConfig) {
111 if v := os.Getenv("HERALD_HOST"); v != "" {
112 cfg.Host = v
113 }
114 if v := os.Getenv("HERALD_SSH_PORT"); v != "" {
115 if port, err := strconv.Atoi(v); err == nil {
116 cfg.SSHPort = port
117 }
118 }
119 if v := os.Getenv("HERALD_EXTERNAL_SSH_PORT"); v != "" {
120 if port, err := strconv.Atoi(v); err == nil {
121 cfg.ExternalSSHPort = port
122 }
123 }
124 if v := os.Getenv("HERALD_HTTP_PORT"); v != "" {
125 if port, err := strconv.Atoi(v); err == nil {
126 cfg.HTTPPort = port
127 }
128 }
129 if v := os.Getenv("HERALD_HOST_KEY_PATH"); v != "" {
130 cfg.HostKeyPath = v
131 }
132 if v := os.Getenv("HERALD_DB_PATH"); v != "" {
133 cfg.DBPath = v
134 }
135 if v := os.Getenv("HERALD_SMTP_HOST"); v != "" {
136 cfg.SMTP.Host = v
137 }
138 if v := os.Getenv("HERALD_SMTP_PORT"); v != "" {
139 if port, err := strconv.Atoi(v); err == nil {
140 cfg.SMTP.Port = port
141 }
142 }
143 if v := os.Getenv("HERALD_SMTP_USER"); v != "" {
144 cfg.SMTP.User = v
145 }
146 if v := os.Getenv("HERALD_SMTP_PASS"); v != "" {
147 cfg.SMTP.Pass = v
148 }
149 if v := os.Getenv("HERALD_SMTP_FROM"); v != "" {
150 cfg.SMTP.From = v
151 }
152 if v := os.Getenv("HERALD_SMTP_DKIM_PRIVATE_KEY"); v != "" {
153 cfg.SMTP.DKIMPrivateKey = v
154 }
155 if v := os.Getenv("HERALD_SMTP_DKIM_PRIVATE_KEY_FILE"); v != "" {
156 cfg.SMTP.DKIMPrivateKeyFile = v
157 }
158 if v := os.Getenv("HERALD_SMTP_DKIM_SELECTOR"); v != "" {
159 cfg.SMTP.DKIMSelector = v
160 }
161 if v := os.Getenv("HERALD_SMTP_DKIM_DOMAIN"); v != "" {
162 cfg.SMTP.DKIMDomain = v
163 }
164 if v := os.Getenv("HERALD_ALLOW_ALL_KEYS"); v != "" {
165 cfg.AllowAllKeys = strings.ToLower(v) == "true"
166 }
167 if v := os.Getenv("HERALD_ORIGIN"); v != "" {
168 cfg.Origin = v
169 }
170 if v := os.Getenv("HERALD_LOG_LEVEL"); v != "" {
171 cfg.LogLevel = v
172 }
173}