1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2025-12-18 18:17:03 +00:00

Refactor configuration parser to use tables

This commit is contained in:
James Hillyerd
2017-01-22 18:57:03 -08:00
parent fa28fa57f8
commit 62b77dfe5e
2 changed files with 161 additions and 320 deletions

View File

@@ -1,17 +1,17 @@
package config package config
import ( import (
"container/list"
"fmt" "fmt"
"net" "net"
"os" "os"
"sort"
"strings" "strings"
"github.com/robfig/config" "github.com/robfig/config"
) )
// SMTPConfig contains the SMTP server configuration - not using pointers // SMTPConfig contains the SMTP server configuration - not using pointers so that we can pass around
// so that we can pass around copies of the object safely. // copies of the object safely.
type SMTPConfig struct { type SMTPConfig struct {
IP4address net.IP IP4address net.IP
IP4port int IP4port int
@@ -52,6 +52,11 @@ type DataStoreConfig struct {
MailboxMsgCap int MailboxMsgCap int
} }
const (
missingErrorFmt = "[%v] missing required option %q"
parseErrorFmt = "[%v] option %q error: %v"
)
var ( var (
// Version of this build, set by main // Version of this build, set by main
Version = "" Version = ""
@@ -60,13 +65,14 @@ var (
BuildDate = "" BuildDate = ""
// Config is our global robfig/config object // Config is our global robfig/config object
Config *config.Config Config *config.Config
logLevel string
// Parsed specific configs // Parsed specific configs
smtpConfig *SMTPConfig smtpConfig = &SMTPConfig{}
pop3Config *POP3Config pop3Config = &POP3Config{}
webConfig *WebConfig webConfig = &WebConfig{}
dataStoreConfig *DataStoreConfig dataStoreConfig = &DataStoreConfig{}
) )
// GetSMTPConfig returns a copy of the SmtpConfig object // GetSMTPConfig returns a copy of the SmtpConfig object
@@ -89,338 +95,174 @@ func GetDataStoreConfig() DataStoreConfig {
return *dataStoreConfig return *dataStoreConfig
} }
// LoadConfig loads the specified configuration file into inbucket.Config // GetLogLevel returns the configured log level
// and performs validations on it. func GetLogLevel() string {
return logLevel
}
// LoadConfig loads the specified configuration file into inbucket.Config and performs validations
// on it.
func LoadConfig(filename string) error { func LoadConfig(filename string) error {
var err error var err error
Config, err = config.ReadDefault(filename) Config, err = config.ReadDefault(filename)
if err != nil { if err != nil {
return err return err
} }
// Validation error messages
messages := list.New() messages := make([]string, 0)
// Validate sections // Validate sections
requireSection(messages, "logging") for _, s := range []string{"logging", "smtp", "pop3", "web", "datastore"} {
requireSection(messages, "smtp") if !Config.HasSection(s) {
requireSection(messages, "pop3") messages = append(messages,
requireSection(messages, "web") fmt.Sprintf("Config section [%v] is required", s))
requireSection(messages, "datastore") }
if messages.Len() > 0 { }
// Return immediately if config is missing entire sections
if len(messages) > 0 {
fmt.Fprintln(os.Stderr, "Error(s) validating configuration:") fmt.Fprintln(os.Stderr, "Error(s) validating configuration:")
for e := messages.Front(); e != nil; e = e.Next() { for _, m := range messages {
fmt.Fprintln(os.Stderr, " -", e.Value.(string)) fmt.Fprintln(os.Stderr, " -", m)
} }
return fmt.Errorf("Failed to validate configuration") return fmt.Errorf("Failed to validate configuration")
} }
// Load string config options
// Validate options stringOptions := []struct {
requireOption(messages, "logging", "level") section string
requireOption(messages, "smtp", "ip4.address") name string
requireOption(messages, "smtp", "ip4.port") target *string
requireOption(messages, "smtp", "domain") required bool
requireOption(messages, "smtp", "max.recipients") }{
requireOption(messages, "smtp", "max.idle.seconds") {"logging", "level", &logLevel, true},
requireOption(messages, "smtp", "max.message.bytes") {"smtp", "domain", &smtpConfig.Domain, true},
requireOption(messages, "smtp", "store.messages") {"smtp", "domain.nostore", &smtpConfig.DomainNoStore, false},
requireOption(messages, "pop3", "ip4.address") {"pop3", "domain", &pop3Config.Domain, true},
requireOption(messages, "pop3", "ip4.port") {"web", "template.dir", &webConfig.TemplateDir, true},
requireOption(messages, "pop3", "domain") {"web", "public.dir", &webConfig.PublicDir, true},
requireOption(messages, "pop3", "max.idle.seconds") {"web", "greeting.file", &webConfig.GreetingFile, true},
requireOption(messages, "web", "ip4.address") {"web", "cookie.auth.key", &webConfig.CookieAuthKey, false},
requireOption(messages, "web", "ip4.port") {"datastore", "path", &dataStoreConfig.Path, true},
requireOption(messages, "web", "template.dir") }
requireOption(messages, "web", "template.cache") for _, opt := range stringOptions {
requireOption(messages, "web", "public.dir") str, err := Config.String(opt.section, opt.name)
requireOption(messages, "web", "monitor.visible") if Config.HasOption(opt.section, opt.name) && err != nil {
requireOption(messages, "web", "monitor.history") messages = append(messages, fmt.Sprintf(parseErrorFmt, opt.section, opt.name, err))
requireOption(messages, "datastore", "path") continue
requireOption(messages, "datastore", "retention.minutes")
requireOption(messages, "datastore", "retention.sleep.millis")
requireOption(messages, "datastore", "mailbox.message.cap")
// Return error if validations failed
if messages.Len() > 0 {
fmt.Fprintln(os.Stderr, "Error(s) validating configuration:")
for e := messages.Front(); e != nil; e = e.Next() {
fmt.Fprintln(os.Stderr, " -", e.Value.(string))
} }
return fmt.Errorf("Failed to validate configuration") if str == "" && opt.required {
messages = append(messages, fmt.Sprintf(missingErrorFmt, opt.section, opt.name))
}
*opt.target = str
} }
// Load boolean config options
if err = parseSMTPConfig(); err != nil { boolOptions := []struct {
return err section string
name string
target *bool
required bool
}{
{"smtp", "store.messages", &smtpConfig.StoreMessages, true},
{"web", "template.cache", &webConfig.TemplateCache, true},
{"web", "monitor.visible", &webConfig.MonitorVisible, true},
} }
for _, opt := range boolOptions {
if err = parsePOP3Config(); err != nil { if Config.HasOption(opt.section, opt.name) {
return err flag, err := Config.Bool(opt.section, opt.name)
if err != nil {
messages = append(messages, fmt.Sprintf(parseErrorFmt, opt.section, opt.name, err))
}
*opt.target = flag
} else {
if opt.required {
messages = append(messages, fmt.Sprintf(missingErrorFmt, opt.section, opt.name))
}
}
} }
// Load integer config options
if err = parseWebConfig(); err != nil { intOptions := []struct {
return err section string
name string
target *int
required bool
}{
{"smtp", "ip4.port", &smtpConfig.IP4port, true},
{"smtp", "max.recipients", &smtpConfig.MaxRecipients, true},
{"smtp", "max.idle.seconds", &smtpConfig.MaxIdleSeconds, true},
{"smtp", "max.message.bytes", &smtpConfig.MaxMessageBytes, true},
{"pop3", "ip4.port", &pop3Config.IP4port, true},
{"pop3", "max.idle.seconds", &pop3Config.MaxIdleSeconds, true},
{"web", "ip4.port", &webConfig.IP4port, true},
{"web", "monitor.history", &webConfig.MonitorHistory, true},
{"datastore", "retention.minutes", &dataStoreConfig.RetentionMinutes, true},
{"datastore", "retention.sleep.millis", &dataStoreConfig.RetentionSleep, true},
{"datastore", "mailbox.message.cap", &dataStoreConfig.MailboxMsgCap, true},
} }
for _, opt := range intOptions {
if err = parseDataStoreConfig(); err != nil { if Config.HasOption(opt.section, opt.name) {
return err num, err := Config.Int(opt.section, opt.name)
if err != nil {
messages = append(messages, fmt.Sprintf(parseErrorFmt, opt.section, opt.name, err))
}
*opt.target = num
} else {
if opt.required {
messages = append(messages, fmt.Sprintf(missingErrorFmt, opt.section, opt.name))
}
}
} }
// Load IP address config options
return nil ipOptions := []struct {
} section string
name string
// parseLoggingConfig trying to catch config errors early target *net.IP
func parseLoggingConfig() error { required bool
section := "logging" }{
{"smtp", "ip4.address", &smtpConfig.IP4address, true},
option := "level" {"pop3", "ip4.address", &pop3Config.IP4address, true},
str, err := Config.String(section, option) {"web", "ip4.address", &webConfig.IP4address, true},
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
} }
switch strings.ToUpper(str) { for _, opt := range ipOptions {
if Config.HasOption(opt.section, opt.name) {
str, err := Config.String(opt.section, opt.name)
if err != nil {
messages = append(messages, fmt.Sprintf(parseErrorFmt, opt.section, opt.name, err))
continue
}
addr := net.ParseIP(str)
if addr == nil {
messages = append(messages,
fmt.Sprintf("Failed to parse IP [%v]%v: %q", opt.section, opt.name, str))
continue
}
addr = addr.To4()
if addr == nil {
messages = append(messages,
fmt.Sprintf("Failed to parse IP [%v]%v: %q not IPv4!",
opt.section, opt.name, str))
}
*opt.target = addr
} else {
if opt.required {
messages = append(messages, fmt.Sprintf(missingErrorFmt, opt.section, opt.name))
}
}
}
// Validate log level
switch strings.ToUpper(logLevel) {
case "":
// Missing was already reported
case "TRACE", "INFO", "WARN", "ERROR": case "TRACE", "INFO", "WARN", "ERROR":
default: default:
return fmt.Errorf("Invalid value provided for [%v]%v: '%v'", section, option, str) messages = append(messages,
fmt.Sprintf("Invalid value provided for [logging]level: %q", logLevel))
} }
return nil // Print messages and return error if any validations failed
} if len(messages) > 0 {
fmt.Fprintln(os.Stderr, "Error(s) validating configuration:")
// parseSMTPConfig trying to catch config errors early sort.Strings(messages)
func parseSMTPConfig() error { for _, m := range messages {
smtpConfig = new(SMTPConfig) fmt.Fprintln(os.Stderr, " -", m)
section := "smtp"
// Parse IP4 address only, error on IP6.
option := "ip4.address"
str, err := Config.String(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
addr := net.ParseIP(str)
if addr == nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, str)
}
addr = addr.To4()
if addr == nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v' not IPv4!", section, option, str)
}
smtpConfig.IP4address = addr
option = "ip4.port"
smtpConfig.IP4port, err = Config.Int(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
option = "domain"
str, err = Config.String(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
smtpConfig.Domain = str
option = "domain.nostore"
if Config.HasOption(section, option) {
str, err = Config.String(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
} }
smtpConfig.DomainNoStore = str return fmt.Errorf("Failed to validate configuration")
} }
option = "max.recipients"
smtpConfig.MaxRecipients, err = Config.Int(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
option = "max.idle.seconds"
smtpConfig.MaxIdleSeconds, err = Config.Int(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
option = "max.message.bytes"
smtpConfig.MaxMessageBytes, err = Config.Int(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
option = "store.messages"
flag, err := Config.Bool(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
smtpConfig.StoreMessages = flag
return nil return nil
} }
// parsePOP3Config trying to catch config errors early
func parsePOP3Config() error {
pop3Config = new(POP3Config)
section := "pop3"
// Parse IP4 address only, error on IP6.
option := "ip4.address"
str, err := Config.String(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
addr := net.ParseIP(str)
if addr == nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, str)
}
addr = addr.To4()
if addr == nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v' not IPv4!", section, option, str)
}
pop3Config.IP4address = addr
option = "ip4.port"
pop3Config.IP4port, err = Config.Int(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
option = "domain"
str, err = Config.String(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
pop3Config.Domain = str
option = "max.idle.seconds"
pop3Config.MaxIdleSeconds, err = Config.Int(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
return nil
}
// parseWebConfig trying to catch config errors early
func parseWebConfig() error {
webConfig = new(WebConfig)
section := "web"
// Parse IP4 address only, error on IP6.
option := "ip4.address"
str, err := Config.String(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
addr := net.ParseIP(str)
if addr == nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, str)
}
addr = addr.To4()
if addr == nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v' not IPv4!", section, option, str)
}
webConfig.IP4address = addr
option = "ip4.port"
webConfig.IP4port, err = Config.Int(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
option = "template.dir"
str, err = Config.String(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
webConfig.TemplateDir = str
option = "template.cache"
flag, err := Config.Bool(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
webConfig.TemplateCache = flag
option = "public.dir"
str, err = Config.String(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
webConfig.PublicDir = str
option = "greeting.file"
str, err = Config.String(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
webConfig.GreetingFile = str
option = "monitor.visible"
flag, err = Config.Bool(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
webConfig.MonitorVisible = flag
option = "monitor.history"
webConfig.MonitorHistory, err = Config.Int(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
option = "cookie.auth.key"
if Config.HasOption(section, option) {
str, err = Config.String(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
webConfig.CookieAuthKey = str
}
return nil
}
// parseDataStoreConfig trying to catch config errors early
func parseDataStoreConfig() error {
dataStoreConfig = new(DataStoreConfig)
section := "datastore"
option := "path"
str, err := Config.String(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
dataStoreConfig.Path = str
option = "retention.minutes"
dataStoreConfig.RetentionMinutes, err = Config.Int(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
option = "retention.sleep.millis"
dataStoreConfig.RetentionSleep, err = Config.Int(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
option = "mailbox.message.cap"
dataStoreConfig.MailboxMsgCap, err = Config.Int(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
return nil
}
// requireSection checks that a [section] is defined in the configuration file,
// appending a message if not.
func requireSection(messages *list.List, section string) {
if !Config.HasSection(section) {
messages.PushBack(fmt.Sprintf("Config section [%v] is required", section))
}
}
// requireOption checks that 'option' is defined in [section] of the config file,
// appending a message if not.
func requireOption(messages *list.List, section string, option string) {
if !Config.HasOption(section, option) {
messages.PushBack(fmt.Sprintf("Config option '%v' is required in section [%v]", option, section))
}
}

View File

@@ -89,8 +89,7 @@ func main() {
signal.Notify(sigChan, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT) signal.Notify(sigChan, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT)
// Initialize logging // Initialize logging
level, _ := config.Config.String("logging", "level") log.SetLogLevel(config.GetLogLevel())
log.SetLogLevel(level)
if err := log.Initialize(*logfile); err != nil { if err := log.Initialize(*logfile); err != nil {
fmt.Fprintf(os.Stderr, "%v", err) fmt.Fprintf(os.Stderr, "%v", err)
os.Exit(1) os.Exit(1)