// Package config implements the officeHours configuration // // It provides a struct type holding all necessary information for the programm. // The configuration can be read from a JSON file. package config import ( "encoding/json" "fmt" "html/template" "io/ioutil" "log" "net/mail" ) // A Config holds all the constants for the programm. // It is passed to most repositories. type Config struct { Server struct { ListenAddress string // Address on which the http server is supposed to listen ListenPort int // Port on which the http server is supposed to listen Protocol string // Protocol to access the server; either "http" or "https" Domain string // A string indicating the tool's base domain and path, e.g. "example.com/subfolder" } Date struct { MinuteGranularity int // Restricts the minutes on which office hours can start and end to multiples of it. MaxDuration int // Limits the length of office hours to minutes EarliestStartTime struct { Hour int Minute int } LatestStartTime struct { Hour int Minute int } } Request struct { SecretLength int // Length of the secret token for requests } Mailer struct { Type string // Send mail to "Stdout" or "Smtp" FromAddress string // Send mail from this address FromName template.HTML // Send mail from this name, e.g. "Office hours " SmtpHost string // Host to use as smarthost SmtpPort int // Port of the smarthost SmtpUseAuth bool // Set whether to use authentication on the smarthosthost SmtpIdentity string // Smarthost username SmtpPassword string // Smarthost password SupportMail string // separate mail address for support contact } SQL struct { Type string // Can be "SQLite" or "Mysql" SQLiteFile string // Path to SQLite file MysqlUser string MysqlPassword string MysqlHost string MysqlPort int MysqlDatabase string } Tutor struct { MailSuffix string // Restrict mailaddresses of tutors to suffixes. // e.g. "example.com" allowes "foo@example.com", "foo@sub.example.com", but not "foo@another.org" or "foo@barexample.com". } } // ReadConfigFile takes a file path as an argument and attempts to // unmarshal the content of the file into a struct containing a // configuration. func ReadConfigFile(filename string, conf *Config) error { configData, err := ioutil.ReadFile(filename) if err != nil { err = fmt.Errorf("Error reading config file: %w", err) log.Println(err.Error()) return err } err = json.Unmarshal(configData, conf) if err != nil { err = fmt.Errorf("Error parsing config file as json: %w", err) log.Println(err.Error()) return err } return validateConfig(conf) } // Checks config for sane values (e.g. correct port range or database type). func validateConfig(conf *Config) error { var err error if !(conf.Server.ListenPort >= 1 && conf.Server.ListenPort <= 65535) { err = fmt.Errorf("Validating config: Server port must be between 1 and 65535, but is %d.", conf.Server.ListenPort) log.Println(err.Error()) } if !(conf.Server.Protocol == "http" || conf.Server.Protocol == "https") { err = fmt.Errorf("Validating config: Server protocol must be 'http' or 'https', but is '%s'.", conf.Server.Protocol) log.Println(err.Error()) } if !(conf.Date.MinuteGranularity >= 1 && conf.Date.MinuteGranularity <= 60) { err = fmt.Errorf("Validating config: Minute granularity must be between 1 and 60, but is %d.", conf.Date.MinuteGranularity) log.Println(err.Error()) } if !(conf.Date.EarliestStartTime.Hour >= 0 && conf.Date.EarliestStartTime.Hour <= 23) { err = fmt.Errorf("Validating config: Earliest start time hour must be between 0 and 23, but is %d.", conf.Date.EarliestStartTime.Hour) log.Println(err.Error()) } if !(conf.Date.EarliestStartTime.Minute >= 0 && conf.Date.EarliestStartTime.Minute <= 60) { err = fmt.Errorf("Validating config: Earliest start time minute must be between 0 and 60, but is %d.", conf.Date.EarliestStartTime.Minute) log.Println(err.Error()) } if !(conf.Date.MaxDuration >= conf.Date.MinuteGranularity && conf.Date.MaxDuration <= 4*60) { err = fmt.Errorf("Validating config: Maximum duration must be between %d minute and 4 hours, but is %d.", conf.Date.MinuteGranularity, conf.Date.MaxDuration) log.Println(err.Error()) } if !(conf.Date.LatestStartTime.Hour >= 0 && conf.Date.LatestStartTime.Hour <= 23) { err = fmt.Errorf("Validating config: Latest start time hour must be between 0 and 23, but is %d.", conf.Date.LatestStartTime.Hour) log.Println(err.Error()) } if !(conf.Date.LatestStartTime.Minute >= 0 && conf.Date.LatestStartTime.Minute <= 60) { err = fmt.Errorf("Validating config: Latest start time minute must be between 0 and 60, but is %d.", conf.Date.LatestStartTime.Minute) log.Println(err.Error()) } if !(conf.Date.EarliestStartTime.Hour < conf.Date.LatestStartTime.Hour || (conf.Date.EarliestStartTime.Hour == conf.Date.LatestStartTime.Hour && conf.Date.EarliestStartTime.Minute < conf.Date.LatestStartTime.Minute)) { err = fmt.Errorf("Validating config: Latest start time minute must be after earliest start time.") log.Println(err.Error()) } if !(conf.Request.SecretLength >= 5 && conf.Request.SecretLength <= 50) { err = fmt.Errorf("Validating config: Requests' secret length must be between 5 and 50, but is %d.", conf.Request.SecretLength) log.Println(err.Error()) } if !(conf.Mailer.Type == "Stdout" || conf.Mailer.Type == "Smtp") { err = fmt.Errorf("Validating config: Mailer type must be 'Stdout' or 'Smtp', but is '%s'.", conf.Mailer.Type) log.Println(err.Error()) } mailFromAddress, mailFromAddressErr := mail.ParseAddress(conf.Mailer.FromAddress) if !(mailFromAddressErr == nil) { err = fmt.Errorf("Validating config: Mail FromAddress could not be parsed (%w)", mailFromAddressErr) log.Println(err) } else { if !(mailFromAddress.Name == "") { err = fmt.Errorf("Validating config: Mail FromAddress must not contain a name value, but has '%s'", mailFromAddress.Name) log.Println(err) } } _, mailFromNameErr := mail.ParseAddress(string(conf.Mailer.FromName)) if !(mailFromNameErr == nil) { err = fmt.Errorf("Validating config: Mail FromName could not be parsed (%w)", mailFromNameErr) log.Println(err) } supportMail, mailSupportMailErr := mail.ParseAddress(string(conf.Mailer.SupportMail)) if !(mailSupportMailErr == nil) { err = fmt.Errorf("Validating config: SupportMail could not be parsed (%w)", mailSupportMailErr) log.Println(err) } if !(supportMail.Name == "") { err = fmt.Errorf("Validating config: SupportMail must not contain a name") log.Println(err) } if !(conf.SQL.Type == "SQLite" || conf.SQL.Type == "Mysql") { err = fmt.Errorf("Validating config: SQL type must be 'SQLite' or 'Mysql', but is '%s'.", conf.SQL.Type) log.Println(err.Error()) } return err }