|
@@ -1,171 +1,208 @@
|
|
|
package guerrilla
|
|
|
|
|
|
import (
|
|
|
+ "encoding/json"
|
|
|
"errors"
|
|
|
"fmt"
|
|
|
- _ "fmt"
|
|
|
"github.com/flashmob/go-guerrilla/backends"
|
|
|
"github.com/flashmob/go-guerrilla/log"
|
|
|
"io/ioutil"
|
|
|
- "os"
|
|
|
+ "time"
|
|
|
)
|
|
|
|
|
|
-type SMTP struct {
|
|
|
- config *AppConfig
|
|
|
- logger log.Logger
|
|
|
- backend backends.Backend
|
|
|
- g Guerrilla
|
|
|
+type Daemon struct {
|
|
|
+ Config *AppConfig
|
|
|
+ Logger log.Logger
|
|
|
+ Backend backends.Backend
|
|
|
+
|
|
|
+ g Guerrilla
|
|
|
+
|
|
|
+ configLoadTime time.Time
|
|
|
}
|
|
|
|
|
|
const defaultInterface = "127.0.0.1:2525"
|
|
|
|
|
|
-// configureDefaults fills in default server settings for values that were not configured
|
|
|
-func (s *SMTP) configureDefaults() error {
|
|
|
- if s.config.LogFile == "" {
|
|
|
- s.config.LogFile = log.OutputStderr.String()
|
|
|
- }
|
|
|
- if s.config.LogLevel == "" {
|
|
|
- s.config.LogLevel = "debug"
|
|
|
- }
|
|
|
- if len(s.config.AllowedHosts) == 0 {
|
|
|
- if h, err := os.Hostname(); err != nil {
|
|
|
+// AddProcessor adds a processor constructor to the backend.
|
|
|
+// name is the identifier to be used in the config. See backends docs for more info.
|
|
|
+func (d *Daemon) AddProcessor(name string, pc backends.ProcessorConstructor) {
|
|
|
+ backends.Svc.AddProcessor(name, pc)
|
|
|
+}
|
|
|
+
|
|
|
+// Starts the daemon, initializing d.Config, d.Logger and d.Backend with defaults
|
|
|
+// can only be called once through the lifetime of the program
|
|
|
+func (d *Daemon) Start() (err error) {
|
|
|
+ if d.g == nil {
|
|
|
+ if d.Config == nil {
|
|
|
+ d.Config = &AppConfig{}
|
|
|
+ }
|
|
|
+ if err = d.configureDefaults(); err != nil {
|
|
|
return err
|
|
|
- } else {
|
|
|
- s.config.AllowedHosts = append(s.config.AllowedHosts, h)
|
|
|
}
|
|
|
- }
|
|
|
- h, err := os.Hostname()
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
- if len(s.config.Servers) == 0 {
|
|
|
- sc := ServerConfig{}
|
|
|
- sc.LogFile = s.config.LogFile
|
|
|
- sc.ListenInterface = defaultInterface
|
|
|
- sc.IsEnabled = true
|
|
|
- sc.Hostname = h
|
|
|
- sc.MaxClients = 100
|
|
|
- sc.Timeout = 30
|
|
|
- sc.MaxSize = 10 << 20 // 10 Mebibytes
|
|
|
- s.config.Servers = append(s.config.Servers, sc)
|
|
|
- } else {
|
|
|
- // make sure each server has defaults correctly configured
|
|
|
- for i := range s.config.Servers {
|
|
|
- if s.config.Servers[i].Hostname == "" {
|
|
|
- s.config.Servers[i].Hostname = h
|
|
|
- }
|
|
|
- if s.config.Servers[i].MaxClients == 0 {
|
|
|
- s.config.Servers[i].MaxClients = 100
|
|
|
- }
|
|
|
- if s.config.Servers[i].Timeout == 0 {
|
|
|
- s.config.Servers[i].Timeout = 20
|
|
|
- }
|
|
|
- if s.config.Servers[i].MaxSize == 0 {
|
|
|
- s.config.Servers[i].MaxSize = 10 << 20 // 10 Mebibytes
|
|
|
- }
|
|
|
- if s.config.Servers[i].ListenInterface == "" {
|
|
|
- return errors.New(fmt.Sprintf("Listen interface not specified for server at index %d", i))
|
|
|
- }
|
|
|
- if s.config.Servers[i].LogFile == "" {
|
|
|
- s.config.Servers[i].LogFile = s.config.LogFile
|
|
|
+ if d.Logger == nil {
|
|
|
+ d.Logger, err = log.GetLogger(d.Config.LogFile)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
}
|
|
|
- // validate the server config
|
|
|
- err = s.config.Servers[i].Validate()
|
|
|
+ d.Logger.SetLevel(d.Config.LogLevel)
|
|
|
+ }
|
|
|
+ if d.Backend == nil {
|
|
|
+ d.Backend, err = backends.New(d.Config.BackendConfig, d.Logger)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
}
|
|
|
+ d.g, err = New(d.Config, d.Backend, d.Logger)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ }
|
|
|
+ err = d.g.Start()
|
|
|
+ if err == nil {
|
|
|
+ if err := d.resetLogger(); err == nil {
|
|
|
+ d.log().Infof("main log configured to %s", d.Config.LogFile)
|
|
|
+ }
|
|
|
|
|
|
}
|
|
|
- return nil
|
|
|
+ return err
|
|
|
+}
|
|
|
|
|
|
+// Shuts down the daemon, including servers and backend.
|
|
|
+// Do not call Start on it again, use a new server.
|
|
|
+func (d *Daemon) Shutdown() {
|
|
|
+ if d.g != nil {
|
|
|
+ d.g.Shutdown()
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-func (s *SMTP) configureDefaultBackend() error {
|
|
|
- h, err := os.Hostname()
|
|
|
+// ReadConfig reads in the config from a JSON file.
|
|
|
+func (d *Daemon) ReadConfig(path string) error {
|
|
|
+ data, err := ioutil.ReadFile(path)
|
|
|
if err != nil {
|
|
|
+ return fmt.Errorf("Could not read config file: %s", err.Error())
|
|
|
+ }
|
|
|
+ d.Config = &AppConfig{}
|
|
|
+ if err := d.Config.Load(data); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
- if len(s.config.BackendConfig) == 0 {
|
|
|
- bcfg := backends.BackendConfig{
|
|
|
- "log_received_mails": true,
|
|
|
- "save_workers_size": 1,
|
|
|
- "process_stack": "HeadersParser|Header|Debugger",
|
|
|
- "primary_mail_host": h,
|
|
|
- }
|
|
|
- s.backend, err = backends.New(bcfg, s.logger)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
+ d.configLoadTime = time.Now()
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// SetConfig is same as ReadConfig, except you can pass AppConfig directly
|
|
|
+func (d *Daemon) SetConfig(c AppConfig) error {
|
|
|
+ // Config.Load takes []byte so we need to serialize
|
|
|
+ data, err := json.Marshal(c)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ // put the data into a fresh d.Config
|
|
|
+ d.Config = &AppConfig{}
|
|
|
+ if err := d.Config.Load(data); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ d.configLoadTime = time.Now()
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// Reload a config using the passed in AppConfig and emit config change events
|
|
|
+func (d *Daemon) ReloadConfig(c AppConfig) error {
|
|
|
+ if d.Config == nil {
|
|
|
+ return errors.New("d.Config nil")
|
|
|
+ }
|
|
|
+ oldConfig := *d.Config
|
|
|
+ err := d.SetConfig(c)
|
|
|
+ if err != nil {
|
|
|
+ d.log().WithError(err).Error("Error while reloading config")
|
|
|
+ return err
|
|
|
} else {
|
|
|
- if _, ok := s.config.BackendConfig["process_stack"]; !ok {
|
|
|
- s.config.BackendConfig["process_stack"] = "HeadersParser|Header|Debugger"
|
|
|
- }
|
|
|
- if _, ok := s.config.BackendConfig["primary_mail_host"]; !ok {
|
|
|
- s.config.BackendConfig["primary_mail_host"] = h
|
|
|
- }
|
|
|
- if _, ok := s.config.BackendConfig["save_workers_size"]; !ok {
|
|
|
- s.config.BackendConfig["save_workers_size"] = 1
|
|
|
- }
|
|
|
+ d.log().Infof("Configuration was reloaded at %s", d.configLoadTime)
|
|
|
+ d.Config.EmitChangeEvents(&oldConfig, d.g)
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
|
|
|
- if _, ok := s.config.BackendConfig["log_received_mails"]; !ok {
|
|
|
- s.config.BackendConfig["log_received_mails"] = false
|
|
|
- }
|
|
|
- s.backend, err = backends.New(s.config.BackendConfig, s.logger)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
+// Reload a config from a file and emit config change events
|
|
|
+func (d *Daemon) ReloadConfigFile(path string) error {
|
|
|
+ if d.Config == nil {
|
|
|
+ return errors.New("d.Config nil")
|
|
|
}
|
|
|
+ var oldConfig AppConfig
|
|
|
+ oldConfig = *d.Config
|
|
|
+ err := d.ReadConfig(path)
|
|
|
|
|
|
+ if err != nil {
|
|
|
+ d.log().WithError(err).Error("Error while reloading config from file")
|
|
|
+ return err
|
|
|
+ } else {
|
|
|
+ d.log().Infof("Configuration was reloaded at %s", d.configLoadTime)
|
|
|
+ d.Config.EmitChangeEvents(&oldConfig, d.g)
|
|
|
+ }
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
-func (s *SMTP) Start() (err error) {
|
|
|
- if s.g == nil {
|
|
|
- if s.config == nil {
|
|
|
- s.config = &AppConfig{}
|
|
|
- }
|
|
|
- err = s.configureDefaults()
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
+// ReopenLogs re-opens all log files. Typically, one would call this after rotating logs
|
|
|
+func (d *Daemon) ReopenLogs() {
|
|
|
+ d.Config.EmitLogReopenEvents(d.g)
|
|
|
+}
|
|
|
|
|
|
- if s.logger == nil {
|
|
|
- s.logger, err = log.GetLogger(s.config.LogFile)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
- }
|
|
|
- if s.backend == nil {
|
|
|
- err = s.configureDefaultBackend()
|
|
|
- if err != nil {
|
|
|
- return
|
|
|
- }
|
|
|
- }
|
|
|
- s.g, err = New(s.config, s.backend, s.logger)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
+// Subscribe for subscribing to config change events
|
|
|
+func (d *Daemon) Subscribe(topic Event, fn interface{}) error {
|
|
|
+ return d.g.Subscribe(topic, fn)
|
|
|
+}
|
|
|
|
|
|
- }
|
|
|
- return s.g.Start()
|
|
|
+// for publishing config change events
|
|
|
+func (d *Daemon) Publish(topic Event, args ...interface{}) {
|
|
|
+ d.g.Publish(topic, args...)
|
|
|
}
|
|
|
|
|
|
-func (s *SMTP) Shutdown() {
|
|
|
- s.g.Shutdown()
|
|
|
+// for unsubscribing from config change events
|
|
|
+func (d *Daemon) Unsubscribe(topic Event, handler interface{}) error {
|
|
|
+ return d.g.Unsubscribe(topic, handler)
|
|
|
}
|
|
|
|
|
|
-// ReadConfig reads in the config from a json file.
|
|
|
-func (s *SMTP) ReadConfig(path string) error {
|
|
|
- data, err := ioutil.ReadFile(path)
|
|
|
+// log returns a logger that implements our log.Logger interface.
|
|
|
+// level is set to "info" by default
|
|
|
+func (d *Daemon) log() log.Logger {
|
|
|
+ if d.Logger != nil {
|
|
|
+ return d.Logger
|
|
|
+ }
|
|
|
+ out := log.OutputStderr.String()
|
|
|
+ if d.Config != nil && len(d.Config.LogFile) > 0 {
|
|
|
+ out = d.Config.LogFile
|
|
|
+ }
|
|
|
+ l, err := log.GetLogger(out)
|
|
|
+ if err == nil {
|
|
|
+ l.SetLevel("info")
|
|
|
+ }
|
|
|
+ return l
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+// set the default values for the servers and backend config options
|
|
|
+func (d *Daemon) configureDefaults() error {
|
|
|
+ err := d.Config.setDefaults()
|
|
|
if err != nil {
|
|
|
- return fmt.Errorf("Could not read config file: %s", err.Error())
|
|
|
+ return err
|
|
|
}
|
|
|
- if s.config == nil {
|
|
|
- s.config = &AppConfig{}
|
|
|
+ if d.Backend == nil {
|
|
|
+ err = d.Config.setBackendDefaults()
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
}
|
|
|
- if err := s.config.Load(data); err != nil {
|
|
|
+ return err
|
|
|
+}
|
|
|
+
|
|
|
+// resetLogger sets the logger to the one specified in the config.
|
|
|
+// This is because at the start, the daemon may be logging to stderr,
|
|
|
+// then attaches to the logs once the config is loaded.
|
|
|
+// This will propagate down to the servers / backend too.
|
|
|
+func (d *Daemon) resetLogger() error {
|
|
|
+ l, err := log.GetLogger(d.Config.LogFile)
|
|
|
+ if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
+ d.Logger = l
|
|
|
+ d.g.SetLogger(d.Logger)
|
|
|
return nil
|
|
|
}
|