1
0
mirror of https://github.com/axllent/mailpit.git synced 2024-12-28 23:06:43 +02:00

Merge branch 'feature/basic-auth-via-env' into develop

This commit is contained in:
Ralph Slooten 2023-09-29 16:44:52 +13:00
commit b6fdcd4ec5
6 changed files with 138 additions and 103 deletions

View File

@ -8,6 +8,7 @@ import (
"strings" "strings"
"github.com/axllent/mailpit/config" "github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/auth"
"github.com/axllent/mailpit/internal/logger" "github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/storage" "github.com/axllent/mailpit/internal/storage"
"github.com/axllent/mailpit/server" "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.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().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.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") 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.QuietLogging, "quiet", "q", logger.QuietLogging, "Quiet logging (errors only)")
rootCmd.Flags().BoolVarP(&logger.VerboseLogging, "verbose", "v", logger.VerboseLogging, "Verbose logging") 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 // 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.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") 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 // Load settings from environment
func initConfigFromEnv() { func initConfigFromEnv() {
// inherit from environment if provided // 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 { if len(os.Getenv("MP_SMTP_BIND_ADDR")) > 0 {
config.SMTPListen = os.Getenv("MP_SMTP_BIND_ADDR") config.SMTPListen = os.Getenv("MP_SMTP_BIND_ADDR")
} }
@ -160,26 +143,16 @@ func initConfigFromEnv() {
} }
// UI // UI
if len(os.Getenv("MP_UI_AUTH_FILE")) > 0 { config.UIAuthFile = os.Getenv("MP_UI_AUTH_FILE")
config.UIAuthFile = os.Getenv("MP_UI_AUTH_FILE") auth.SetUIAuth(os.Getenv("MP_UI_AUTH"))
} config.UITLSCert = os.Getenv("MP_UI_TLS_CERT")
if len(os.Getenv("MP_UI_TLS_CERT")) > 0 { config.UITLSKey = os.Getenv("MP_UI_TLS_KEY")
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")
}
// SMTP // SMTP
if len(os.Getenv("MP_SMTP_AUTH_FILE")) > 0 { config.SMTPAuthFile = os.Getenv("MP_SMTP_AUTH_FILE")
config.SMTPAuthFile = os.Getenv("MP_SMTP_AUTH_FILE") auth.SetSMTPAuth(os.Getenv("MP_SMTP_AUTH"))
} config.SMTPTLSCert = os.Getenv("MP_SMTP_TLS_CERT")
if len(os.Getenv("MP_SMTP_TLS_CERT")) > 0 { config.SMTPTLSKey = os.Getenv("MP_SMTP_TLS_KEY")
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")
}
if getEnabledFromEnv("MP_SMTP_AUTH_ACCEPT_ANY") { if getEnabledFromEnv("MP_SMTP_AUTH_ACCEPT_ANY") {
config.SMTPAuthAcceptAny = true config.SMTPAuthAcceptAny = true
} }
@ -191,9 +164,7 @@ func initConfigFromEnv() {
} }
// Relay server config // 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") { if getEnabledFromEnv("MP_SMTP_RELAY_ALL") {
config.SMTPRelayAllIncoming = true config.SMTPRelayAllIncoming = true
} }
@ -227,39 +198,22 @@ func initConfigFromEnv() {
// load deprecated settings from environment and warn // load deprecated settings from environment and warn
func initDeprecatedConfigFromEnv() { 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 // deprecated 2023/03/12
if len(os.Getenv("MP_UI_SSL_CERT")) > 0 { if len(os.Getenv("MP_UI_SSL_CERT")) > 0 {
fmt.Println("ENV MP_UI_SSL_CERT has been deprecated, use MP_UI_TLS_CERT") fmt.Println("ENV MP_UI_SSL_CERT has been deprecated, use MP_UI_TLS_CERT")
config.UITLSCert = os.Getenv("MP_UI_SSL_CERT") config.UITLSCert = os.Getenv("MP_UI_SSL_CERT")
} }
// deprecated 2023/03/12
if len(os.Getenv("MP_UI_SSL_KEY")) > 0 { if len(os.Getenv("MP_UI_SSL_KEY")) > 0 {
fmt.Println("ENV MP_UI_SSL_KEY has been deprecated, use MP_UI_TLS_KEY") fmt.Println("ENV MP_UI_SSL_KEY has been deprecated, use MP_UI_TLS_KEY")
config.UITLSKey = os.Getenv("MP_UI_SSL_KEY") config.UITLSKey = os.Getenv("MP_UI_SSL_KEY")
} }
// deprecated 2023/03/12
if len(os.Getenv("MP_SMTP_SSL_CERT")) > 0 { if len(os.Getenv("MP_SMTP_SSL_CERT")) > 0 {
fmt.Println("ENV MP_SMTP_CERT has been deprecated, use MP_SMTP_TLS_CERT") fmt.Println("ENV MP_SMTP_CERT has been deprecated, use MP_SMTP_TLS_CERT")
config.SMTPTLSCert = os.Getenv("MP_SMTP_SSL_CERT") config.SMTPTLSCert = os.Getenv("MP_SMTP_SSL_CERT")
} }
// deprecated 2023/03/12
if len(os.Getenv("MP_SMTP_SSL_KEY")) > 0 { if len(os.Getenv("MP_SMTP_SSL_KEY")) > 0 {
fmt.Println("ENV MP_SMTP_KEY has been deprecated, use MP_SMTP_TLS_KEY") fmt.Println("ENV MP_SMTP_KEY has been deprecated, use MP_SMTP_TLS_KEY")
config.SMTPTLSKey = os.Getenv("MP_SMTP_SMTP_KEY") config.SMTPTLSKey = os.Getenv("MP_SMTP_SMTP_KEY")

View File

@ -10,9 +10,9 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/axllent/mailpit/internal/auth"
"github.com/axllent/mailpit/internal/logger" "github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/tools" "github.com/axllent/mailpit/internal/tools"
"github.com/tg123/go-htpasswd"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@ -38,12 +38,9 @@ var (
// UITLSKey file // UITLSKey file
UITLSKey string UITLSKey string
// UIAuthFile for basic authentication // UIAuthFile for UI & API authentication
UIAuthFile string UIAuthFile string
// UIAuth used for authentication
UIAuth *htpasswd.File
// Webroot to define the base path for the UI and API // Webroot to define the base path for the UI and API
Webroot = "/" Webroot = "/"
@ -56,9 +53,6 @@ var (
// SMTPAuthFile for SMTP authentication // SMTPAuthFile for SMTP authentication
SMTPAuthFile string SMTPAuthFile string
// SMTPAuthConfig used for authentication auto-generated from SMTPAuthFile
SMTPAuthConfig *htpasswd.File
// SMTPAuthAllowInsecure allows PLAIN & LOGIN unencrypted authentication // SMTPAuthAllowInsecure allows PLAIN & LOGIN unencrypted authentication
SMTPAuthAllowInsecure bool SMTPAuthAllowInsecure bool
@ -161,12 +155,13 @@ func VerifyConfig() error {
if !isFile(UIAuthFile) { if !isFile(UIAuthFile) {
return fmt.Errorf("HTTP password file not found: %s", UIAuthFile) return fmt.Errorf("HTTP password file not found: %s", UIAuthFile)
} }
b, err := os.ReadFile(UIAuthFile)
a, err := htpasswd.New(UIAuthFile, htpasswd.DefaultSystems, nil)
if err != nil { if err != nil {
return err return err
} }
UIAuth = a if err := auth.SetUIAuth(string(b)); err != nil {
return err
}
} }
if UITLSCert != "" && UITLSKey == "" || UITLSCert == "" && UITLSKey != "" { if UITLSCert != "" && UITLSKey == "" || UITLSCert == "" && UITLSKey != "" {
@ -202,18 +197,21 @@ func VerifyConfig() error {
return fmt.Errorf("SMTP password file not found: %s", SMTPAuthFile) return fmt.Errorf("SMTP password file not found: %s", SMTPAuthFile)
} }
if SMTPAuthAcceptAny { b, err := os.ReadFile(SMTPAuthFile)
return errors.New("SMTP authentication can either use --smtp-auth-file or --smtp-auth-accept-any")
}
a, err := htpasswd.New(SMTPAuthFile, htpasswd.DefaultSystems, nil)
if err != nil { if err != nil {
return err 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") return errors.New("SMTP authentication requires TLS encryption, run with `--smtp-auth-allow-insecure` to allow insecure authentication")
} }

69
internal/auth/auth.go Normal file
View File

@ -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
}

View File

@ -15,6 +15,7 @@ import (
"text/template" "text/template"
"github.com/axllent/mailpit/config" "github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/auth"
"github.com/axllent/mailpit/internal/logger" "github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/storage" "github.com/axllent/mailpit/internal/storage"
"github.com/axllent/mailpit/server/apiv1" "github.com/axllent/mailpit/server/apiv1"
@ -79,8 +80,8 @@ func Listen() {
// put it all together // put it all together
http.Handle("/", r) http.Handle("/", r)
if config.UIAuthFile != "" { if auth.UICredentials != nil {
logger.Log().Info("[http] enabling web UI basic authentication") logger.Log().Info("[http] enabling basic authentication")
} }
// Mark the application here as ready // Mark the application here as ready
@ -158,7 +159,7 @@ func middleWareFunc(fn http.HandlerFunc) http.HandlerFunc {
w.Header().Set("Access-Control-Allow-Headers", "*") w.Header().Set("Access-Control-Allow-Headers", "*")
} }
if config.UIAuthFile != "" { if auth.UICredentials != nil {
user, pass, ok := r.BasicAuth() user, pass, ok := r.BasicAuth()
if !ok { if !ok {
@ -166,7 +167,21 @@ func middleWareFunc(fn http.HandlerFunc) http.HandlerFunc {
return 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) basicAuthResponse(w)
return return
} }
@ -197,7 +212,7 @@ func middlewareHandler(h http.Handler) http.Handler {
w.Header().Set("Access-Control-Allow-Headers", "*") w.Header().Set("Access-Control-Allow-Headers", "*")
} }
if config.UIAuthFile != "" { if auth.UICredentials != nil {
user, pass, ok := r.BasicAuth() user, pass, ok := r.BasicAuth()
if !ok { if !ok {
@ -205,7 +220,7 @@ func middlewareHandler(h http.Handler) http.Handler {
return return
} }
if !config.UIAuth.Match(user, pass) { if !auth.UICredentials.Match(user, pass) {
basicAuthResponse(w) basicAuthResponse(w)
return return
} }

View File

@ -10,6 +10,7 @@ import (
"strings" "strings"
"github.com/axllent/mailpit/config" "github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/auth"
"github.com/axllent/mailpit/internal/logger" "github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/storage" "github.com/axllent/mailpit/internal/storage"
"github.com/mhale/smtpd" "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) { 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 { if allow {
logger.Log().Debugf("[smtpd] allow %s login:%q from:%s", mechanism, string(username), cleanIP(remoteAddr)) logger.Log().Debugf("[smtpd] allow %s login:%q from:%s", mechanism, string(username), cleanIP(remoteAddr))
} else { } else {
@ -149,14 +150,14 @@ func authHandlerAny(remoteAddr net.Addr, mechanism string, username []byte, _ []
// Listen starts the SMTPD server // Listen starts the SMTPD server
func Listen() error { func Listen() error {
if config.SMTPAuthAllowInsecure { if config.SMTPAuthAllowInsecure {
if config.SMTPAuthFile != "" { if auth.SMTPCredentials != nil {
logger.Log().Infof("[smtpd] enabling login auth via %s (insecure)", config.SMTPAuthFile) logger.Log().Info("[smtpd] enabling login auth (insecure)")
} else if config.SMTPAuthAcceptAny { } else if config.SMTPAuthAcceptAny {
logger.Log().Info("[smtpd] enabling all auth (insecure)") logger.Log().Info("[smtpd] enabling all auth (insecure)")
} }
} else { } else {
if config.SMTPAuthFile != "" { if auth.SMTPCredentials != nil {
logger.Log().Infof("[smtpd] enabling login auth via %s (TLS)", config.SMTPAuthFile) logger.Log().Info("[smtpd] enabling login auth (TLS)")
} else if config.SMTPAuthAcceptAny { } else if config.SMTPAuthAcceptAny {
logger.Log().Info("[smtpd] enabling any auth (TLS)") 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} 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.AuthMechs = map[string]bool{"CRAM-MD5": false, "PLAIN": true, "LOGIN": true}
srv.AuthHandler = authHandler srv.AuthHandler = authHandler
srv.AuthRequired = true srv.AuthRequired = true

View File

@ -8,7 +8,7 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/axllent/mailpit/config" "github.com/axllent/mailpit/internal/auth"
"github.com/axllent/mailpit/internal/logger" "github.com/axllent/mailpit/internal/logger"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )
@ -99,19 +99,17 @@ func (c *Client) writePump() {
// ServeWs handles websocket requests from the peer. // ServeWs handles websocket requests from the peer.
func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) { func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
if config.UIAuthFile != "" { if auth.UICredentials != nil {
if config.UIAuthFile != "" { user, pass, ok := r.BasicAuth()
user, pass, ok := r.BasicAuth()
if !ok { if !ok {
basicAuthResponse(w) basicAuthResponse(w)
return return
} }
if !config.UIAuth.Match(user, pass) { if !auth.UICredentials.Match(user, pass) {
basicAuthResponse(w) basicAuthResponse(w)
return return
}
} }
} }