Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature : Strong Data Validation #1929

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

"github.com/Masterminds/sprig/v3"
"github.com/gdgvda/cron"
"github.com/go-playground/validator/v10"
"github.com/jmoiron/sqlx"
"github.com/jmoiron/sqlx/types"
"github.com/knadh/goyesql/v2"
Expand Down Expand Up @@ -789,6 +790,8 @@ func initHTTPServer(app *App) *echo.Echo {
srv.Static(ko.String("upload.filesystem.upload_uri"), ko.String("upload.filesystem.upload_path"))
}

srv.Validator = &CustomValidator{Validator: validator.New()}

// Register all HTTP handlers.
initHTTPHandlers(srv, app)

Expand Down
50 changes: 31 additions & 19 deletions cmd/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"github.com/gdgvda/cron"
"github.com/gofrs/uuid/v5"
"github.com/jinzhu/copier"
"github.com/jmoiron/sqlx/types"
"github.com/knadh/koanf/parsers/json"
"github.com/knadh/koanf/providers/rawbytes"
Expand Down Expand Up @@ -78,12 +79,16 @@ func handleGetSettings(c echo.Context) error {
// handleUpdateSettings returns settings from the DB.
func handleUpdateSettings(c echo.Context) error {
var (
app = c.Get("app").(*App)
set models.Settings
app = c.Get("app").(*App)
req, set models.Settings
)

// Unmarshal and marshal the fields once to sanitize the settings blob.
if err := c.Bind(&set); err != nil {
if err := c.Bind(&req); err != nil {
return err
}

if err := c.Validate(req); err != nil {
return err
}

Expand All @@ -93,10 +98,13 @@ func handleUpdateSettings(c echo.Context) error {
return err
}

set = cur
copier.CopyWithOption(&set, &req, copier.Option{IgnoreEmpty: true})

// There should be at least one SMTP block that's enabled.
has := false
for i, s := range set.SMTP {
if s.Enabled {
if *s.Enabled {
has = true
}

Expand Down Expand Up @@ -157,6 +165,24 @@ func handleUpdateSettings(c echo.Context) error {
}
}

// handle bounce postmark
if set.BouncePostmark.Enabled == nil {
set.BouncePostmark.Enabled = cur.BouncePostmark.Enabled
}
if set.BouncePostmark.Username == "" {
set.BouncePostmark.Username = cur.BouncePostmark.Username
}
if set.BouncePostmark.Password == "" {
set.BouncePostmark.Password = cur.BouncePostmark.Password
}

// handle bounce actions
for name, action := range cur.BounceActions {
if _, ok := set.BounceActions[name]; !ok {
set.BounceActions[name] = action
}
}

// Validate and sanitize postback Messenger names. Duplicates are disallowed
// and "email" is a reserved name.
names := map[string]bool{emailMsgr: true}
Expand Down Expand Up @@ -188,20 +214,6 @@ func handleUpdateSettings(c echo.Context) error {
names[name] = true
}

// S3 password?
if set.UploadS3AwsSecretAccessKey == "" {
set.UploadS3AwsSecretAccessKey = cur.UploadS3AwsSecretAccessKey
}
if set.SendgridKey == "" {
set.SendgridKey = cur.SendgridKey
}
if set.BouncePostmark.Password == "" {
set.BouncePostmark.Password = cur.BouncePostmark.Password
}
if set.SecurityCaptchaSecret == "" {
set.SecurityCaptchaSecret = cur.SecurityCaptchaSecret
}

for n, v := range set.UploadExtensions {
set.UploadExtensions[n] = strings.ToLower(strings.TrimPrefix(strings.TrimSpace(v), "."))
}
Expand All @@ -217,7 +229,7 @@ func handleUpdateSettings(c echo.Context) error {
set.DomainBlocklist = doms

// Validate slow query caching cron.
if set.CacheSlowQueries {
if *set.CacheSlowQueries {
if _, err := cron.ParseStandard(set.CacheSlowQueriesInterval); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidData")+": slow query cron: "+err.Error())
}
Expand Down
19 changes: 19 additions & 0 deletions cmd/validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package main

import (
"net/http"

"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
)

type CustomValidator struct {
Validator *validator.Validate
}

func (c *CustomValidator) Validate(i interface{}) error {
if err := c.Validator.Struct(i); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return nil
}
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ require (
github.com/disintegration/imaging v1.6.2
github.com/emersion/go-message v0.16.0
github.com/gdgvda/cron v0.2.0
github.com/go-playground/validator/v10 v10.22.0
github.com/gofrs/uuid/v5 v5.0.0
github.com/gorilla/feeds v1.1.1
github.com/jinzhu/copier v0.4.0
github.com/jmoiron/sqlx v1.3.5
github.com/knadh/go-pop3 v0.3.0
github.com/knadh/goyesql/v2 v2.2.0
Expand Down Expand Up @@ -40,13 +42,17 @@ require (
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/imdario/mergo v0.3.14 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
Expand Down
13 changes: 13 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,17 @@ github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gdgvda/cron v0.2.0 h1:oX8qdLZq4tC5StnCsZsTNs2BIzaRjcjmPZ4o+BArKX4=
github.com/gdgvda/cron v0.2.0/go.mod h1:VEwidZXB255kESB5DcUGRWTYZS8KkOBYD1YBn8Wiyx8=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
Expand All @@ -39,6 +48,8 @@ github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
Expand Down Expand Up @@ -80,6 +91,8 @@ github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zG
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
Expand Down
106 changes: 53 additions & 53 deletions models/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,35 @@ type Settings struct {
AppFaviconURL string `json:"app.favicon_url"`
AppFromEmail string `json:"app.from_email"`
AppNotifyEmails []string `json:"app.notify_emails"`
EnablePublicSubPage bool `json:"app.enable_public_subscription_page"`
EnablePublicArchive bool `json:"app.enable_public_archive"`
EnablePublicArchiveRSSContent bool `json:"app.enable_public_archive_rss_content"`
SendOptinConfirmation bool `json:"app.send_optin_confirmation"`
CheckUpdates bool `json:"app.check_updates"`
EnablePublicSubPage *bool `json:"app.enable_public_subscription_page"`
EnablePublicArchive *bool `json:"app.enable_public_archive"`
EnablePublicArchiveRSSContent *bool `json:"app.enable_public_archive_rss_content"`
SendOptinConfirmation *bool `json:"app.send_optin_confirmation"`
CheckUpdates *bool `json:"app.check_updates"`
AppLang string `json:"app.lang"`

AppBatchSize int `json:"app.batch_size"`
AppConcurrency int `json:"app.concurrency"`
AppMaxSendErrors int `json:"app.max_send_errors"`
AppMessageRate int `json:"app.message_rate"`
CacheSlowQueries bool `json:"app.cache_slow_queries"`
CacheSlowQueries *bool `json:"app.cache_slow_queries"`
CacheSlowQueriesInterval string `json:"app.cache_slow_queries_interval"`

AppMessageSlidingWindow bool `json:"app.message_sliding_window"`
AppMessageSlidingWindow *bool `json:"app.message_sliding_window"`
AppMessageSlidingWindowDuration string `json:"app.message_sliding_window_duration"`
AppMessageSlidingWindowRate int `json:"app.message_sliding_window_rate"`

PrivacyIndividualTracking bool `json:"privacy.individual_tracking"`
PrivacyUnsubHeader bool `json:"privacy.unsubscribe_header"`
PrivacyAllowBlocklist bool `json:"privacy.allow_blocklist"`
PrivacyAllowPreferences bool `json:"privacy.allow_preferences"`
PrivacyAllowExport bool `json:"privacy.allow_export"`
PrivacyAllowWipe bool `json:"privacy.allow_wipe"`
PrivacyIndividualTracking *bool `json:"privacy.individual_tracking"`
PrivacyUnsubHeader *bool `json:"privacy.unsubscribe_header"`
PrivacyAllowBlocklist *bool `json:"privacy.allow_blocklist"`
PrivacyAllowPreferences *bool `json:"privacy.allow_preferences"`
PrivacyAllowExport *bool `json:"privacy.allow_export"`
PrivacyAllowWipe *bool `json:"privacy.allow_wipe"`
PrivacyExportable []string `json:"privacy.exportable"`
PrivacyRecordOptinIP bool `json:"privacy.record_optin_ip"`
PrivacyRecordOptinIP *bool `json:"privacy.record_optin_ip"`
DomainBlocklist []string `json:"privacy.domain_blocklist"`

SecurityEnableCaptcha bool `json:"security.enable_captcha"`
SecurityEnableCaptcha *bool `json:"security.enable_captcha"`
SecurityCaptchaKey string `json:"security.captcha_key"`
SecurityCaptchaSecret string `json:"security.captcha_secret"`

Expand All @@ -57,62 +57,62 @@ type Settings struct {

SMTP []struct {
UUID string `json:"uuid"`
Enabled bool `json:"enabled"`
Host string `json:"host"`
Enabled *bool `json:"enabled" validate:"required"`
Host string `json:"host" validate:"required"`
HelloHostname string `json:"hello_hostname"`
Port int `json:"port"`
AuthProtocol string `json:"auth_protocol"`
Username string `json:"username"`
Port int `json:"port" validate:"required"`
AuthProtocol string `json:"auth_protocol" validate:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password,omitempty"`
EmailHeaders []map[string]string `json:"email_headers"`
MaxConns int `json:"max_conns"`
MaxMsgRetries int `json:"max_msg_retries"`
IdleTimeout string `json:"idle_timeout"`
WaitTimeout string `json:"wait_timeout"`
TLSType string `json:"tls_type"`
TLSSkipVerify bool `json:"tls_skip_verify"`
} `json:"smtp"`
MaxConns int `json:"max_conns" validate:"required"`
MaxMsgRetries int `json:"max_msg_retries" validate:"required"`
IdleTimeout string `json:"idle_timeout" validate:"required"`
WaitTimeout string `json:"wait_timeout" validate:"required"`
TLSType string `json:"tls_type" validate:"required"`
TLSSkipVerify *bool `json:"tls_skip_verify" validate:"required"`
} `json:"smtp" validate:"omitempty,dive"`

Messengers []struct {
UUID string `json:"uuid"`
Enabled bool `json:"enabled"`
Enabled *bool `json:"enabled" validate:"required"`
Name string `json:"name"`
RootURL string `json:"root_url"`
Username string `json:"username"`
RootURL string `json:"root_url" validate:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password,omitempty"`
MaxConns int `json:"max_conns"`
Timeout string `json:"timeout"`
MaxMsgRetries int `json:"max_msg_retries"`
} `json:"messengers"`
MaxConns int `json:"max_conns" validate:"required"`
Timeout string `json:"timeout" validate:"required"`
MaxMsgRetries int `json:"max_msg_retries" validate:"required"`
} `json:"messengers" validate:"omitempty,dive"`

BounceEnabled bool `json:"bounce.enabled"`
BounceEnableWebhooks bool `json:"bounce.webhooks_enabled"`
BounceEnabled *bool `json:"bounce.enabled"`
BounceEnableWebhooks *bool `json:"bounce.webhooks_enabled"`
BounceActions map[string]struct {
Count int `json:"count"`
Action string `json:"action"`
} `json:"bounce.actions"`
SESEnabled bool `json:"bounce.ses_enabled"`
SendgridEnabled bool `json:"bounce.sendgrid_enabled"`
Count int `json:"count" validate:"required"`
Action string `json:"action" validate:"required"`
} `json:"bounce.actions" validate:"omitempty,dive"`
SESEnabled *bool `json:"bounce.ses_enabled"`
SendgridEnabled *bool `json:"bounce.sendgrid_enabled"`
SendgridKey string `json:"bounce.sendgrid_key"`
BouncePostmark struct {
Enabled bool `json:"enabled"`
Enabled *bool `json:"enabled"`
Username string `json:"username"`
Password string `json:"password"`
} `json:"bounce.postmark"`
BounceBoxes []struct {
UUID string `json:"uuid"`
Enabled bool `json:"enabled"`
Type string `json:"type"`
Host string `json:"host"`
Port int `json:"port"`
AuthProtocol string `json:"auth_protocol"`
ReturnPath string `json:"return_path"`
Username string `json:"username"`
Enabled *bool `json:"enabled" validate:"required"`
Type string `json:"type" validate:"required"`
Host string `json:"host" validate:"required"`
Port int `json:"port" validate:"required"`
AuthProtocol string `json:"auth_protocol" validate:"required"`
ReturnPath string `json:"return_path" validate:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password,omitempty"`
TLSEnabled bool `json:"tls_enabled"`
TLSSkipVerify bool `json:"tls_skip_verify"`
ScanInterval string `json:"scan_interval"`
} `json:"bounce.mailboxes"`
TLSEnabled *bool `json:"tls_enabled" validate:"required"`
TLSSkipVerify *bool `json:"tls_skip_verify" validate:"required"`
ScanInterval string `json:"scan_interval" validate:"required"`
} `json:"bounce.mailboxes" validate:"omitempty,dive"`

AdminCustomCSS string `json:"appearance.admin.custom_css"`
AdminCustomJS string `json:"appearance.admin.custom_js"`
Expand Down