satellite/nodeevents: validate emails before notifying
Simple email validation before attempting to send notifications. If the email is not valid, skip sending notifications and go to update email_sent so we don't try it again. Also, move ValidateEmail function into new package so it can be used in nodeevents without import cycle. Change-Id: I63ce0fc84f7b1d964f7cc6da61206f54baaf1a21
This commit is contained in:
parent
746c3b2d83
commit
a3ff3eb193
@ -20,6 +20,7 @@ import (
|
|||||||
"storj.io/storj/private/web"
|
"storj.io/storj/private/web"
|
||||||
"storj.io/storj/satellite/analytics"
|
"storj.io/storj/satellite/analytics"
|
||||||
"storj.io/storj/satellite/console"
|
"storj.io/storj/satellite/console"
|
||||||
|
"storj.io/storj/satellite/console/consoleweb/consoleapi/utils"
|
||||||
"storj.io/storj/satellite/console/consoleweb/consolewebauth"
|
"storj.io/storj/satellite/console/consoleweb/consolewebauth"
|
||||||
"storj.io/storj/satellite/mailservice"
|
"storj.io/storj/satellite/mailservice"
|
||||||
"storj.io/storj/satellite/rewards"
|
"storj.io/storj/satellite/rewards"
|
||||||
@ -206,7 +207,7 @@ func (a *Auth) Register(w http.ResponseWriter, r *http.Request) {
|
|||||||
// trim leading and trailing spaces of email address.
|
// trim leading and trailing spaces of email address.
|
||||||
registerData.Email = strings.TrimSpace(registerData.Email)
|
registerData.Email = strings.TrimSpace(registerData.Email)
|
||||||
|
|
||||||
isValidEmail := ValidateEmail(registerData.Email)
|
isValidEmail := utils.ValidateEmail(registerData.Email)
|
||||||
if !isValidEmail {
|
if !isValidEmail {
|
||||||
a.serveJSONError(w, console.ErrValidation.Wrap(errs.New("Invalid email.")))
|
a.serveJSONError(w, console.ErrValidation.Wrap(errs.New("Invalid email.")))
|
||||||
return
|
return
|
||||||
@ -356,15 +357,6 @@ func (a *Auth) Register(w http.ResponseWriter, r *http.Request) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateEmail validates email to have correct form and syntax.
|
|
||||||
func ValidateEmail(email string) bool {
|
|
||||||
// This regular expression was built according to RFC 5322 and then extended to include international characters.
|
|
||||||
re := regexp.MustCompile(`^(?:[a-z0-9\p{L}!#$%&'*+/=?^_{|}~\x60-]+(?:\.[a-z0-9\p{L}!#$%&'*+/=?^_{|}~\x60-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9\p{L}](?:[a-z0-9\p{L}-]*[a-z0-9\p{L}])?\.)+[a-z0-9\p{L}](?:[a-z\p{L}]*[a-z\p{L}])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9\p{L}-]*[a-z0-9\p{L}]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$`)
|
|
||||||
match := re.MatchString(email)
|
|
||||||
|
|
||||||
return match
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadSession looks for a cookie for the session id.
|
// loadSession looks for a cookie for the session id.
|
||||||
// this cookie is set from the reverse proxy if the user opts into cookies from Storj.
|
// this cookie is set from the reverse proxy if the user opts into cookies from Storj.
|
||||||
func loadSession(req *http.Request) string {
|
func loadSession(req *http.Request) string {
|
||||||
|
@ -915,41 +915,3 @@ func TestAuth_Register_PasswordLength(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEmailValidation(t *testing.T) {
|
|
||||||
invalidEmailAddresses := []string{
|
|
||||||
"test@t@t.test",
|
|
||||||
"test",
|
|
||||||
"test@!t.test",
|
|
||||||
"test@#test.test",
|
|
||||||
"test@$t.test",
|
|
||||||
"t%t.test",
|
|
||||||
"test@^test.test",
|
|
||||||
"test@&test.test",
|
|
||||||
"test@*test.test",
|
|
||||||
"test@(test.test",
|
|
||||||
"test@)test.test",
|
|
||||||
"test@=test.test",
|
|
||||||
"test@[test.test",
|
|
||||||
"test@]test.test",
|
|
||||||
"test@{test.test",
|
|
||||||
"test@}test.test",
|
|
||||||
"test@/test.test",
|
|
||||||
"test@\\test.test",
|
|
||||||
"test@|test.test",
|
|
||||||
"test@:test.test",
|
|
||||||
"test@;test.test",
|
|
||||||
"test@,test.test",
|
|
||||||
"test@\"test.test",
|
|
||||||
"test@'test.test",
|
|
||||||
"test@<test.test",
|
|
||||||
"test@>test.test",
|
|
||||||
"test@_test.test",
|
|
||||||
"test@?test.test",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, e := range invalidEmailAddresses {
|
|
||||||
result := consoleapi.ValidateEmail(e)
|
|
||||||
require.False(t, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
15
satellite/console/consoleweb/consoleapi/utils/validation.go
Normal file
15
satellite/console/consoleweb/consoleapi/utils/validation.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
// Copyright (C) 2022 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import "regexp"
|
||||||
|
|
||||||
|
// ValidateEmail validates email to have correct form and syntax.
|
||||||
|
func ValidateEmail(email string) bool {
|
||||||
|
// This regular expression was built according to RFC 5322 and then extended to include international characters.
|
||||||
|
re := regexp.MustCompile(`^(?:[a-z0-9\p{L}!#$%&'*+/=?^_{|}~\x60-]+(?:\.[a-z0-9\p{L}!#$%&'*+/=?^_{|}~\x60-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9\p{L}](?:[a-z0-9\p{L}-]*[a-z0-9\p{L}])?\.)+[a-z0-9\p{L}](?:[a-z\p{L}]*[a-z\p{L}])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9\p{L}-]*[a-z0-9\p{L}]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$`)
|
||||||
|
match := re.MatchString(email)
|
||||||
|
|
||||||
|
return match
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
// Copyright (C) 2022 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
|
package utils_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"storj.io/storj/satellite/console/consoleweb/consoleapi/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEmailValidation(t *testing.T) {
|
||||||
|
invalidEmailAddresses := []string{
|
||||||
|
"test@t@t.test",
|
||||||
|
"test",
|
||||||
|
"test@!t.test",
|
||||||
|
"test@#test.test",
|
||||||
|
"test@$t.test",
|
||||||
|
"t%t.test",
|
||||||
|
"test@^test.test",
|
||||||
|
"test@&test.test",
|
||||||
|
"test@*test.test",
|
||||||
|
"test@(test.test",
|
||||||
|
"test@)test.test",
|
||||||
|
"test@=test.test",
|
||||||
|
"test@[test.test",
|
||||||
|
"test@]test.test",
|
||||||
|
"test@{test.test",
|
||||||
|
"test@}test.test",
|
||||||
|
"test@/test.test",
|
||||||
|
"test@\\test.test",
|
||||||
|
"test@|test.test",
|
||||||
|
"test@:test.test",
|
||||||
|
"test@;test.test",
|
||||||
|
"test@,test.test",
|
||||||
|
"test@\"test.test",
|
||||||
|
"test@'test.test",
|
||||||
|
"test@<test.test",
|
||||||
|
"test@>test.test",
|
||||||
|
"test@_test.test",
|
||||||
|
"test@?test.test",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range invalidEmailAddresses {
|
||||||
|
result := utils.ValidateEmail(e)
|
||||||
|
require.False(t, result)
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"storj.io/common/sync2"
|
"storj.io/common/sync2"
|
||||||
"storj.io/common/uuid"
|
"storj.io/common/uuid"
|
||||||
|
"storj.io/storj/satellite/console/consoleweb/consoleapi/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -92,16 +93,25 @@ func (chore *Chore) process(ctx context.Context) (n int, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
if len(batch) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
email := batch[0].Email
|
||||||
|
|
||||||
var rowIDs []uuid.UUID
|
var rowIDs []uuid.UUID
|
||||||
for _, event := range batch {
|
for _, event := range batch {
|
||||||
rowIDs = append(rowIDs, event.ID)
|
rowIDs = append(rowIDs, event.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if utils.ValidateEmail(email) {
|
||||||
if err = chore.notifier.Notify(ctx, chore.satellite, batch); err != nil {
|
if err = chore.notifier.Notify(ctx, chore.satellite, batch); err != nil {
|
||||||
err = errs.Combine(err, chore.db.UpdateLastAttempted(ctx, rowIDs, chore.nowFn()))
|
err = errs.Combine(err, chore.db.UpdateLastAttempted(ctx, rowIDs, chore.nowFn()))
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
chore.log.Error("invalid email", zap.String("email", email))
|
||||||
|
}
|
||||||
|
|
||||||
err = chore.db.UpdateEmailSent(ctx, rowIDs, chore.nowFn())
|
err = chore.db.UpdateEmailSent(ctx, rowIDs, chore.nowFn())
|
||||||
return len(batch), err
|
return len(batch), err
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
"storj.io/common/testcontext"
|
"storj.io/common/testcontext"
|
||||||
"storj.io/common/uuid"
|
"storj.io/common/uuid"
|
||||||
"storj.io/storj/private/testplanet"
|
"storj.io/storj/private/testplanet"
|
||||||
|
"storj.io/storj/private/teststorj"
|
||||||
"storj.io/storj/satellite"
|
"storj.io/storj/satellite"
|
||||||
"storj.io/storj/satellite/nodeevents"
|
"storj.io/storj/satellite/nodeevents"
|
||||||
"storj.io/storj/satellite/overlay"
|
"storj.io/storj/satellite/overlay"
|
||||||
@ -154,3 +155,65 @@ func TestNodeEventsChoreFailedNotify(t *testing.T) {
|
|||||||
require.Nil(t, event.EmailSent)
|
require.Nil(t, event.EmailSent)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNodeEventsChoreInvalidEmails(t *testing.T) {
|
||||||
|
testplanet.Run(t, testplanet.Config{
|
||||||
|
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
||||||
|
Reconfigure: testplanet.Reconfigure{
|
||||||
|
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
||||||
|
config.Overlay.SendNodeEmails = true
|
||||||
|
config.NodeEvents.SelectionWaitPeriod = 5 * time.Minute
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||||
|
sat := planet.Satellites[0]
|
||||||
|
|
||||||
|
// just a handful of emails, not exhaustive
|
||||||
|
emails := []string{
|
||||||
|
"",
|
||||||
|
"abc",
|
||||||
|
"abc.storj.test",
|
||||||
|
"abc@def@storj.test",
|
||||||
|
"abc\"@storj.test",
|
||||||
|
"abc@storj..test",
|
||||||
|
"abc @storj.test",
|
||||||
|
|
||||||
|
// one valid email as a control group
|
||||||
|
"abc@storj.test",
|
||||||
|
}
|
||||||
|
|
||||||
|
validEmail := emails[len(emails)-1]
|
||||||
|
|
||||||
|
chore := sat.NodeEvents.Chore
|
||||||
|
chore.Loop.Pause()
|
||||||
|
|
||||||
|
tn := &TestNotifier{
|
||||||
|
notifications: make(map[string][]nodeevents.NodeEvent),
|
||||||
|
}
|
||||||
|
chore.SetNotifier(tn)
|
||||||
|
|
||||||
|
// set nowFn on chore to 5 minutes in the future to test that chore will select node events.
|
||||||
|
futureTime := func() time.Time {
|
||||||
|
return time.Now().Add(5 * time.Minute)
|
||||||
|
}
|
||||||
|
chore.SetNow(futureTime)
|
||||||
|
|
||||||
|
event := nodeevents.Disqualified
|
||||||
|
for _, e := range emails {
|
||||||
|
_, err := sat.DB.NodeEvents().Insert(ctx, e, teststorj.NodeIDFromString("test"), event)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
chore.Loop.TriggerWait()
|
||||||
|
|
||||||
|
require.Len(t, tn.notifications, 1)
|
||||||
|
require.NotEmpty(t, tn.notifications[validEmail])
|
||||||
|
|
||||||
|
// Check that email_sent is not null for invalid emails, so they don't clog up the table
|
||||||
|
for _, e := range emails {
|
||||||
|
ne, err := sat.DB.NodeEvents().GetLatestByEmailAndEvent(ctx, e, event)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, ne.EmailSent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user