Skip to content

Commit 020d5b0

Browse files
committed
Merge branch 'feature/send-auth' into develop
2 parents 8c59229 + f2b91ac commit 020d5b0

File tree

5 files changed

+335
-3
lines changed

5 files changed

+335
-3
lines changed

cmd/root.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ func init() {
108108
rootCmd.Flags().BoolVar(&config.DisableHTTPCompression, "disable-http-compression", config.DisableHTTPCompression, "Disable HTTP compression support (web UI & API)")
109109
rootCmd.Flags().BoolVar(&config.HideDeleteAllButton, "hide-delete-all-button", config.HideDeleteAllButton, "Hide the \"Delete all\" button in the web UI")
110110

111+
// Send API
112+
rootCmd.Flags().StringVar(&config.SendAPIAuthFile, "send-api-auth-file", config.SendAPIAuthFile, "A password file for Send API authentication")
113+
rootCmd.Flags().BoolVar(&config.SendAPIAuthAcceptAny, "send-api-auth-accept-any", config.SendAPIAuthAcceptAny, "Accept any username and password for the Send API endpoint, including none")
114+
111115
// SMTP server
112116
rootCmd.Flags().StringVarP(&config.SMTPListen, "smtp", "s", config.SMTPListen, "SMTP bind interface and port")
113117
rootCmd.Flags().StringVar(&config.SMTPAuthFile, "smtp-auth-file", config.SMTPAuthFile, "A password file for SMTP authentication")
@@ -249,6 +253,15 @@ func initConfigFromEnv() {
249253
config.HideDeleteAllButton = true
250254
}
251255

256+
// Send API
257+
config.SendAPIAuthFile = os.Getenv("MP_SEND_API_AUTH_FILE")
258+
if err := auth.SetSendAPIAuth(os.Getenv("MP_SEND_API_AUTH")); err != nil {
259+
logger.Log().Error(err.Error())
260+
}
261+
if getEnabledFromEnv("MP_SEND_API_AUTH_ACCEPT_ANY") {
262+
config.SendAPIAuthAcceptAny = true
263+
}
264+
252265
// SMTP server
253266
if len(os.Getenv("MP_SMTP_BIND_ADDR")) > 0 {
254267
config.SMTPListen = os.Getenv("MP_SMTP_BIND_ADDR")
@@ -362,9 +375,9 @@ func initConfigFromEnv() {
362375
func initDeprecatedConfigFromEnv() {
363376
// deprecated 2024/04/12 - but will not be removed to maintain backwards compatibility
364377
if len(os.Getenv("MP_DATA_FILE")) > 0 {
378+
logger.Log().Warn("ENV MP_DATA_FILE has been deprecated, use MP_DATABASE")
365379
config.Database = os.Getenv("MP_DATA_FILE")
366380
}
367-
368381
// deprecated 2023/03/12
369382
if len(os.Getenv("MP_UI_SSL_CERT")) > 0 {
370383
logger.Log().Warn("ENV MP_UI_SSL_CERT has been deprecated, use MP_UI_TLS_CERT")

config/config.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ var (
7272
// DisableHTTPCompression will explicitly disable HTTP compression in the web UI and API
7373
DisableHTTPCompression bool
7474

75+
// SendAPIAuthFile for Send API authentication
76+
SendAPIAuthFile string
77+
78+
// SendAPIAuthAcceptAny accepts any username/password for the send API endpoint, including none
79+
SendAPIAuthAcceptAny bool
80+
7581
// SMTPTLSCert file
7682
SMTPTLSCert string
7783

@@ -289,6 +295,7 @@ func VerifyConfig() error {
289295
return errors.New("[ui] HTTP bind should be in the format of <ip>:<port>")
290296
}
291297

298+
// Web UI & API
292299
if UIAuthFile != "" {
293300
UIAuthFile = filepath.Clean(UIAuthFile)
294301

@@ -323,6 +330,35 @@ func VerifyConfig() error {
323330
}
324331
}
325332

333+
// Send API
334+
if SendAPIAuthFile != "" {
335+
SendAPIAuthFile = filepath.Clean(SendAPIAuthFile)
336+
337+
if !isFile(SendAPIAuthFile) {
338+
return fmt.Errorf("[send-api] password file not found or readable: %s", SendAPIAuthFile)
339+
}
340+
341+
b, err := os.ReadFile(SendAPIAuthFile)
342+
if err != nil {
343+
return err
344+
}
345+
346+
if err := auth.SetSendAPIAuth(string(b)); err != nil {
347+
return err
348+
}
349+
350+
logger.Log().Info("[send-api] enabling basic authentication")
351+
}
352+
353+
if auth.SendAPICredentials != nil && SendAPIAuthAcceptAny {
354+
return errors.New("[send-api] authentication cannot use both credentials and --send-api-auth-accept-any")
355+
}
356+
357+
if SendAPIAuthAcceptAny && auth.UICredentials != nil {
358+
logger.Log().Info("[send-api] disabling authentication")
359+
}
360+
361+
// SMTP server
326362
if SMTPTLSCert != "" && SMTPTLSKey == "" || SMTPTLSCert == "" && SMTPTLSKey != "" {
327363
return errors.New("[smtp] you must provide both an SMTP TLS certificate and a key")
328364
}

internal/auth/auth.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
var (
1212
// UICredentials passwords
1313
UICredentials *htpasswd.File
14+
// SendAPICredentials passwords
15+
SendAPICredentials *htpasswd.File
1416
// SMTPCredentials passwords
1517
SMTPCredentials *htpasswd.File
1618
// POP3Credentials passwords
@@ -36,6 +38,25 @@ func SetUIAuth(s string) error {
3638
return nil
3739
}
3840

41+
// SetSendAPIAuth will set Send API credentials
42+
func SetSendAPIAuth(s string) error {
43+
var err error
44+
45+
credentials := credentialsFromString(s)
46+
if len(credentials) == 0 {
47+
return nil
48+
}
49+
50+
r := strings.NewReader(strings.Join(credentials, "\n"))
51+
52+
SendAPICredentials, err = htpasswd.NewFromReader(r, htpasswd.DefaultSystems, nil)
53+
if err != nil {
54+
return err
55+
}
56+
57+
return nil
58+
}
59+
3960
// SetSMTPAuth will set SMTP credentials
4061
func SetSMTPAuth(s string) error {
4162
var err error

server/server.go

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ func apiRoutes() *mux.Router {
158158
r.HandleFunc(config.Webroot+"api/v1/messages", middleWareFunc(apiv1.DeleteMessages)).Methods("DELETE")
159159
r.HandleFunc(config.Webroot+"api/v1/search", middleWareFunc(apiv1.Search)).Methods("GET")
160160
r.HandleFunc(config.Webroot+"api/v1/search", middleWareFunc(apiv1.DeleteSearch)).Methods("DELETE")
161-
r.HandleFunc(config.Webroot+"api/v1/send", middleWareFunc(apiv1.SendMessageHandler)).Methods("POST")
161+
r.HandleFunc(config.Webroot+"api/v1/send", sendAPIAuthMiddleware(apiv1.SendMessageHandler)).Methods("POST")
162162
r.HandleFunc(config.Webroot+"api/v1/tags", middleWareFunc(apiv1.GetAllTags)).Methods("GET")
163163
r.HandleFunc(config.Webroot+"api/v1/tags", middleWareFunc(apiv1.SetMessageTags)).Methods("PUT")
164164
r.HandleFunc(config.Webroot+"api/v1/tags/{tag}", middleWareFunc(apiv1.RenameTag)).Methods("PUT")
@@ -198,6 +198,48 @@ func basicAuthResponse(w http.ResponseWriter) {
198198
_, _ = w.Write([]byte("Unauthorised.\n"))
199199
}
200200

201+
// sendAPIAuthMiddleware handles authentication specifically for the send API endpoint
202+
// It can use dedicated send API authentication or accept any credentials based on configuration
203+
func sendAPIAuthMiddleware(fn http.HandlerFunc) http.HandlerFunc {
204+
return func(w http.ResponseWriter, r *http.Request) {
205+
// If send API auth accept any is enabled, bypass all authentication
206+
if config.SendAPIAuthAcceptAny {
207+
// Temporarily disable UI auth for this request
208+
originalCredentials := auth.UICredentials
209+
auth.UICredentials = nil
210+
defer func() { auth.UICredentials = originalCredentials }()
211+
// Call the standard middleware
212+
middleWareFunc(fn)(w, r)
213+
return
214+
}
215+
216+
// If Send API credentials are configured, only accept those credentials
217+
if auth.SendAPICredentials != nil {
218+
user, pass, ok := r.BasicAuth()
219+
220+
if !ok {
221+
basicAuthResponse(w)
222+
return
223+
}
224+
225+
if !auth.SendAPICredentials.Match(user, pass) {
226+
basicAuthResponse(w)
227+
return
228+
}
229+
230+
// Valid Send API credentials - bypass UI auth and call function directly
231+
originalCredentials := auth.UICredentials
232+
auth.UICredentials = nil
233+
defer func() { auth.UICredentials = originalCredentials }()
234+
middleWareFunc(fn)(w, r)
235+
return
236+
}
237+
238+
// No Send API credentials configured - fall back to UI auth
239+
middleWareFunc(fn)(w, r)
240+
}
241+
}
242+
201243
type gzipResponseWriter struct {
202244
io.Writer
203245
http.ResponseWriter
@@ -239,7 +281,9 @@ func middleWareFunc(fn http.HandlerFunc) http.HandlerFunc {
239281
w.Header().Set("Access-Control-Allow-Headers", "*")
240282
}
241283

242-
if auth.UICredentials != nil {
284+
// Check basic authentication headers if configured.
285+
// OPTIONS requests are skipped if CORS is enabled, since browsers omit credentials for preflight.
286+
if !(AccessControlAllowOrigin != "" && r.Method == http.MethodOptions) && auth.UICredentials != nil {
243287
user, pass, ok := r.BasicAuth()
244288

245289
if !ok {

0 commit comments

Comments
 (0)