diff --git a/cmd/root.go b/cmd/root.go index 2345b67..1f4d623 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/axllent/mailpit/config" + "github.com/axllent/mailpit/internal/auth" "github.com/axllent/mailpit/internal/logger" "github.com/axllent/mailpit/internal/storage" "github.com/axllent/mailpit/server" @@ -91,7 +92,7 @@ func init() { rootCmd.Flags().BoolVar(&config.DisableHTMLCheck, "disable-html-check", config.DisableHTMLCheck, "Disable the HTML check functionality (web UI & API)") rootCmd.Flags().BoolVar(&config.BlockRemoteCSSAndFonts, "block-remote-css-and-fonts", config.BlockRemoteCSSAndFonts, "Block access to remote CSS & fonts") - rootCmd.Flags().StringVar(&config.UIAuthFile, "ui-auth-file", config.UIAuthFile, "A password file for web UI authentication") + rootCmd.Flags().StringVar(&config.UIAuthFile, "ui-auth-file", config.UIAuthFile, "A password file for web UI & API authentication") rootCmd.Flags().StringVar(&config.UITLSCert, "ui-tls-cert", config.UITLSCert, "TLS certificate for web UI (HTTPS) - requires ui-tls-key") rootCmd.Flags().StringVar(&config.UITLSKey, "ui-tls-key", config.UITLSKey, "TLS key for web UI (HTTPS) - requires ui-tls-cert") @@ -109,22 +110,6 @@ func init() { rootCmd.Flags().BoolVarP(&logger.QuietLogging, "quiet", "q", logger.QuietLogging, "Quiet logging (errors only)") rootCmd.Flags().BoolVarP(&logger.VerboseLogging, "verbose", "v", logger.VerboseLogging, "Verbose logging") - // deprecated flags 2022/08/06 - rootCmd.Flags().StringVarP(&config.UIAuthFile, "auth-file", "a", config.UIAuthFile, "A password file for web UI authentication") - rootCmd.Flags().StringVar(&config.UITLSCert, "ssl-cert", config.UITLSCert, "SSL certificate - requires ssl-key") - rootCmd.Flags().StringVar(&config.UITLSKey, "ssl-key", config.UITLSKey, "SSL key - requires ssl-cert") - rootCmd.Flags().Lookup("auth-file").Hidden = true - rootCmd.Flags().Lookup("auth-file").Deprecated = "use --ui-auth-file" - rootCmd.Flags().Lookup("ssl-cert").Hidden = true - rootCmd.Flags().Lookup("ssl-cert").Deprecated = "use --ui-tls-cert" - rootCmd.Flags().Lookup("ssl-key").Hidden = true - rootCmd.Flags().Lookup("ssl-key").Deprecated = "use --ui-tls-key" - - // deprecated flags 2022/08/30 - rootCmd.Flags().StringVar(&config.DataFile, "data", config.DataFile, "Database file to store persistent data") - rootCmd.Flags().Lookup("data").Hidden = true - rootCmd.Flags().Lookup("data").Deprecated = "use --db-file" - // deprecated flags 2023/03/12 rootCmd.Flags().StringVar(&config.UITLSCert, "ui-ssl-cert", config.UITLSCert, "SSL certificate for web UI - requires ui-ssl-key") rootCmd.Flags().StringVar(&config.UITLSKey, "ui-ssl-key", config.UITLSKey, "SSL key for web UI - requires ui-ssl-cert") @@ -143,9 +128,7 @@ func init() { // Load settings from environment func initConfigFromEnv() { // inherit from environment if provided - if len(os.Getenv("MP_DATA_FILE")) > 0 { - config.DataFile = os.Getenv("MP_DATA_FILE") - } + config.DataFile = os.Getenv("MP_DATA_FILE") if len(os.Getenv("MP_SMTP_BIND_ADDR")) > 0 { config.SMTPListen = os.Getenv("MP_SMTP_BIND_ADDR") } @@ -160,26 +143,16 @@ func initConfigFromEnv() { } // UI - if len(os.Getenv("MP_UI_AUTH_FILE")) > 0 { - config.UIAuthFile = os.Getenv("MP_UI_AUTH_FILE") - } - if len(os.Getenv("MP_UI_TLS_CERT")) > 0 { - config.UITLSCert = os.Getenv("MP_UI_TLS_CERT") - } - if len(os.Getenv("MP_UI_TLS_KEY")) > 0 { - config.UITLSKey = os.Getenv("MP_UI_TLS_KEY") - } + config.UIAuthFile = os.Getenv("MP_UI_AUTH_FILE") + auth.SetUIAuth(os.Getenv("MP_UI_AUTH")) + config.UITLSCert = os.Getenv("MP_UI_TLS_CERT") + config.UITLSKey = os.Getenv("MP_UI_TLS_KEY") // SMTP - if len(os.Getenv("MP_SMTP_AUTH_FILE")) > 0 { - config.SMTPAuthFile = os.Getenv("MP_SMTP_AUTH_FILE") - } - if len(os.Getenv("MP_SMTP_TLS_CERT")) > 0 { - config.SMTPTLSCert = os.Getenv("MP_SMTP_TLS_CERT") - } - if len(os.Getenv("MP_SMTP_TLS_KEY")) > 0 { - config.SMTPTLSKey = os.Getenv("MP_SMTP_TLS_KEY") - } + config.SMTPAuthFile = os.Getenv("MP_SMTP_AUTH_FILE") + auth.SetSMTPAuth(os.Getenv("MP_SMTP_AUTH")) + config.SMTPTLSCert = os.Getenv("MP_SMTP_TLS_CERT") + config.SMTPTLSKey = os.Getenv("MP_SMTP_TLS_KEY") if getEnabledFromEnv("MP_SMTP_AUTH_ACCEPT_ANY") { config.SMTPAuthAcceptAny = true } @@ -191,9 +164,7 @@ func initConfigFromEnv() { } // Relay server config - if len(os.Getenv("MP_SMTP_RELAY_CONFIG")) > 0 { - config.SMTPRelayConfigFile = os.Getenv("MP_SMTP_RELAY_CONFIG") - } + config.SMTPRelayConfigFile = os.Getenv("MP_SMTP_RELAY_CONFIG") if getEnabledFromEnv("MP_SMTP_RELAY_ALL") { config.SMTPRelayAllIncoming = true } @@ -227,39 +198,22 @@ func initConfigFromEnv() { // load deprecated settings from environment and warn func initDeprecatedConfigFromEnv() { - // deprecated 2022/08/06 - if len(os.Getenv("MP_AUTH_FILE")) > 0 { - fmt.Println("ENV MP_AUTH_FILE has been deprecated, use MP_UI_AUTH_FILE") - config.UIAuthFile = os.Getenv("MP_AUTH_FILE") - } - // deprecated 2022/08/06 - if len(os.Getenv("MP_SSL_CERT")) > 0 { - fmt.Println("ENV MP_SSL_CERT has been deprecated, use MP_UI_TLS_CERT") - config.UITLSCert = os.Getenv("MP_SSL_CERT") - } - // deprecated 2022/08/06 - if len(os.Getenv("MP_SSL_KEY")) > 0 { - fmt.Println("ENV MP_SSL_KEY has been deprecated, use MP_UI_TLS_KEY") - config.UITLSKey = os.Getenv("MP_TLS_KEY") - } - // deprecated 2022/08/28 - if len(os.Getenv("MP_DATA_DIR")) > 0 { - fmt.Println("ENV MP_DATA_DIR has been deprecated, use MP_DATA_FILE") - config.DataFile = os.Getenv("MP_DATA_DIR") - } // deprecated 2023/03/12 if len(os.Getenv("MP_UI_SSL_CERT")) > 0 { fmt.Println("ENV MP_UI_SSL_CERT has been deprecated, use MP_UI_TLS_CERT") config.UITLSCert = os.Getenv("MP_UI_SSL_CERT") } + // deprecated 2023/03/12 if len(os.Getenv("MP_UI_SSL_KEY")) > 0 { fmt.Println("ENV MP_UI_SSL_KEY has been deprecated, use MP_UI_TLS_KEY") config.UITLSKey = os.Getenv("MP_UI_SSL_KEY") } + // deprecated 2023/03/12 if len(os.Getenv("MP_SMTP_SSL_CERT")) > 0 { fmt.Println("ENV MP_SMTP_CERT has been deprecated, use MP_SMTP_TLS_CERT") config.SMTPTLSCert = os.Getenv("MP_SMTP_SSL_CERT") } + // deprecated 2023/03/12 if len(os.Getenv("MP_SMTP_SSL_KEY")) > 0 { fmt.Println("ENV MP_SMTP_KEY has been deprecated, use MP_SMTP_TLS_KEY") config.SMTPTLSKey = os.Getenv("MP_SMTP_SMTP_KEY") diff --git a/config/config.go b/config/config.go index b76effc..7e07389 100644 --- a/config/config.go +++ b/config/config.go @@ -10,9 +10,9 @@ import ( "regexp" "strings" + "github.com/axllent/mailpit/internal/auth" "github.com/axllent/mailpit/internal/logger" "github.com/axllent/mailpit/internal/tools" - "github.com/tg123/go-htpasswd" "gopkg.in/yaml.v3" ) @@ -38,12 +38,9 @@ var ( // UITLSKey file UITLSKey string - // UIAuthFile for basic authentication + // UIAuthFile for UI & API authentication UIAuthFile string - // UIAuth used for authentication - UIAuth *htpasswd.File - // Webroot to define the base path for the UI and API Webroot = "/" @@ -56,9 +53,6 @@ var ( // SMTPAuthFile for SMTP authentication SMTPAuthFile string - // SMTPAuthConfig used for authentication auto-generated from SMTPAuthFile - SMTPAuthConfig *htpasswd.File - // SMTPAuthAllowInsecure allows PLAIN & LOGIN unencrypted authentication SMTPAuthAllowInsecure bool @@ -161,12 +155,13 @@ func VerifyConfig() error { if !isFile(UIAuthFile) { return fmt.Errorf("HTTP password file not found: %s", UIAuthFile) } - - a, err := htpasswd.New(UIAuthFile, htpasswd.DefaultSystems, nil) + b, err := os.ReadFile(UIAuthFile) if err != nil { return err } - UIAuth = a + if err := auth.SetUIAuth(string(b)); err != nil { + return err + } } if UITLSCert != "" && UITLSKey == "" || UITLSCert == "" && UITLSKey != "" { @@ -202,18 +197,21 @@ func VerifyConfig() error { return fmt.Errorf("SMTP password file not found: %s", SMTPAuthFile) } - if SMTPAuthAcceptAny { - return errors.New("SMTP authentication can either use --smtp-auth-file or --smtp-auth-accept-any") - } - - a, err := htpasswd.New(SMTPAuthFile, htpasswd.DefaultSystems, nil) + b, err := os.ReadFile(SMTPAuthFile) if err != nil { return err } - SMTPAuthConfig = a + + if err := auth.SetSMTPAuth(string(b)); err != nil { + return err + } } - if SMTPTLSCert == "" && (SMTPAuthFile != "" || SMTPAuthAcceptAny) && !SMTPAuthAllowInsecure { + if auth.SMTPCredentials != nil && SMTPAuthAcceptAny { + return errors.New("SMTP authentication cannot use both credentials and --smtp-auth-accept-any") + } + + if SMTPTLSCert == "" && (auth.SMTPCredentials != nil || SMTPAuthAcceptAny) && !SMTPAuthAllowInsecure { return errors.New("SMTP authentication requires TLS encryption, run with `--smtp-auth-allow-insecure` to allow insecure authentication") } diff --git a/internal/auth/auth.go b/internal/auth/auth.go new file mode 100644 index 0000000..5bd9a9d --- /dev/null +++ b/internal/auth/auth.go @@ -0,0 +1,69 @@ +// Package auth handles the web UI and SMTP authentication +package auth + +import ( + "regexp" + "strings" + + "github.com/tg123/go-htpasswd" +) + +var ( + // UICredentials passwords + UICredentials *htpasswd.File + // SMTPCredentials passwords + SMTPCredentials *htpasswd.File +) + +// SetUIAuth will set Basic Auth credentials required for the UI & API +func SetUIAuth(s string) error { + var err error + + credentials := credentialsFromString(s) + if len(credentials) == 0 { + return nil + } + + r := strings.NewReader(strings.Join(credentials, "\n")) + + UICredentials, err = htpasswd.NewFromReader(r, htpasswd.DefaultSystems, nil) + if err != nil { + return err + } + + return nil +} + +// SetSMTPAuth will set SMTP credentials +func SetSMTPAuth(s string) error { + var err error + + credentials := credentialsFromString(s) + if len(credentials) == 0 { + return nil + } + + r := strings.NewReader(strings.Join(credentials, "\n")) + + SMTPCredentials, err = htpasswd.NewFromReader(r, htpasswd.DefaultSystems, nil) + if err != nil { + return err + } + + return nil +} + +func credentialsFromString(s string) []string { + // split string by any whitespace character + re := regexp.MustCompile(`\s+`) + + words := re.Split(s, -1) + credentials := []string{} + for _, w := range words { + if w != "" { + credentials = append(credentials, w) + } + } + + return credentials +} diff --git a/server/server.go b/server/server.go index 71d1f03..084ffd8 100644 --- a/server/server.go +++ b/server/server.go @@ -15,6 +15,7 @@ import ( "text/template" "github.com/axllent/mailpit/config" + "github.com/axllent/mailpit/internal/auth" "github.com/axllent/mailpit/internal/logger" "github.com/axllent/mailpit/internal/storage" "github.com/axllent/mailpit/server/apiv1" @@ -79,8 +80,8 @@ func Listen() { // put it all together http.Handle("/", r) - if config.UIAuthFile != "" { - logger.Log().Info("[http] enabling web UI basic authentication") + if auth.UICredentials != nil { + logger.Log().Info("[http] enabling basic authentication") } // Mark the application here as ready @@ -158,7 +159,7 @@ func middleWareFunc(fn http.HandlerFunc) http.HandlerFunc { w.Header().Set("Access-Control-Allow-Headers", "*") } - if config.UIAuthFile != "" { + if auth.UICredentials != nil { user, pass, ok := r.BasicAuth() if !ok { @@ -166,7 +167,21 @@ func middleWareFunc(fn http.HandlerFunc) http.HandlerFunc { return } - if !config.UIAuth.Match(user, pass) { + if !auth.UICredentials.Match(user, pass) { + basicAuthResponse(w) + return + } + } + + if auth.UICredentials != nil { + user, pass, ok := r.BasicAuth() + + if !ok { + basicAuthResponse(w) + return + } + + if !auth.UICredentials.Match(user, pass) { basicAuthResponse(w) return } @@ -197,7 +212,7 @@ func middlewareHandler(h http.Handler) http.Handler { w.Header().Set("Access-Control-Allow-Headers", "*") } - if config.UIAuthFile != "" { + if auth.UICredentials != nil { user, pass, ok := r.BasicAuth() if !ok { @@ -205,7 +220,7 @@ func middlewareHandler(h http.Handler) http.Handler { return } - if !config.UIAuth.Match(user, pass) { + if !auth.UICredentials.Match(user, pass) { basicAuthResponse(w) return } diff --git a/server/smtpd/smtpd.go b/server/smtpd/smtpd.go index 482cb32..e953106 100644 --- a/server/smtpd/smtpd.go +++ b/server/smtpd/smtpd.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/axllent/mailpit/config" + "github.com/axllent/mailpit/internal/auth" "github.com/axllent/mailpit/internal/logger" "github.com/axllent/mailpit/internal/storage" "github.com/mhale/smtpd" @@ -129,7 +130,7 @@ func mailHandler(origin net.Addr, from string, to []string, data []byte) error { } func authHandler(remoteAddr net.Addr, mechanism string, username []byte, password []byte, _ []byte) (bool, error) { - allow := config.SMTPAuthConfig.Match(string(username), string(password)) + allow := auth.SMTPCredentials.Match(string(username), string(password)) if allow { logger.Log().Debugf("[smtpd] allow %s login:%q from:%s", mechanism, string(username), cleanIP(remoteAddr)) } else { @@ -149,14 +150,14 @@ func authHandlerAny(remoteAddr net.Addr, mechanism string, username []byte, _ [] // Listen starts the SMTPD server func Listen() error { if config.SMTPAuthAllowInsecure { - if config.SMTPAuthFile != "" { - logger.Log().Infof("[smtpd] enabling login auth via %s (insecure)", config.SMTPAuthFile) + if auth.SMTPCredentials != nil { + logger.Log().Info("[smtpd] enabling login auth (insecure)") } else if config.SMTPAuthAcceptAny { logger.Log().Info("[smtpd] enabling all auth (insecure)") } } else { - if config.SMTPAuthFile != "" { - logger.Log().Infof("[smtpd] enabling login auth via %s (TLS)", config.SMTPAuthFile) + if auth.SMTPCredentials != nil { + logger.Log().Info("[smtpd] enabling login auth (TLS)") } else if config.SMTPAuthAcceptAny { logger.Log().Info("[smtpd] enabling any auth (TLS)") } @@ -181,7 +182,7 @@ func listenAndServe(addr string, handler smtpd.Handler, authHandler smtpd.AuthHa srv.AuthMechs = map[string]bool{"CRAM-MD5": false, "PLAIN": true, "LOGIN": true} } - if config.SMTPAuthFile != "" { + if auth.SMTPCredentials != nil { srv.AuthMechs = map[string]bool{"CRAM-MD5": false, "PLAIN": true, "LOGIN": true} srv.AuthHandler = authHandler srv.AuthRequired = true diff --git a/server/websockets/client.go b/server/websockets/client.go index aebdcc8..51e7bb3 100644 --- a/server/websockets/client.go +++ b/server/websockets/client.go @@ -8,7 +8,7 @@ import ( "net/http" "time" - "github.com/axllent/mailpit/config" + "github.com/axllent/mailpit/internal/auth" "github.com/axllent/mailpit/internal/logger" "github.com/gorilla/websocket" ) @@ -99,19 +99,17 @@ func (c *Client) writePump() { // ServeWs handles websocket requests from the peer. func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) { - if config.UIAuthFile != "" { - if config.UIAuthFile != "" { - user, pass, ok := r.BasicAuth() + if auth.UICredentials != nil { + user, pass, ok := r.BasicAuth() - if !ok { - basicAuthResponse(w) - return - } + if !ok { + basicAuthResponse(w) + return + } - if !config.UIAuth.Match(user, pass) { - basicAuthResponse(w) - return - } + if !auth.UICredentials.Match(user, pass) { + basicAuthResponse(w) + return } }