浏览代码

Add BasicAuth to admin endpoints.

This removes the Nginx dependency for protecting admin pages.
BasicAuth is configured in config.toml. This is a "temporary"
setup until a full fledged auth mechanism is added.
Kailash Nadh 5 年之前
父节点
当前提交
b822955ac9
共有 8 个文件被更改,包括 127 次插入88 次删除
  1. 2 6
      campaigns.go
  2. 7 0
      config.toml.sample
  3. 1 0
      go.mod
  4. 3 0
      go.sum
  5. 97 75
      handlers.go
  6. 8 7
      init.go
  7. 8 0
      install.go
  8. 1 0
      internal/migrations/v0.7.0.go

+ 2 - 6
campaigns.go

@@ -560,16 +560,12 @@ func sendTestMessage(sub models.Subscriber, camp *models.Campaign, app *App) err
 			fmt.Sprintf("Error rendering message: %v", err))
 	}
 
-	if err := app.messenger.Push(messenger.Message{
+	return app.messenger.Push(messenger.Message{
 		From:    camp.FromEmail,
 		To:      []string{sub.Email},
 		Subject: m.Subject(),
 		Body:    m.Body(),
-	}); err != nil {
-		return err
-	}
-
-	return nil
+	})
 }
 
 // validateCampaignFields validates incoming campaign field values.

+ 7 - 0
config.toml.sample

@@ -2,6 +2,13 @@
     # Interface and port where the app will run its webserver.
     address = "0.0.0.0:9000"
 
+    # BasicAuth authentication for the admin dashboard. This will eventually
+    # be replaced with a better multi-user, role-based authentication system.
+    # IMPORTANT: Leave both values empty to disable authentication on admin
+    # only where an external authentication is already setup.
+    admin_username = "listmonk"
+    admin_password = "listmonk"
+
 # Database.
 [db]
     host = "db"

+ 1 - 0
go.mod

@@ -3,6 +3,7 @@ module github.com/knadh/listmonk
 go 1.13
 
 require (
+	github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
 	github.com/disintegration/imaging v1.6.2
 	github.com/gofrs/uuid v3.2.0+incompatible
 	github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195

+ 3 - 0
go.sum

@@ -3,6 +3,9 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v1.0.2 h1:KPldsxuKGsS2FPWsNeg9ZO18aCrGKujPoWXn2yo+KQM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
 github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
 github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=

+ 97 - 75
handlers.go

@@ -1,12 +1,14 @@
 package main
 
 import (
+	"crypto/subtle"
 	"net/http"
 	"net/url"
 	"regexp"
 	"strconv"
 
 	"github.com/labstack/echo"
+	"github.com/labstack/echo/middleware"
 )
 
 const (
@@ -30,71 +32,87 @@ var reUUID = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[
 
 // registerHandlers registers HTTP handlers.
 func registerHTTPHandlers(e *echo.Echo) {
-	e.GET("/", handleIndexPage)
-	e.GET("/api/health", handleHealthCheck)
-	e.GET("/api/config.js", handleGetConfigScript)
-	e.GET("/api/dashboard/charts", handleGetDashboardCharts)
-	e.GET("/api/dashboard/counts", handleGetDashboardCounts)
-
-	e.GET("/api/settings", handleGetSettings)
-	e.PUT("/api/settings", handleUpdateSettings)
-	e.POST("/api/admin/reload", handleReloadApp)
-
-	e.GET("/api/subscribers/:id", handleGetSubscriber)
-	e.GET("/api/subscribers/:id/export", handleExportSubscriberData)
-	e.POST("/api/subscribers", handleCreateSubscriber)
-	e.PUT("/api/subscribers/:id", handleUpdateSubscriber)
-	e.POST("/api/subscribers/:id/optin", handleSubscriberSendOptin)
-	e.PUT("/api/subscribers/blocklist", handleBlocklistSubscribers)
-	e.PUT("/api/subscribers/:id/blocklist", handleBlocklistSubscribers)
-	e.PUT("/api/subscribers/lists/:id", handleManageSubscriberLists)
-	e.PUT("/api/subscribers/lists", handleManageSubscriberLists)
-	e.DELETE("/api/subscribers/:id", handleDeleteSubscribers)
-	e.DELETE("/api/subscribers", handleDeleteSubscribers)
+	// Group of private handlers with BasicAuth.
+	g := e.Group("", middleware.BasicAuth(basicAuth))
+
+	g.GET("/", handleIndexPage)
+	g.GET("/api/health", handleHealthCheck)
+	g.GET("/api/config.js", handleGetConfigScript)
+	g.GET("/api/dashboard/charts", handleGetDashboardCharts)
+	g.GET("/api/dashboard/counts", handleGetDashboardCounts)
+
+	g.GET("/api/settings", handleGetSettings)
+	g.PUT("/api/settings", handleUpdateSettings)
+	g.POST("/api/admin/reload", handleReloadApp)
+
+	g.GET("/api/subscribers/:id", handleGetSubscriber)
+	g.GET("/api/subscribers/:id/export", handleExportSubscriberData)
+	g.POST("/api/subscribers", handleCreateSubscriber)
+	g.PUT("/api/subscribers/:id", handleUpdateSubscriber)
+	g.POST("/api/subscribers/:id/optin", handleSubscriberSendOptin)
+	g.PUT("/api/subscribers/blocklist", handleBlocklistSubscribers)
+	g.PUT("/api/subscribers/:id/blocklist", handleBlocklistSubscribers)
+	g.PUT("/api/subscribers/lists/:id", handleManageSubscriberLists)
+	g.PUT("/api/subscribers/lists", handleManageSubscriberLists)
+	g.DELETE("/api/subscribers/:id", handleDeleteSubscribers)
+	g.DELETE("/api/subscribers", handleDeleteSubscribers)
 
 	// Subscriber operations based on arbitrary SQL queries.
 	// These aren't very REST-like.
-	e.POST("/api/subscribers/query/delete", handleDeleteSubscribersByQuery)
-	e.PUT("/api/subscribers/query/blocklist", handleBlocklistSubscribersByQuery)
-	e.PUT("/api/subscribers/query/lists", handleManageSubscriberListsByQuery)
-	e.GET("/api/subscribers", handleQuerySubscribers)
-
-	e.GET("/api/import/subscribers", handleGetImportSubscribers)
-	e.GET("/api/import/subscribers/logs", handleGetImportSubscriberStats)
-	e.POST("/api/import/subscribers", handleImportSubscribers)
-	e.DELETE("/api/import/subscribers", handleStopImportSubscribers)
-
-	e.GET("/api/lists", handleGetLists)
-	e.GET("/api/lists/:id", handleGetLists)
-	e.POST("/api/lists", handleCreateList)
-	e.PUT("/api/lists/:id", handleUpdateList)
-	e.DELETE("/api/lists/:id", handleDeleteLists)
-
-	e.GET("/api/campaigns", handleGetCampaigns)
-	e.GET("/api/campaigns/running/stats", handleGetRunningCampaignStats)
-	e.GET("/api/campaigns/:id", handleGetCampaigns)
-	e.GET("/api/campaigns/:id/preview", handlePreviewCampaign)
-	e.POST("/api/campaigns/:id/preview", handlePreviewCampaign)
-	e.POST("/api/campaigns/:id/test", handleTestCampaign)
-	e.POST("/api/campaigns", handleCreateCampaign)
-	e.PUT("/api/campaigns/:id", handleUpdateCampaign)
-	e.PUT("/api/campaigns/:id/status", handleUpdateCampaignStatus)
-	e.DELETE("/api/campaigns/:id", handleDeleteCampaign)
-
-	e.GET("/api/media", handleGetMedia)
-	e.POST("/api/media", handleUploadMedia)
-	e.DELETE("/api/media/:id", handleDeleteMedia)
-
-	e.GET("/api/templates", handleGetTemplates)
-	e.GET("/api/templates/:id", handleGetTemplates)
-	e.GET("/api/templates/:id/preview", handlePreviewTemplate)
-	e.POST("/api/templates/preview", handlePreviewTemplate)
-	e.POST("/api/templates", handleCreateTemplate)
-	e.PUT("/api/templates/:id", handleUpdateTemplate)
-	e.PUT("/api/templates/:id/default", handleTemplateSetDefault)
-	e.DELETE("/api/templates/:id", handleDeleteTemplate)
-
-	// Subscriber facing views.
+	g.POST("/api/subscribers/query/delete", handleDeleteSubscribersByQuery)
+	g.PUT("/api/subscribers/query/blocklist", handleBlocklistSubscribersByQuery)
+	g.PUT("/api/subscribers/query/lists", handleManageSubscriberListsByQuery)
+	g.GET("/api/subscribers", handleQuerySubscribers)
+
+	g.GET("/api/import/subscribers", handleGetImportSubscribers)
+	g.GET("/api/import/subscribers/logs", handleGetImportSubscriberStats)
+	g.POST("/api/import/subscribers", handleImportSubscribers)
+	g.DELETE("/api/import/subscribers", handleStopImportSubscribers)
+
+	g.GET("/api/lists", handleGetLists)
+	g.GET("/api/lists/:id", handleGetLists)
+	g.POST("/api/lists", handleCreateList)
+	g.PUT("/api/lists/:id", handleUpdateList)
+	g.DELETE("/api/lists/:id", handleDeleteLists)
+
+	g.GET("/api/campaigns", handleGetCampaigns)
+	g.GET("/api/campaigns/running/stats", handleGetRunningCampaignStats)
+	g.GET("/api/campaigns/:id", handleGetCampaigns)
+	g.GET("/api/campaigns/:id/preview", handlePreviewCampaign)
+	g.POST("/api/campaigns/:id/preview", handlePreviewCampaign)
+	g.POST("/api/campaigns/:id/test", handleTestCampaign)
+	g.POST("/api/campaigns", handleCreateCampaign)
+	g.PUT("/api/campaigns/:id", handleUpdateCampaign)
+	g.PUT("/api/campaigns/:id/status", handleUpdateCampaignStatus)
+	g.DELETE("/api/campaigns/:id", handleDeleteCampaign)
+
+	g.GET("/api/media", handleGetMedia)
+	g.POST("/api/media", handleUploadMedia)
+	g.DELETE("/api/media/:id", handleDeleteMedia)
+
+	g.GET("/api/templates", handleGetTemplates)
+	g.GET("/api/templates/:id", handleGetTemplates)
+	g.GET("/api/templates/:id/preview", handlePreviewTemplate)
+	g.POST("/api/templates/preview", handlePreviewTemplate)
+	g.POST("/api/templates", handleCreateTemplate)
+	g.PUT("/api/templates/:id", handleUpdateTemplate)
+	g.PUT("/api/templates/:id/default", handleTemplateSetDefault)
+	g.DELETE("/api/templates/:id", handleDeleteTemplate)
+
+	// Static admin views.
+	g.GET("/lists", handleIndexPage)
+	g.GET("/lists/forms", handleIndexPage)
+	g.GET("/subscribers", handleIndexPage)
+	g.GET("/subscribers/lists/:listID", handleIndexPage)
+	g.GET("/subscribers/import", handleIndexPage)
+	g.GET("/campaigns", handleIndexPage)
+	g.GET("/campaigns/new", handleIndexPage)
+	g.GET("/campaigns/media", handleIndexPage)
+	g.GET("/campaigns/templates", handleIndexPage)
+	g.GET("/campaigns/:campignID", handleIndexPage)
+	g.GET("/settings", handleIndexPage)
+
+	// Public subscriber facing views.
 	e.POST("/subscription/form", handleSubscriptionForm)
 	e.GET("/subscription/:campUUID/:subUUID", validateUUID(subscriberExists(handleSubscriptionPage),
 		"campUUID", "subUUID"))
@@ -112,19 +130,6 @@ func registerHTTPHandlers(e *echo.Echo) {
 		"campUUID", "subUUID"))
 	e.GET("/campaign/:campUUID/:subUUID/px.png", validateUUID(handleRegisterCampaignView,
 		"campUUID", "subUUID"))
-
-	// Static views.
-	e.GET("/lists", handleIndexPage)
-	e.GET("/lists/forms", handleIndexPage)
-	e.GET("/subscribers", handleIndexPage)
-	e.GET("/subscribers/lists/:listID", handleIndexPage)
-	e.GET("/subscribers/import", handleIndexPage)
-	e.GET("/campaigns", handleIndexPage)
-	e.GET("/campaigns/new", handleIndexPage)
-	e.GET("/campaigns/media", handleIndexPage)
-	e.GET("/campaigns/templates", handleIndexPage)
-	e.GET("/campaigns/:campignID", handleIndexPage)
-	e.GET("/settings", handleIndexPage)
 }
 
 // handleIndex is the root handler that renders the Javascript frontend.
@@ -145,6 +150,23 @@ func handleHealthCheck(c echo.Context) error {
 	return c.JSON(http.StatusOK, okResp{true})
 }
 
+// basicAuth middleware does an HTTP BasicAuth authentication for admin handlers.
+func basicAuth(username, password string, c echo.Context) (bool, error) {
+	app := c.Get("app").(*App)
+
+	// Auth is disabled.
+	if len(app.constants.AdminUsername) == 0 &&
+		len(app.constants.AdminPassword) == 0 {
+		return true, nil
+	}
+
+	if subtle.ConstantTimeCompare([]byte(username), app.constants.AdminUsername) == 1 &&
+		subtle.ConstantTimeCompare([]byte(password), app.constants.AdminPassword) == 1 {
+		return true, nil
+	}
+	return false, nil
+}
+
 // validateUUID middleware validates the UUID string format for a given set of params.
 func validateUUID(next echo.HandlerFunc, params ...string) echo.HandlerFunc {
 	return func(c echo.Context) error {

+ 8 - 7
init.go

@@ -48,13 +48,14 @@ type constants struct {
 		AllowWipe      bool            `koanf:"allow_wipe"`
 		Exportable     map[string]bool `koanf:"-"`
 	} `koanf:"privacy"`
-
-	UnsubURL     string
-	LinkTrackURL string
-	ViewTrackURL string
-	OptinURL     string
-	MessageURL   string
-
+	AdminUsername []byte `koanf:"admin_username"`
+	AdminPassword []byte `koanf:"admin_password"`
+
+	UnsubURL      string
+	LinkTrackURL  string
+	ViewTrackURL  string
+	OptinURL      string
+	MessageURL    string
 	MediaProvider string
 }
 

+ 8 - 0
install.go

@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"io/ioutil"
 	"os"
+	"regexp"
 	"strings"
 
 	"github.com/gofrs/uuid"
@@ -170,5 +171,12 @@ func newConfigFile() error {
 		return fmt.Errorf("error reading sample config (is binary stuffed?): %v", err)
 	}
 
+	// Generate a random admin password.
+	pwd, err := generateRandomString(16)
+	if err == nil {
+		b = regexp.MustCompile(`admin_password\s+?=\s+?(.*)`).
+			ReplaceAll(b, []byte(fmt.Sprintf(`admin_password = "%s"`, pwd)))
+	}
+
 	return ioutil.WriteFile("config.toml", b, 0644)
 }

+ 1 - 0
internal/migrations/v0.7.0.go

@@ -75,6 +75,7 @@ func V0_7_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
 		('app.batch_size', '1000'),
 		('app.max_send_errors', '1000'),
 		('app.notify_emails', '["admin1@mysite.com", "admin2@mysite.com"]'),
+		('privacy.unsubscribe_header', 'true'),
 		('privacy.allow_blocklist', 'true'),
 		('privacy.allow_export', 'true'),
 		('privacy.allow_wipe', 'true'),