diff --git a/satellite/console/consoleweb/consoleapi/auth.go b/satellite/console/consoleweb/consoleapi/auth.go index ec733740b..40e08d462 100644 --- a/satellite/console/consoleweb/consoleapi/auth.go +++ b/satellite/console/consoleweb/consoleapi/auth.go @@ -20,6 +20,7 @@ import ( "storj.io/storj/private/web" "storj.io/storj/satellite/analytics" "storj.io/storj/satellite/console" + "storj.io/storj/satellite/console/consoleweb/consoleapi/utils" "storj.io/storj/satellite/console/consoleweb/consolewebauth" "storj.io/storj/satellite/mailservice" "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. registerData.Email = strings.TrimSpace(registerData.Email) - isValidEmail := ValidateEmail(registerData.Email) + isValidEmail := utils.ValidateEmail(registerData.Email) if !isValidEmail { a.serveJSONError(w, console.ErrValidation.Wrap(errs.New("Invalid email."))) 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. // this cookie is set from the reverse proxy if the user opts into cookies from Storj. func loadSession(req *http.Request) string { diff --git a/satellite/console/consoleweb/consoleapi/auth_test.go b/satellite/console/consoleweb/consoleapi/auth_test.go index 4492ab4ba..47032a07c 100644 --- a/satellite/console/consoleweb/consoleapi/auth_test.go +++ b/satellite/console/consoleweb/consoleapi/auth_test.go @@ -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", - } - - for _, e := range invalidEmailAddresses { - result := consoleapi.ValidateEmail(e) - require.False(t, result) - } -} diff --git a/satellite/console/consoleweb/consoleapi/utils/validation.go b/satellite/console/consoleweb/consoleapi/utils/validation.go new file mode 100644 index 000000000..6cc6497a8 --- /dev/null +++ b/satellite/console/consoleweb/consoleapi/utils/validation.go @@ -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 +} diff --git a/satellite/console/consoleweb/consoleapi/utils/validation_test.go b/satellite/console/consoleweb/consoleapi/utils/validation_test.go new file mode 100644 index 000000000..a2c071dd9 --- /dev/null +++ b/satellite/console/consoleweb/consoleapi/utils/validation_test.go @@ -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", + } + + for _, e := range invalidEmailAddresses { + result := utils.ValidateEmail(e) + require.False(t, result) + } +} diff --git a/satellite/nodeevents/chore.go b/satellite/nodeevents/chore.go index 12f25172a..e2711d184 100644 --- a/satellite/nodeevents/chore.go +++ b/satellite/nodeevents/chore.go @@ -13,6 +13,7 @@ import ( "storj.io/common/sync2" "storj.io/common/uuid" + "storj.io/storj/satellite/console/consoleweb/consoleapi/utils" ) var ( @@ -92,15 +93,24 @@ func (chore *Chore) process(ctx context.Context) (n int, err error) { if err != nil { return 0, err } + if len(batch) == 0 { + return 0, nil + } + + email := batch[0].Email var rowIDs []uuid.UUID for _, event := range batch { rowIDs = append(rowIDs, event.ID) } - if err = chore.notifier.Notify(ctx, chore.satellite, batch); err != nil { - err = errs.Combine(err, chore.db.UpdateLastAttempted(ctx, rowIDs, chore.nowFn())) - return 0, err + if utils.ValidateEmail(email) { + if err = chore.notifier.Notify(ctx, chore.satellite, batch); err != nil { + err = errs.Combine(err, chore.db.UpdateLastAttempted(ctx, rowIDs, chore.nowFn())) + return 0, err + } + } else { + chore.log.Error("invalid email", zap.String("email", email)) } err = chore.db.UpdateEmailSent(ctx, rowIDs, chore.nowFn()) diff --git a/satellite/nodeevents/chore_test.go b/satellite/nodeevents/chore_test.go index 9de884ca7..0e3f50d33 100644 --- a/satellite/nodeevents/chore_test.go +++ b/satellite/nodeevents/chore_test.go @@ -15,6 +15,7 @@ import ( "storj.io/common/testcontext" "storj.io/common/uuid" "storj.io/storj/private/testplanet" + "storj.io/storj/private/teststorj" "storj.io/storj/satellite" "storj.io/storj/satellite/nodeevents" "storj.io/storj/satellite/overlay" @@ -154,3 +155,65 @@ func TestNodeEventsChoreFailedNotify(t *testing.T) { 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) + } + }) +}