Files
ric930-fake-smtp/config.go
T

414 lines
7.2 KiB
Go

package main
import (
"bytes"
"fmt"
"log"
"os"
"path/filepath"
"time"
"gopkg.in/yaml.v3"
)
type Config struct {
SMTP SMTPConfig `yaml:"smtp"`
Web WebConfig `yaml:"web"`
}
type SMTPConfig struct {
ListenAddress string `yaml:"listen_address"`
ListenPort int `yaml:"listen_port"`
StorageDir string `yaml:"storage_dir"`
DomainName string `yaml:"domain_name"`
MaxMessageSize int64 `yaml:"max_message_size"`
EnableAuth bool `yaml:"enable_auth"`
}
type WebConfig struct {
Enabled bool `yaml:"enabled"`
Port int `yaml:"port"`
Directory string `yaml:"directory"`
Auth AuthConfig `yaml:"auth"`
}
type AuthConfig struct {
Enabled bool `yaml:"enabled"`
Username string `yaml:"username"`
Password string `yaml:"password"`
SessionTimeout string `yaml:"session_timeout"`
CookieSecure bool `yaml:"cookie_secure"`
LoginMaxAttempts int `yaml:"login_max_attempts"`
LoginWindow string `yaml:"login_window"`
LoginLockout string `yaml:"login_lockout"`
}
func defaultConfig() *Config {
return &Config{
SMTP: SMTPConfig{
ListenAddress: "0.0.0.0",
ListenPort: 25,
StorageDir: "emails",
DomainName: "m.ric930.ru",
MaxMessageSize: 50 * 1024 * 1024,
EnableAuth: true,
},
Web: WebConfig{
Enabled: true,
Port: 8089,
Directory: "web",
Auth: AuthConfig{
Enabled: true,
Username: "admin",
Password: "admin",
SessionTimeout: "15m",
CookieSecure: false,
LoginMaxAttempts: 3,
LoginWindow: "5m",
LoginLockout: "15m",
},
},
}
}
func getExecutableDir() (string, error) {
exe, err := os.Executable()
if err != nil {
return "", err
}
exe, err = filepath.Abs(exe)
if err != nil {
return "", err
}
return filepath.Dir(exe), nil
}
func resolveConfigPaths(
cfg *Config,
exeDir string,
) {
if cfg.SMTP.StorageDir != "" &&
!filepath.IsAbs(cfg.SMTP.StorageDir) {
cfg.SMTP.StorageDir = filepath.Join(
exeDir,
cfg.SMTP.StorageDir,
)
}
if cfg.Web.Directory != "" &&
!filepath.IsAbs(cfg.Web.Directory) {
cfg.Web.Directory = filepath.Join(
exeDir,
cfg.Web.Directory,
)
}
}
func validateConfig(cfg *Config) error {
if cfg.SMTP.ListenPort < 1 ||
cfg.SMTP.ListenPort > 65535 {
return fmt.Errorf(
"smtp.listen_port must be between 1 and 65535",
)
}
if cfg.SMTP.StorageDir == "" {
return fmt.Errorf(
"smtp.storage_dir must not be empty",
)
}
if cfg.SMTP.DomainName == "" {
return fmt.Errorf(
"smtp.domain_name must not be empty",
)
}
if cfg.SMTP.MaxMessageSize <= 0 {
return fmt.Errorf(
"smtp.max_message_size must be greater than zero",
)
}
if !cfg.Web.Enabled {
return nil
}
if cfg.Web.Port < 1 ||
cfg.Web.Port > 65535 {
return fmt.Errorf(
"web.port must be between 1 and 65535",
)
}
if cfg.Web.Directory == "" {
return fmt.Errorf(
"web.directory must not be empty",
)
}
if !cfg.Web.Auth.Enabled {
return nil
}
if cfg.Web.Auth.Username == "" {
return fmt.Errorf(
"web.auth.username must not be empty",
)
}
if cfg.Web.Auth.Password == "" {
return fmt.Errorf(
"web.auth.password must not be empty",
)
}
if cfg.Web.Auth.SessionTimeout == "" {
return fmt.Errorf(
"web.auth.session_timeout must not be empty",
)
}
sessionTimeout, err := time.ParseDuration(
cfg.Web.Auth.SessionTimeout,
)
if err != nil {
return fmt.Errorf(
"invalid web.auth.session_timeout %q: %w",
cfg.Web.Auth.SessionTimeout,
err,
)
}
if sessionTimeout <= 0 {
return fmt.Errorf(
"web.auth.session_timeout must be greater than zero",
)
}
if cfg.Web.Auth.LoginMaxAttempts <= 0 {
return fmt.Errorf(
"web.auth.login_max_attempts must be greater than zero",
)
}
loginWindow, err := time.ParseDuration(
cfg.Web.Auth.LoginWindow,
)
if err != nil {
return fmt.Errorf(
"invalid web.auth.login_window %q: %w",
cfg.Web.Auth.LoginWindow,
err,
)
}
if loginWindow <= 0 {
return fmt.Errorf(
"web.auth.login_window must be greater than zero",
)
}
loginLockout, err := time.ParseDuration(
cfg.Web.Auth.LoginLockout,
)
if err != nil {
return fmt.Errorf(
"invalid web.auth.login_lockout %q: %w",
cfg.Web.Auth.LoginLockout,
err,
)
}
if loginLockout <= 0 {
return fmt.Errorf(
"web.auth.login_lockout must be greater than zero",
)
}
return nil
}
func LoadConfig(
configPath string,
) (*Config, error) {
exeDir, err := getExecutableDir()
if err != nil {
log.Printf(
"Warning: cannot get executable directory: %v, using current directory",
err,
)
exeDir = "."
}
if configPath == "" {
configPath = "config.yaml"
}
finalConfigPath := configPath
if !filepath.IsAbs(finalConfigPath) {
finalConfigPath = filepath.Join(
exeDir,
finalConfigPath,
)
}
finalConfigPath, err = filepath.Abs(
finalConfigPath,
)
if err != nil {
return nil, fmt.Errorf(
"failed to resolve config path: %w",
err,
)
}
log.Printf(
"Loading config from %s",
finalConfigPath,
)
data, err := os.ReadFile(
finalConfigPath,
)
if err != nil {
if !os.IsNotExist(err) {
return nil, fmt.Errorf(
"failed to read config %s: %w",
finalConfigPath,
err,
)
}
// Конфига нет вообще:
// создаём новый default config.
cfg := defaultConfig()
if err := validateConfig(cfg); err != nil {
return nil, fmt.Errorf(
"default config is invalid: %w",
err,
)
}
cfgData, err := yaml.Marshal(cfg)
if err != nil {
return nil, fmt.Errorf(
"failed to encode default config: %w",
err,
)
}
configDir := filepath.Dir(
finalConfigPath,
)
if err := os.MkdirAll(
configDir,
0755,
); err != nil {
return nil, fmt.Errorf(
"failed to create config directory %s: %w",
configDir,
err,
)
}
if err := os.WriteFile(
finalConfigPath,
cfgData,
0600,
); err != nil {
return nil, fmt.Errorf(
"failed to create default config %s: %w",
finalConfigPath,
err,
)
}
log.Printf(
"Created default config at %s",
finalConfigPath,
)
resolveConfigPaths(
cfg,
exeDir,
)
return cfg, nil
}
/*
ВАЖНО:
Если config.yaml существует, НЕ используем defaultConfig()
как основу.
Иначе ошибочное/отсутствующее поле может незаметно получить
значение, зашитое в бинарник.
*/
var cfg Config
decoder := yaml.NewDecoder(
bytes.NewReader(data),
)
/*
Запрещаем неизвестные поля.
Например старое:
basic_auth:
вместо:
auth:
теперь вызовет ошибку при запуске.
*/
decoder.KnownFields(true)
if err := decoder.Decode(&cfg); err != nil {
return nil, fmt.Errorf(
"failed to parse YAML config %s: %w",
finalConfigPath,
err,
)
}
if err := validateConfig(&cfg); err != nil {
return nil, fmt.Errorf(
"invalid config %s: %w",
finalConfigPath,
err,
)
}
log.Printf(
"Config loaded: web_auth=%v username=%q session_timeout=%s",
cfg.Web.Auth.Enabled,
cfg.Web.Auth.Username,
cfg.Web.Auth.SessionTimeout,
)
resolveConfigPaths(
&cfg,
exeDir,
)
return &cfg, nil
}