// Package config contains distillery configuration package config import ( "hash/fnv" "math/rand" "reflect" "time" "github.com/tkw1536/pkglib/reflectx" "github.com/tkw1536/pkglib/yamlx" "gopkg.in/yaml.v3" _ "embed" ) // Config represents the configuration of a WissKI Distillery. // // Config is read from a byte stream using [Unmarshal]. // // Config contains many methods that do not require any interaction with any running components. // Methods that require running components are instead store inside the [Distillery] or an appropriate [Component]. type Config struct { Listen ListenConfig `yaml:"listen" recurse:"true"` Paths PathsConfig `yaml:"paths" recurse:"true"` HTTP HTTPConfig `yaml:"http" recurse:"true"` Home HomeConfig `yaml:"home" recurse:"true"` Docker DockerConfig `yaml:"docker" recurse:"true"` SQL SQLConfig `yaml:"sql" recurse:"true"` TS TSConfig `yaml:"triplestore" recurse:"true"` // Maximum age for backup in days MaxBackupAge time.Duration `yaml:"age" validate:"duration"` // Various components use password-based-authentication. // These passwords are generated automatically. // This variable can be used to determine their length. PasswordLength int `yaml:"password_length" default:"64" validate:"positive"` // session secret holds the secret for login SessionSecret string `yaml:"session_secret" validate:"nonempty" sensitive:"true"` // interval to trigger distillery cron tasks in CronInterval time.Duration `yaml:"cron_interval" default:"10m" validate:"duration"` // ConfigPath is the path this configuration was loaded from (if any) ConfigPath string `yaml:"-"` } func zeroSensitive(v reflect.Value) { reflectx.IterateFields(v.Type(), func(field reflect.StructField, index int) (stop bool) { // if we set the recurse tag, recurse into it if _, ok := field.Tag.Lookup("recurse"); ok { zeroSensitive(v.FieldByName(field.Name)) } // if the field is sensitive, set the zero value! if _, ok := field.Tag.Lookup("sensitive"); ok { v.FieldByName(field.Name).Set(reflect.Zero(field.Type)) } return false }) } func (config Config) MarshalSensitive() string { // zero out all the sensitive fields zeroSensitive(reflect.ValueOf(&config).Elem()) // marshal the result result, err := Marshal(&config, nil) if err != nil { return "" } return string(result) } //go:embed config.yml var configBytes []byte // Marshal marshals this configuration in nicely formatted form. // Where possible, this will maintain yaml comments. // // Previous may optionally provide the bytes of a previous configuration file to replace settings in. // The previous yaml file must be a valid configuration yaml, meaning all fields should be set. // When previous is of length 0, the default configuration yaml will be used instead. func Marshal(config *Config, previous []byte) ([]byte, error) { if len(previous) == 0 { previous = configBytes } // load the template yaml template := new(yaml.Node) if err := yaml.Unmarshal(previous, template); err != nil { return nil, err } // load the config yaml cfg, err := yamlx.Marshal(config) if err != nil { return nil, err } // transplant the configuration yaml into the template if err := yamlx.Transplant(template, cfg, true); err != nil { return nil, err } // marshal it again as a set of bytes return yaml.Marshal(template) } // CSRFSecret return the csrfSecret derived from the session secret func (config *Config) CSRFSecret() []byte { // take the hash of the secret h := fnv.New32a() h.Write([]byte(config.SessionSecret)) // seed a random number generator rand := rand.New(rand.NewSource(int64(h.Sum32()))) // take a bunch of bytes from it secret := make([]byte, 32) rand.Read(secret) return secret }