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:
Cameron 2022-11-18 11:25:31 -05:00
parent 746c3b2d83
commit a3ff3eb193
6 changed files with 143 additions and 51 deletions

View File

@ -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 {

View File

@ -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)
}
}

View 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
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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)
}
})
}