123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535 |
- package guerrilla
- import (
- "crypto/tls"
- "encoding/json"
- "errors"
- "fmt"
- "os"
- "reflect"
- "strings"
- "time"
- "github.com/flashmob/go-guerrilla/backends"
- "github.com/flashmob/go-guerrilla/dashboard"
- "github.com/flashmob/go-guerrilla/log"
- )
- // AppConfig is the holder of the configuration of the app
- type AppConfig struct {
- // Servers can have one or more items.
- /// Defaults to 1 server listening on 127.0.0.1:2525
- Servers []ServerConfig `json:"servers"`
- // AllowedHosts lists which hosts to accept email for. Defaults to os.Hostname
- AllowedHosts []string `json:"allowed_hosts"`
- // PidFile is the path for writing out the process id. No output if empty
- PidFile string `json:"pid_file"`
- // LogFile is where the logs go. Use path to file, or "stderr", "stdout"
- // or "off". Default "stderr"
- LogFile string `json:"log_file,omitempty"`
- // LogLevel controls the lowest level we log.
- // "info", "debug", "error", "panic". Default "info"
- LogLevel string `json:"log_level,omitempty"`
- // BackendConfig configures the email envelope processing backend
- BackendConfig backends.BackendConfig `json:"backend_config"`
- // Dashboard config configures how analytics are gathered and displayed
- Dashboard dashboard.Config `json:"dashboard"`
- }
- // ServerConfig specifies config options for a single server
- type ServerConfig struct {
- // IsEnabled set to true to start the server, false will ignore it
- IsEnabled bool `json:"is_enabled"`
- // Hostname will be used in the server's reply to HELO/EHLO. If TLS enabled
- // make sure that the Hostname matches the cert. Defaults to os.Hostname()
- Hostname string `json:"host_name"`
- // MaxSize is the maximum size of an email that will be accepted for delivery.
- // Defaults to 10 Mebibytes
- MaxSize int64 `json:"max_size"`
- // TLS Configuration
- TLS ServerTLSConfig `json:"tls,omitempty"`
- // Timeout specifies the connection timeout in seconds. Defaults to 30
- Timeout int `json:"timeout"`
- // Listen interface specified in <ip>:<port> - defaults to 127.0.0.1:2525
- ListenInterface string `json:"listen_interface"`
- // MaxClients controls how many maximum clients we can handle at once.
- // Defaults to defaultMaxClients
- MaxClients int `json:"max_clients"`
- // LogFile is where the logs go. Use path to file, or "stderr", "stdout" or "off".
- // defaults to AppConfig.Log file setting
- LogFile string `json:"log_file,omitempty"`
- // XClientOn when using a proxy such as Nginx, XCLIENT command is used to pass the
- // original client's IP address & client's HELO
- XClientOn bool `json:"xclient_on,omitempty"`
- }
- type ServerTLSConfig struct {
- // StartTLSOn should we offer STARTTLS command. Cert must be valid.
- // False by default
- StartTLSOn bool `json:"start_tls_on,omitempty"`
- // AlwaysOn run this server as a pure TLS server, i.e. SMTPS
- AlwaysOn bool `json:"tls_always_on,omitempty"`
- // PrivateKeyFile path to cert private key in PEM format.
- PrivateKeyFile string `json:"private_key_file"`
- // PublicKeyFile path to cert (public key) chain in PEM format.
- PublicKeyFile string `json:"public_key_file"`
- // TLS Protocols to use. [0] = min, [1]max
- // Use Go's default if empty
- Protocols []string `json:"protocols,omitempty"`
- // TLS Ciphers to use.
- // Use Go's default if empty
- Ciphers []string `json:"ciphers,omitempty"`
- // TLS Curves to use.
- // Use Go's default if empty
- Curves []string `json:"curves,omitempty"`
- // TLS Root cert authorities to use. "A PEM encoded CA's certificate file.
- // Defaults to system's root CA file if empty
- RootCAs string `json:"root_cas_file,omitempty"`
- // declares the policy the server will follow for TLS Client Authentication.
- // Use Go's default if empty
- ClientAuthType string `json:"client_auth_type,omitempty"`
- // controls whether the server selects the
- // client's most preferred cipher suite
- PreferServerCipherSuites bool `json:"prefer_server_cipher_suites,omitempty"`
- // The following used to watch certificate changes so that the TLS can be reloaded
- _privateKeyFileMtime int64
- _publicKeyFileMtime int64
- }
- // https://golang.org/pkg/crypto/tls/#pkg-constants
- // Ciphers introduced before Go 1.7 are listed here,
- // ciphers since Go 1.8, see tls_go1.8.go
- var TLSCiphers = map[string]uint16{
- // // Note: Generally avoid using CBC unless for compatibility
- "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
- "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
- "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
- "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
- "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
- "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
- "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
- "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
- "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA,
- "TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
- "TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
- "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
- "TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
- "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
- "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
- "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
- // Include to prevent downgrade attacks
- "TLS_FALLBACK_SCSV": tls.TLS_FALLBACK_SCSV,
- }
- // https://golang.org/pkg/crypto/tls/#pkg-constants
- var TLSProtocols = map[string]uint16{
- "ssl3.0": tls.VersionSSL30,
- "tls1.0": tls.VersionTLS10,
- "tls1.1": tls.VersionTLS11,
- "tls1.2": tls.VersionTLS12,
- }
- // https://golang.org/pkg/crypto/tls/#CurveID
- var TLSCurves = map[string]tls.CurveID{
- "P256": tls.CurveP256,
- "P384": tls.CurveP384,
- "P521": tls.CurveP521,
- }
- // https://golang.org/pkg/crypto/tls/#ClientAuthType
- var TLSClientAuthTypes = map[string]tls.ClientAuthType{
- "NoClientCert": tls.NoClientCert,
- "RequestClientCert": tls.RequestClientCert,
- "RequireAnyClientCert": tls.RequireAnyClientCert,
- "VerifyClientCertIfGiven": tls.VerifyClientCertIfGiven,
- "RequireAndVerifyClientCert": tls.RequireAndVerifyClientCert,
- }
- const defaultMaxClients = 100
- const defaultTimeout = 30
- const defaultInterface = "127.0.0.1:2525"
- const defaultMaxSize = int64(10 << 20) // 10 Mebibytes
- // Unmarshalls json data into AppConfig struct and any other initialization of the struct
- // also does validation, returns error if validation failed or something went wrong
- func (c *AppConfig) Load(jsonBytes []byte) error {
- err := json.Unmarshal(jsonBytes, c)
- if err != nil {
- return fmt.Errorf("could not parse config file: %s", err)
- }
- if err = c.setDefaults(); err != nil {
- return err
- }
- if err = c.setBackendDefaults(); err != nil {
- return err
- }
- // all servers must be valid in order to continue
- for _, server := range c.Servers {
- if errs := server.Validate(); errs != nil {
- return errs
- }
- }
- // read the timestamps for the ssl keys, to determine if they need to be reloaded
- for i := 0; i < len(c.Servers); i++ {
- if err := c.Servers[i].loadTlsKeyTimestamps(); err != nil {
- return err
- }
- }
- return nil
- }
- // Emits any configuration change events onto the event bus.
- func (c *AppConfig) EmitChangeEvents(oldConfig *AppConfig, app Guerrilla) {
- // has backend changed?
- if !reflect.DeepEqual((*c).BackendConfig, (*oldConfig).BackendConfig) {
- app.Publish(EventConfigBackendConfig, c)
- }
- // has config changed, general check
- if !reflect.DeepEqual(oldConfig, c) {
- app.Publish(EventConfigNewConfig, c)
- }
- // has 'allowed hosts' changed?
- if !reflect.DeepEqual(oldConfig.AllowedHosts, c.AllowedHosts) {
- app.Publish(EventConfigAllowedHosts, c)
- }
- // has pid file changed?
- if strings.Compare(oldConfig.PidFile, c.PidFile) != 0 {
- app.Publish(EventConfigPidFile, c)
- }
- // has mainlog log changed?
- if strings.Compare(oldConfig.LogFile, c.LogFile) != 0 {
- app.Publish(EventConfigLogFile, c)
- }
- // has log level changed?
- if strings.Compare(oldConfig.LogLevel, c.LogLevel) != 0 {
- app.Publish(EventConfigLogLevel, c)
- }
- // server config changes
- oldServers := oldConfig.getServers()
- for iface, newServer := range c.getServers() {
- // is server is in both configs?
- if oldServer, ok := oldServers[iface]; ok {
- // since old server exists in the new config, we do not track it anymore
- delete(oldServers, iface)
- // so we know the server exists in both old & new configs
- newServer.emitChangeEvents(oldServer, app)
- } else {
- // start new server
- app.Publish(EventConfigServerNew, newServer)
- }
- }
- // remove any servers that don't exist anymore
- for _, oldServer := range oldServers {
- app.Publish(EventConfigServerRemove, oldServer)
- }
- }
- // EmitLogReopen emits log reopen events using existing config
- func (c *AppConfig) EmitLogReopenEvents(app Guerrilla) {
- app.Publish(EventConfigLogReopen, c)
- for _, sc := range c.getServers() {
- app.Publish(EventConfigServerLogReopen, sc)
- }
- }
- // gets the servers in a map (key by interface) for easy lookup
- func (c *AppConfig) getServers() map[string]*ServerConfig {
- servers := make(map[string]*ServerConfig, len(c.Servers))
- for i := 0; i < len(c.Servers); i++ {
- servers[c.Servers[i].ListenInterface] = &c.Servers[i]
- }
- return servers
- }
- // setDefaults fills in default server settings for values that were not configured
- // The defaults are:
- // * Server listening to 127.0.0.1:2525
- // * use your hostname to determine your which hosts to accept email for
- // * 100 maximum clients
- // * 10MB max message size
- // * log to Stderr,
- // * log level set to "`debug`"
- // * timeout to 30 sec
- // * Backend configured with the following processors: `HeadersParser|Header|Debugger`
- // where it will log the received emails.
- func (c *AppConfig) setDefaults() error {
- if c.LogFile == "" {
- c.LogFile = log.OutputStderr.String()
- }
- if c.LogLevel == "" {
- c.LogLevel = "debug"
- }
- if len(c.AllowedHosts) == 0 {
- if h, err := os.Hostname(); err != nil {
- return err
- } else {
- c.AllowedHosts = append(c.AllowedHosts, h)
- }
- }
- h, err := os.Hostname()
- if err != nil {
- return err
- }
- if len(c.Servers) == 0 {
- sc := ServerConfig{}
- sc.LogFile = c.LogFile
- sc.ListenInterface = defaultInterface
- sc.IsEnabled = true
- sc.Hostname = h
- sc.MaxClients = defaultMaxClients
- sc.Timeout = defaultTimeout
- sc.MaxSize = defaultMaxSize
- c.Servers = append(c.Servers, sc)
- } else {
- // make sure each server has defaults correctly configured
- for i := range c.Servers {
- if c.Servers[i].Hostname == "" {
- c.Servers[i].Hostname = h
- }
- if c.Servers[i].MaxClients == 0 {
- c.Servers[i].MaxClients = defaultMaxClients
- }
- if c.Servers[i].Timeout == 0 {
- c.Servers[i].Timeout = defaultTimeout
- }
- if c.Servers[i].MaxSize == 0 {
- c.Servers[i].MaxSize = defaultMaxSize // 10 Mebibytes
- }
- if c.Servers[i].ListenInterface == "" {
- return errors.New(fmt.Sprintf("Listen interface not specified for server at index %d", i))
- }
- if c.Servers[i].LogFile == "" {
- c.Servers[i].LogFile = c.LogFile
- }
- // validate the server config
- err = c.Servers[i].Validate()
- if err != nil {
- return err
- }
- }
- }
- return nil
- }
- // setBackendDefaults sets default values for the backend config,
- // if no backend config was added before starting, then use a default config
- // otherwise, see what required values were missed in the config and add any missing with defaults
- func (c *AppConfig) setBackendDefaults() error {
- if len(c.BackendConfig) == 0 {
- h, err := os.Hostname()
- if err != nil {
- return err
- }
- c.BackendConfig = backends.BackendConfig{
- "log_received_mails": true,
- "save_workers_size": 1,
- "save_process": "HeadersParser|Header|Debugger",
- "primary_mail_host": h,
- }
- } else {
- if _, ok := c.BackendConfig["save_process"]; !ok {
- c.BackendConfig["save_process"] = "HeadersParser|Header|Debugger"
- }
- if _, ok := c.BackendConfig["primary_mail_host"]; !ok {
- h, err := os.Hostname()
- if err != nil {
- return err
- }
- c.BackendConfig["primary_mail_host"] = h
- }
- if _, ok := c.BackendConfig["save_workers_size"]; !ok {
- c.BackendConfig["save_workers_size"] = 1
- }
- if _, ok := c.BackendConfig["log_received_mails"]; !ok {
- c.BackendConfig["log_received_mails"] = false
- }
- }
- return nil
- }
- // Emits any configuration change events on the server.
- // All events are fired and run synchronously
- func (sc *ServerConfig) emitChangeEvents(oldServer *ServerConfig, app Guerrilla) {
- // get a list of changes
- changes := getChanges(
- *oldServer,
- *sc,
- )
- tlsChanges := getChanges(
- (*oldServer).TLS,
- (*sc).TLS,
- )
- if len(changes) > 0 || len(tlsChanges) > 0 {
- // something changed in the server config
- app.Publish(EventConfigServerConfig, sc)
- }
- // enable or disable?
- if _, ok := changes["IsEnabled"]; ok {
- if sc.IsEnabled {
- app.Publish(EventConfigServerStart, sc)
- } else {
- app.Publish(EventConfigServerStop, sc)
- }
- // do not emit any more events when IsEnabled changed
- return
- }
- // log file change?
- if _, ok := changes["LogFile"]; ok {
- app.Publish(EventConfigServerLogFile, sc)
- } else {
- // since config file has not changed, we reload it
- app.Publish(EventConfigServerLogReopen, sc)
- }
- // timeout changed
- if _, ok := changes["Timeout"]; ok {
- app.Publish(EventConfigServerTimeout, sc)
- }
- // max_clients changed
- if _, ok := changes["MaxClients"]; ok {
- app.Publish(EventConfigServerMaxClients, sc)
- }
- if len(tlsChanges) > 0 {
- app.Publish(EventConfigServerTLSConfig, sc)
- }
- }
- // Loads in timestamps for the ssl keys
- func (sc *ServerConfig) loadTlsKeyTimestamps() error {
- var statErr = func(iface string, err error) error {
- return errors.New(
- fmt.Sprintf(
- "could not stat key for server [%s], %s",
- iface,
- err.Error()))
- }
- if sc.TLS.PrivateKeyFile == "" {
- sc.TLS._privateKeyFileMtime = time.Now().Unix()
- return nil
- }
- if sc.TLS.PublicKeyFile == "" {
- sc.TLS._publicKeyFileMtime = time.Now().Unix()
- return nil
- }
- if info, err := os.Stat(sc.TLS.PrivateKeyFile); err == nil {
- sc.TLS._privateKeyFileMtime = info.ModTime().Unix()
- } else {
- return statErr(sc.ListenInterface, err)
- }
- if info, err := os.Stat(sc.TLS.PublicKeyFile); err == nil {
- sc.TLS._publicKeyFileMtime = info.ModTime().Unix()
- } else {
- return statErr(sc.ListenInterface, err)
- }
- return nil
- }
- // Validate validates the server's configuration.
- func (sc *ServerConfig) Validate() error {
- var errs Errors
- if sc.TLS.StartTLSOn || sc.TLS.AlwaysOn {
- if sc.TLS.PublicKeyFile == "" {
- errs = append(errs, errors.New("PublicKeyFile is empty"))
- }
- if sc.TLS.PrivateKeyFile == "" {
- errs = append(errs, errors.New("PrivateKeyFile is empty"))
- }
- if _, err := tls.LoadX509KeyPair(sc.TLS.PublicKeyFile, sc.TLS.PrivateKeyFile); err != nil {
- errs = append(errs,
- errors.New(fmt.Sprintf("cannot use TLS config for [%s], %v", sc.ListenInterface, err)))
- }
- }
- if len(errs) > 0 {
- return errs
- }
- return nil
- }
- // Gets the timestamp of the TLS certificates. Returns a unix time of when they were last modified
- // when the config was read. We use this info to determine if TLS needs to be re-loaded.
- func (stc *ServerTLSConfig) getTlsKeyTimestamps() (int64, int64) {
- return stc._privateKeyFileMtime, stc._publicKeyFileMtime
- }
- // Returns value changes between struct a & struct b.
- // Results are returned in a map, where each key is the name of the field that was different.
- // a and b are struct values, must not be pointer
- // and of the same struct type
- func getChanges(a interface{}, b interface{}) map[string]interface{} {
- ret := make(map[string]interface{}, 5)
- compareWith := structtomap(b)
- for key, val := range structtomap(a) {
- if sliceOfStr, ok := val.([]string); ok {
- val, _ = json.Marshal(sliceOfStr)
- val = string(val.([]uint8))
- }
- if sliceOfStr, ok := compareWith[key].([]string); ok {
- compareWith[key], _ = json.Marshal(sliceOfStr)
- compareWith[key] = string(compareWith[key].([]uint8))
- }
- if val != compareWith[key] {
- ret[key] = compareWith[key]
- }
- }
- // detect changes to TLS keys (have the key files been modified?)
- if oldTLS, ok := a.(ServerTLSConfig); ok {
- t1, t2 := oldTLS.getTlsKeyTimestamps()
- if newTLS, ok := b.(ServerTLSConfig); ok {
- t3, t4 := newTLS.getTlsKeyTimestamps()
- if t1 != t3 {
- ret["PrivateKeyFile"] = newTLS.PrivateKeyFile
- }
- if t2 != t4 {
- ret["PublicKeyFile"] = newTLS.PublicKeyFile
- }
- }
- }
- return ret
- }
- // Convert fields of a struct to a map
- // only able to convert int, bool, slice-of-strings and string; not recursive
- // slices are marshal'd to json for convenient comparison later
- func structtomap(obj interface{}) map[string]interface{} {
- ret := make(map[string]interface{}, 0)
- v := reflect.ValueOf(obj)
- t := v.Type()
- for index := 0; index < v.NumField(); index++ {
- vField := v.Field(index)
- fName := t.Field(index).Name
- k := vField.Kind()
- switch k {
- case reflect.Int:
- fallthrough
- case reflect.Int64:
- value := vField.Int()
- ret[fName] = value
- case reflect.String:
- value := vField.String()
- ret[fName] = value
- case reflect.Bool:
- value := vField.Bool()
- ret[fName] = value
- case reflect.Slice:
- ret[fName] = vField.Interface().([]string)
- }
- }
- return ret
- }
|