8985e5c24a
Bots easily bypass the simple `nonce` hack. This commit adds support for the hcaptcha.com widget. - New `Security` tab in the admin settings UI. - Enable/disable CAPTCHA. - Render CAPTCHA on the public subscription form. Closes #1116.
265 lines
7.3 KiB
Go
265 lines
7.3 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/gofrs/uuid"
|
|
"github.com/knadh/koanf"
|
|
"github.com/knadh/koanf/parsers/json"
|
|
"github.com/knadh/koanf/providers/rawbytes"
|
|
"github.com/knadh/listmonk/internal/messenger"
|
|
"github.com/knadh/listmonk/internal/messenger/email"
|
|
"github.com/knadh/listmonk/models"
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
var (
|
|
reAlphaNum = regexp.MustCompile(`[^a-z0-9\-]`)
|
|
)
|
|
|
|
// handleGetSettings returns settings from the DB.
|
|
func handleGetSettings(c echo.Context) error {
|
|
app := c.Get("app").(*App)
|
|
|
|
s, err := app.core.GetSettings()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Empty out passwords.
|
|
for i := 0; i < len(s.SMTP); i++ {
|
|
s.SMTP[i].Password = ""
|
|
}
|
|
for i := 0; i < len(s.BounceBoxes); i++ {
|
|
s.BounceBoxes[i].Password = ""
|
|
}
|
|
for i := 0; i < len(s.Messengers); i++ {
|
|
s.Messengers[i].Password = ""
|
|
}
|
|
s.UploadS3AwsSecretAccessKey = ""
|
|
s.SendgridKey = ""
|
|
s.SecurityCaptchaSecret = ""
|
|
|
|
return c.JSON(http.StatusOK, okResp{s})
|
|
}
|
|
|
|
// handleUpdateSettings returns settings from the DB.
|
|
func handleUpdateSettings(c echo.Context) error {
|
|
var (
|
|
app = c.Get("app").(*App)
|
|
set models.Settings
|
|
)
|
|
|
|
// Unmarshal and marshal the fields once to sanitize the settings blob.
|
|
if err := c.Bind(&set); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get the existing settings.
|
|
cur, err := app.core.GetSettings()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// There should be at least one SMTP block that's enabled.
|
|
has := false
|
|
for i, s := range set.SMTP {
|
|
if s.Enabled {
|
|
has = true
|
|
}
|
|
|
|
// Assign a UUID. The frontend only sends a password when the user explicitly
|
|
// changes the password. In other cases, the existing password in the DB
|
|
// is copied while updating the settings and the UUID is used to match
|
|
// the incoming array of SMTP blocks with the array in the DB.
|
|
if s.UUID == "" {
|
|
set.SMTP[i].UUID = uuid.Must(uuid.NewV4()).String()
|
|
}
|
|
|
|
// If there's no password coming in from the frontend, copy the existing
|
|
// password by matching the UUID.
|
|
if s.Password == "" {
|
|
for _, c := range cur.SMTP {
|
|
if s.UUID == c.UUID {
|
|
set.SMTP[i].Password = c.Password
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !has {
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("settings.errorNoSMTP"))
|
|
}
|
|
|
|
// Bounce boxes.
|
|
for i, s := range set.BounceBoxes {
|
|
// Assign a UUID. The frontend only sends a password when the user explicitly
|
|
// changes the password. In other cases, the existing password in the DB
|
|
// is copied while updating the settings and the UUID is used to match
|
|
// the incoming array of blocks with the array in the DB.
|
|
if s.UUID == "" {
|
|
set.BounceBoxes[i].UUID = uuid.Must(uuid.NewV4()).String()
|
|
}
|
|
|
|
if d, _ := time.ParseDuration(s.ScanInterval); d.Minutes() < 1 {
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("settings.bounces.invalidScanInterval"))
|
|
}
|
|
|
|
// If there's no password coming in from the frontend, copy the existing
|
|
// password by matching the UUID.
|
|
if s.Password == "" {
|
|
for _, c := range cur.BounceBoxes {
|
|
if s.UUID == c.UUID {
|
|
set.BounceBoxes[i].Password = c.Password
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate and sanitize postback Messenger names. Duplicates are disallowed
|
|
// and "email" is a reserved name.
|
|
names := map[string]bool{emailMsgr: true}
|
|
|
|
for i, m := range set.Messengers {
|
|
// UUID to keep track of password changes similar to the SMTP logic above.
|
|
if m.UUID == "" {
|
|
set.Messengers[i].UUID = uuid.Must(uuid.NewV4()).String()
|
|
}
|
|
|
|
if m.Password == "" {
|
|
for _, c := range cur.Messengers {
|
|
if m.UUID == c.UUID {
|
|
set.Messengers[i].Password = c.Password
|
|
}
|
|
}
|
|
}
|
|
|
|
name := reAlphaNum.ReplaceAllString(strings.ToLower(m.Name), "")
|
|
if _, ok := names[name]; ok {
|
|
return echo.NewHTTPError(http.StatusBadRequest,
|
|
app.i18n.Ts("settings.duplicateMessengerName", "name", name))
|
|
}
|
|
if len(name) == 0 {
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("settings.invalidMessengerName"))
|
|
}
|
|
|
|
set.Messengers[i].Name = name
|
|
names[name] = true
|
|
}
|
|
|
|
// S3 password?
|
|
if set.UploadS3AwsSecretAccessKey == "" {
|
|
set.UploadS3AwsSecretAccessKey = cur.UploadS3AwsSecretAccessKey
|
|
}
|
|
if set.SendgridKey == "" {
|
|
set.SendgridKey = cur.SendgridKey
|
|
}
|
|
if set.SecurityCaptchaSecret == "" {
|
|
set.SecurityCaptchaSecret = cur.SecurityCaptchaSecret
|
|
}
|
|
|
|
// Domain blocklist.
|
|
doms := make([]string, 0)
|
|
for _, d := range set.DomainBlocklist {
|
|
d = strings.TrimSpace(strings.ToLower(d))
|
|
if d != "" {
|
|
doms = append(doms, d)
|
|
}
|
|
}
|
|
set.DomainBlocklist = doms
|
|
|
|
// Update the settings in the DB.
|
|
if err := app.core.UpdateSettings(set); err != nil {
|
|
return err
|
|
}
|
|
|
|
// If there are any active campaigns, don't do an auto reload and
|
|
// warn the user on the frontend.
|
|
if app.manager.HasRunningCampaigns() {
|
|
app.Lock()
|
|
app.needsRestart = true
|
|
app.Unlock()
|
|
|
|
return c.JSON(http.StatusOK, okResp{struct {
|
|
NeedsRestart bool `json:"needs_restart"`
|
|
}{true}})
|
|
}
|
|
|
|
// No running campaigns. Reload the app.
|
|
go func() {
|
|
<-time.After(time.Millisecond * 500)
|
|
app.sigChan <- syscall.SIGHUP
|
|
}()
|
|
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
}
|
|
|
|
// handleGetLogs returns the log entries stored in the log buffer.
|
|
func handleGetLogs(c echo.Context) error {
|
|
app := c.Get("app").(*App)
|
|
return c.JSON(http.StatusOK, okResp{app.bufLog.Lines()})
|
|
}
|
|
|
|
// handleTestSMTPSettings returns the log entries stored in the log buffer.
|
|
func handleTestSMTPSettings(c echo.Context) error {
|
|
app := c.Get("app").(*App)
|
|
|
|
// Copy the raw JSON post body.
|
|
reqBody, err := ioutil.ReadAll(c.Request().Body)
|
|
if err != nil {
|
|
app.log.Printf("error reading SMTP test: %v", err)
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.internalError"))
|
|
}
|
|
|
|
// Load the JSON into koanf to parse SMTP settings properly including timestrings.
|
|
ko := koanf.New(".")
|
|
if err := ko.Load(rawbytes.Provider(reqBody), json.Parser()); err != nil {
|
|
app.log.Printf("error unmarshalling SMTP test request: %v", err)
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.internalError"))
|
|
}
|
|
|
|
req := email.Server{}
|
|
if err := ko.UnmarshalWithConf("", &req, koanf.UnmarshalConf{Tag: "json"}); err != nil {
|
|
app.log.Printf("error scanning SMTP test request: %v", err)
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.internalError"))
|
|
}
|
|
|
|
to := ko.String("email")
|
|
if to == "" {
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.missingFields", "name", "email"))
|
|
}
|
|
|
|
// Initialize a new SMTP pool.
|
|
req.MaxConns = 1
|
|
req.IdleTimeout = time.Second * 2
|
|
req.PoolWaitTimeout = time.Second * 2
|
|
msgr, err := email.New(req)
|
|
if err != nil {
|
|
return echo.NewHTTPError(http.StatusBadRequest,
|
|
app.i18n.Ts("globals.messages.errorCreating", "name", "SMTP", "error", err.Error()))
|
|
}
|
|
|
|
var b bytes.Buffer
|
|
if err := app.notifTpls.tpls.ExecuteTemplate(&b, "smtp-test", nil); err != nil {
|
|
app.log.Printf("error compiling notification template '%s': %v", "smtp-test", err)
|
|
return err
|
|
}
|
|
|
|
m := messenger.Message{}
|
|
m.ContentType = app.notifTpls.contentType
|
|
m.From = app.constants.FromEmail
|
|
m.To = []string{to}
|
|
m.Subject = app.i18n.T("settings.smtp.testConnection")
|
|
m.Body = b.Bytes()
|
|
if err := msgr.Push(m); err != nil {
|
|
app.log.Printf("error sending SMTP test (%s): %v", m.Subject, err)
|
|
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, okResp{app.bufLog.Lines()})
|
|
}
|