summaryrefslogtreecommitdiff
path: root/internal/util
diff options
context:
space:
mode:
Diffstat (limited to 'internal/util')
-rw-r--r--internal/util/parse.go96
-rw-r--r--internal/util/regexes.go79
-rw-r--r--internal/util/statustools.go (renamed from internal/util/status.go)20
-rw-r--r--internal/util/statustools_test.go (renamed from internal/util/status_test.go)11
-rw-r--r--internal/util/uri.go218
-rw-r--r--internal/util/validation.go49
-rw-r--r--internal/util/validation_test.go81
7 files changed, 357 insertions, 197 deletions
diff --git a/internal/util/parse.go b/internal/util/parse.go
deleted file mode 100644
index f0bcff5dc..000000000
--- a/internal/util/parse.go
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package util
-
-import (
- "fmt"
-
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- mastotypes "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
-)
-
-// URIs contains a bunch of URIs and URLs for a user, host, account, etc.
-type URIs struct {
- HostURL string
- UserURL string
- StatusesURL string
-
- UserURI string
- StatusesURI string
- InboxURI string
- OutboxURI string
- FollowersURI string
- CollectionURI string
-}
-
-// GenerateURIs throws together a bunch of URIs for the given username, with the given protocol and host.
-func GenerateURIs(username string, protocol string, host string) *URIs {
- hostURL := fmt.Sprintf("%s://%s", protocol, host)
- userURL := fmt.Sprintf("%s/@%s", hostURL, username)
- statusesURL := fmt.Sprintf("%s/statuses", userURL)
-
- userURI := fmt.Sprintf("%s/users/%s", hostURL, username)
- statusesURI := fmt.Sprintf("%s/statuses", userURI)
- inboxURI := fmt.Sprintf("%s/inbox", userURI)
- outboxURI := fmt.Sprintf("%s/outbox", userURI)
- followersURI := fmt.Sprintf("%s/followers", userURI)
- collectionURI := fmt.Sprintf("%s/collections/featured", userURI)
- return &URIs{
- HostURL: hostURL,
- UserURL: userURL,
- StatusesURL: statusesURL,
-
- UserURI: userURI,
- StatusesURI: statusesURI,
- InboxURI: inboxURI,
- OutboxURI: outboxURI,
- FollowersURI: followersURI,
- CollectionURI: collectionURI,
- }
-}
-
-// ParseGTSVisFromMastoVis converts a mastodon visibility into its gts equivalent.
-func ParseGTSVisFromMastoVis(m mastotypes.Visibility) gtsmodel.Visibility {
- switch m {
- case mastotypes.VisibilityPublic:
- return gtsmodel.VisibilityPublic
- case mastotypes.VisibilityUnlisted:
- return gtsmodel.VisibilityUnlocked
- case mastotypes.VisibilityPrivate:
- return gtsmodel.VisibilityFollowersOnly
- case mastotypes.VisibilityDirect:
- return gtsmodel.VisibilityDirect
- }
- return ""
-}
-
-// ParseMastoVisFromGTSVis converts a gts visibility into its mastodon equivalent
-func ParseMastoVisFromGTSVis(m gtsmodel.Visibility) mastotypes.Visibility {
- switch m {
- case gtsmodel.VisibilityPublic:
- return mastotypes.VisibilityPublic
- case gtsmodel.VisibilityUnlocked:
- return mastotypes.VisibilityUnlisted
- case gtsmodel.VisibilityFollowersOnly, gtsmodel.VisibilityMutualsOnly:
- return mastotypes.VisibilityPrivate
- case gtsmodel.VisibilityDirect:
- return mastotypes.VisibilityDirect
- }
- return ""
-}
diff --git a/internal/util/regexes.go b/internal/util/regexes.go
index 60b397d86..a59bd678a 100644
--- a/internal/util/regexes.go
+++ b/internal/util/regexes.go
@@ -18,19 +18,78 @@
package util
-import "regexp"
+import (
+ "fmt"
+ "regexp"
+)
+
+const (
+ minimumPasswordEntropy = 60 // dictates password strength. See https://github.com/wagslane/go-password-validator
+ minimumReasonLength = 40
+ maximumReasonLength = 500
+ maximumEmailLength = 256
+ maximumUsernameLength = 64
+ maximumPasswordLength = 64
+ maximumEmojiShortcodeLength = 30
+ maximumHashtagLength = 30
+)
var (
// mention regex can be played around with here: https://regex101.com/r/qwM9D3/1
- mentionRegexString = `(?: |^|\W)(@[a-zA-Z0-9_]+(?:@[a-zA-Z0-9_\-\.]+)?)(?: |\n)`
- mentionRegex = regexp.MustCompile(mentionRegexString)
+ mentionFinderRegexString = `(?: |^|\W)(@[a-zA-Z0-9_]+(?:@[a-zA-Z0-9_\-\.]+)?)(?: |\n)`
+ mentionFinderRegex = regexp.MustCompile(mentionFinderRegexString)
+
// hashtag regex can be played with here: https://regex101.com/r/Vhy8pg/1
- hashtagRegexString = `(?: |^|\W)?#([a-zA-Z0-9]{1,30})(?:\b|\r)`
- hashtagRegex = regexp.MustCompile(hashtagRegexString)
- // emoji regex can be played with here: https://regex101.com/r/478XGM/1
- emojiRegexString = `(?: |^|\W)?:([a-zA-Z0-9_]{2,30}):(?:\b|\r)?`
- emojiRegex = regexp.MustCompile(emojiRegexString)
+ hashtagFinderRegexString = fmt.Sprintf(`(?: |^|\W)?#([a-zA-Z0-9]{1,%d})(?:\b|\r)`, maximumHashtagLength)
+ hashtagFinderRegex = regexp.MustCompile(hashtagFinderRegexString)
+
// emoji shortcode regex can be played with here: https://regex101.com/r/zMDRaG/1
- emojiShortcodeString = `^[a-z0-9_]{2,30}$`
- emojiShortcodeRegex = regexp.MustCompile(emojiShortcodeString)
+ emojiShortcodeRegexString = fmt.Sprintf(`[a-z0-9_]{2,%d}`, maximumEmojiShortcodeLength)
+ emojiShortcodeValidationRegex = regexp.MustCompile(fmt.Sprintf("^%s$", emojiShortcodeRegexString))
+
+ // emoji regex can be played with here: https://regex101.com/r/478XGM/1
+ emojiFinderRegexString = fmt.Sprintf(`(?: |^|\W)?:(%s):(?:\b|\r)?`, emojiShortcodeRegexString)
+ emojiFinderRegex = regexp.MustCompile(emojiFinderRegexString)
+
+ // usernameRegexString defines an acceptable username on this instance
+ usernameRegexString = fmt.Sprintf(`[a-z0-9_]{2,%d}`, maximumUsernameLength)
+ // usernameValidationRegex can be used to validate usernames of new signups
+ usernameValidationRegex = regexp.MustCompile(fmt.Sprintf(`^%s$`, usernameRegexString))
+
+ userPathRegexString = fmt.Sprintf(`^?/%s/(%s)$`, UsersPath, usernameRegexString)
+ // userPathRegex parses a path that validates and captures the username part from eg /users/example_username
+ userPathRegex = regexp.MustCompile(userPathRegexString)
+
+ inboxPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s$`, UsersPath, usernameRegexString, InboxPath)
+ // inboxPathRegex parses a path that validates and captures the username part from eg /users/example_username/inbox
+ inboxPathRegex = regexp.MustCompile(inboxPathRegexString)
+
+ outboxPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s$`, UsersPath, usernameRegexString, OutboxPath)
+ // outboxPathRegex parses a path that validates and captures the username part from eg /users/example_username/outbox
+ outboxPathRegex = regexp.MustCompile(outboxPathRegexString)
+
+ actorPathRegexString = fmt.Sprintf(`^?/%s/(%s)$`, ActorsPath, usernameRegexString)
+ // actorPathRegex parses a path that validates and captures the username part from eg /actors/example_username
+ actorPathRegex = regexp.MustCompile(actorPathRegexString)
+
+ followersPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s$`, UsersPath, usernameRegexString, FollowersPath)
+ // followersPathRegex parses a path that validates and captures the username part from eg /users/example_username/followers
+ followersPathRegex = regexp.MustCompile(followersPathRegexString)
+
+ followingPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s$`, UsersPath, usernameRegexString, FollowingPath)
+ // followingPathRegex parses a path that validates and captures the username part from eg /users/example_username/following
+ followingPathRegex = regexp.MustCompile(followingPathRegexString)
+
+ likedPathRegexString = fmt.Sprintf(`^/?%s/%s/%s$`, UsersPath, usernameRegexString, LikedPath)
+ // followingPathRegex parses a path that validates and captures the username part from eg /users/example_username/liked
+ likedPathRegex = regexp.MustCompile(likedPathRegexString)
+
+ // see https://ihateregex.io/expr/uuid/
+ uuidRegexString = `[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}`
+
+ statusesPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, UsersPath, usernameRegexString, StatusesPath, uuidRegexString)
+ // statusesPathRegex parses a path that validates and captures the username part and the uuid part
+ // from eg /users/example_username/statuses/123e4567-e89b-12d3-a456-426655440000.
+ // The regex can be played with here: https://regex101.com/r/G9zuxQ/1
+ statusesPathRegex = regexp.MustCompile(statusesPathRegexString)
)
diff --git a/internal/util/status.go b/internal/util/statustools.go
index e4b3ec6a5..5591f185a 100644
--- a/internal/util/status.go
+++ b/internal/util/statustools.go
@@ -31,10 +31,10 @@ import (
// The case of the returned mentions will be lowered, for consistency.
func DeriveMentions(status string) []string {
mentionedAccounts := []string{}
- for _, m := range mentionRegex.FindAllStringSubmatch(status, -1) {
+ for _, m := range mentionFinderRegex.FindAllStringSubmatch(status, -1) {
mentionedAccounts = append(mentionedAccounts, m[1])
}
- return Lower(Unique(mentionedAccounts))
+ return lower(unique(mentionedAccounts))
}
// DeriveHashtags takes a plaintext (ie., not html-formatted) status,
@@ -43,10 +43,10 @@ func DeriveMentions(status string) []string {
// tags will be lowered, for consistency.
func DeriveHashtags(status string) []string {
tags := []string{}
- for _, m := range hashtagRegex.FindAllStringSubmatch(status, -1) {
+ for _, m := range hashtagFinderRegex.FindAllStringSubmatch(status, -1) {
tags = append(tags, m[1])
}
- return Lower(Unique(tags))
+ return lower(unique(tags))
}
// DeriveEmojis takes a plaintext (ie., not html-formatted) status,
@@ -55,14 +55,14 @@ func DeriveHashtags(status string) []string {
// emojis will be lowered, for consistency.
func DeriveEmojis(status string) []string {
emojis := []string{}
- for _, m := range emojiRegex.FindAllStringSubmatch(status, -1) {
+ for _, m := range emojiFinderRegex.FindAllStringSubmatch(status, -1) {
emojis = append(emojis, m[1])
}
- return Lower(Unique(emojis))
+ return lower(unique(emojis))
}
-// Unique returns a deduplicated version of a given string slice.
-func Unique(s []string) []string {
+// unique returns a deduplicated version of a given string slice.
+func unique(s []string) []string {
keys := make(map[string]bool)
list := []string{}
for _, entry := range s {
@@ -74,8 +74,8 @@ func Unique(s []string) []string {
return list
}
-// Lower lowercases all strings in a given string slice
-func Lower(s []string) []string {
+// lower lowercases all strings in a given string slice
+func lower(s []string) []string {
new := []string{}
for _, i := range s {
new = append(new, strings.ToLower(i))
diff --git a/internal/util/status_test.go b/internal/util/statustools_test.go
index 72bd3e885..7c9af2cbd 100644
--- a/internal/util/status_test.go
+++ b/internal/util/statustools_test.go
@@ -16,13 +16,14 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-package util
+package util_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
)
type StatusTestSuite struct {
@@ -41,7 +42,7 @@ func (suite *StatusTestSuite) TestDeriveMentionsOK() {
here is a duplicate mention: @hello@test.lgbt
`
- menchies := DeriveMentions(statusText)
+ menchies := util.DeriveMentions(statusText)
assert.Len(suite.T(), menchies, 4)
assert.Equal(suite.T(), "@dumpsterqueer@example.org", menchies[0])
assert.Equal(suite.T(), "@someone_else@testing.best-horse.com", menchies[1])
@@ -51,7 +52,7 @@ func (suite *StatusTestSuite) TestDeriveMentionsOK() {
func (suite *StatusTestSuite) TestDeriveMentionsEmpty() {
statusText := ``
- menchies := DeriveMentions(statusText)
+ menchies := util.DeriveMentions(statusText)
assert.Len(suite.T(), menchies, 0)
}
@@ -66,7 +67,7 @@ func (suite *StatusTestSuite) TestDeriveHashtagsOK() {
#111111 thisalsoshouldn'twork#### ##`
- tags := DeriveHashtags(statusText)
+ tags := util.DeriveHashtags(statusText)
assert.Len(suite.T(), tags, 5)
assert.Equal(suite.T(), "testing123", tags[0])
assert.Equal(suite.T(), "also", tags[1])
@@ -89,7 +90,7 @@ Here's some normal text with an :emoji: at the end
:underscores_ok_too:
`
- tags := DeriveEmojis(statusText)
+ tags := util.DeriveEmojis(statusText)
assert.Len(suite.T(), tags, 7)
assert.Equal(suite.T(), "test", tags[0])
assert.Equal(suite.T(), "another", tags[1])
diff --git a/internal/util/uri.go b/internal/util/uri.go
new file mode 100644
index 000000000..9b96edc61
--- /dev/null
+++ b/internal/util/uri.go
@@ -0,0 +1,218 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package util
+
+import (
+ "fmt"
+ "net/url"
+ "strings"
+)
+
+const (
+ // UsersPath is for serving users info
+ UsersPath = "users"
+ // ActorsPath is for serving actors info
+ ActorsPath = "actors"
+ // StatusesPath is for serving statuses
+ StatusesPath = "statuses"
+ // InboxPath represents the webfinger inbox location
+ InboxPath = "inbox"
+ // OutboxPath represents the webfinger outbox location
+ OutboxPath = "outbox"
+ // FollowersPath represents the webfinger followers location
+ FollowersPath = "followers"
+ // FollowingPath represents the webfinger following location
+ FollowingPath = "following"
+ // LikedPath represents the webfinger liked location
+ LikedPath = "liked"
+ // CollectionsPath represents the webfinger collections location
+ CollectionsPath = "collections"
+ // FeaturedPath represents the webfinger featured location
+ FeaturedPath = "featured"
+ // PublicKeyPath is for serving an account's public key
+ PublicKeyPath = "publickey"
+)
+
+// APContextKey is a type used specifically for settings values on contexts within go-fed AP request chains
+type APContextKey string
+
+const (
+ // APActivity can be used to set and retrieve the actual go-fed pub.Activity within a context.
+ APActivity APContextKey = "activity"
+ // APAccount can be used the set and retrieve the account being interacted with
+ APAccount APContextKey = "account"
+ // APRequestingAccount can be used to set and retrieve the account of an incoming federation request.
+ APRequestingAccount APContextKey = "requestingAccount"
+ // APRequestingPublicKeyID can be used to set and retrieve the public key ID of an incoming federation request.
+ APRequestingPublicKeyID APContextKey = "requestingPublicKeyID"
+)
+
+type ginContextKey struct{}
+
+// GinContextKey is used solely for setting and retrieving the gin context from a context.Context
+var GinContextKey = &ginContextKey{}
+
+// UserURIs contains a bunch of UserURIs and URLs for a user, host, account, etc.
+type UserURIs struct {
+ // The web URL of the instance host, eg https://example.org
+ HostURL string
+ // The web URL of the user, eg., https://example.org/@example_user
+ UserURL string
+ // The web URL for statuses of this user, eg., https://example.org/@example_user/statuses
+ StatusesURL string
+
+ // The webfinger URI of this user, eg., https://example.org/users/example_user
+ UserURI string
+ // The webfinger URI for this user's statuses, eg., https://example.org/users/example_user/statuses
+ StatusesURI string
+ // The webfinger URI for this user's activitypub inbox, eg., https://example.org/users/example_user/inbox
+ InboxURI string
+ // The webfinger URI for this user's activitypub outbox, eg., https://example.org/users/example_user/outbox
+ OutboxURI string
+ // The webfinger URI for this user's followers, eg., https://example.org/users/example_user/followers
+ FollowersURI string
+ // The webfinger URI for this user's following, eg., https://example.org/users/example_user/following
+ FollowingURI string
+ // The webfinger URI for this user's liked posts eg., https://example.org/users/example_user/liked
+ LikedURI string
+ // The webfinger URI for this user's featured collections, eg., https://example.org/users/example_user/collections/featured
+ CollectionURI string
+ // The URI for this user's public key, eg., https://example.org/users/example_user/publickey
+ PublicKeyURI string
+}
+
+// GenerateURIsForAccount throws together a bunch of URIs for the given username, with the given protocol and host.
+func GenerateURIsForAccount(username string, protocol string, host string) *UserURIs {
+ // The below URLs are used for serving web requests
+ hostURL := fmt.Sprintf("%s://%s", protocol, host)
+ userURL := fmt.Sprintf("%s/@%s", hostURL, username)
+ statusesURL := fmt.Sprintf("%s/%s", userURL, StatusesPath)
+
+ // the below URIs are used in ActivityPub and Webfinger
+ userURI := fmt.Sprintf("%s/%s/%s", hostURL, UsersPath, username)
+ statusesURI := fmt.Sprintf("%s/%s", userURI, StatusesPath)
+ inboxURI := fmt.Sprintf("%s/%s", userURI, InboxPath)
+ outboxURI := fmt.Sprintf("%s/%s", userURI, OutboxPath)
+ followersURI := fmt.Sprintf("%s/%s", userURI, FollowersPath)
+ followingURI := fmt.Sprintf("%s/%s", userURI, FollowingPath)
+ likedURI := fmt.Sprintf("%s/%s", userURI, LikedPath)
+ collectionURI := fmt.Sprintf("%s/%s/%s", userURI, CollectionsPath, FeaturedPath)
+ publicKeyURI := fmt.Sprintf("%s/%s", userURI, PublicKeyPath)
+
+ return &UserURIs{
+ HostURL: hostURL,
+ UserURL: userURL,
+ StatusesURL: statusesURL,
+
+ UserURI: userURI,
+ StatusesURI: statusesURI,
+ InboxURI: inboxURI,
+ OutboxURI: outboxURI,
+ FollowersURI: followersURI,
+ FollowingURI: followingURI,
+ LikedURI: likedURI,
+ CollectionURI: collectionURI,
+ PublicKeyURI: publicKeyURI,
+ }
+}
+
+// IsUserPath returns true if the given URL path corresponds to eg /users/example_username
+func IsUserPath(id *url.URL) bool {
+ return userPathRegex.MatchString(strings.ToLower(id.Path))
+}
+
+// IsInboxPath returns true if the given URL path corresponds to eg /users/example_username/inbox
+func IsInboxPath(id *url.URL) bool {
+ return inboxPathRegex.MatchString(strings.ToLower(id.Path))
+}
+
+// IsOutboxPath returns true if the given URL path corresponds to eg /users/example_username/outbox
+func IsOutboxPath(id *url.URL) bool {
+ return outboxPathRegex.MatchString(strings.ToLower(id.Path))
+}
+
+// IsInstanceActorPath returns true if the given URL path corresponds to eg /actors/example_username
+func IsInstanceActorPath(id *url.URL) bool {
+ return actorPathRegex.MatchString(strings.ToLower(id.Path))
+}
+
+// IsFollowersPath returns true if the given URL path corresponds to eg /users/example_username/followers
+func IsFollowersPath(id *url.URL) bool {
+ return followersPathRegex.MatchString(strings.ToLower(id.Path))
+}
+
+// IsFollowingPath returns true if the given URL path corresponds to eg /users/example_username/following
+func IsFollowingPath(id *url.URL) bool {
+ return followingPathRegex.MatchString(strings.ToLower(id.Path))
+}
+
+// IsLikedPath returns true if the given URL path corresponds to eg /users/example_username/liked
+func IsLikedPath(id *url.URL) bool {
+ return likedPathRegex.MatchString(strings.ToLower(id.Path))
+}
+
+// IsStatusesPath returns true if the given URL path corresponds to eg /users/example_username/statuses/SOME_UUID_OF_A_STATUS
+func IsStatusesPath(id *url.URL) bool {
+ return statusesPathRegex.MatchString(strings.ToLower(id.Path))
+}
+
+// ParseStatusesPath returns the username and uuid from a path such as /users/example_username/statuses/SOME_UUID_OF_A_STATUS
+func ParseStatusesPath(id *url.URL) (username string, uuid string, err error) {
+ matches := statusesPathRegex.FindStringSubmatch(id.Path)
+ if len(matches) != 3 {
+ err = fmt.Errorf("expected 3 matches but matches length was %d", len(matches))
+ return
+ }
+ username = matches[1]
+ uuid = matches[2]
+ return
+}
+
+// ParseUserPath returns the username from a path such as /users/example_username
+func ParseUserPath(id *url.URL) (username string, err error) {
+ matches := userPathRegex.FindStringSubmatch(id.Path)
+ if len(matches) != 2 {
+ err = fmt.Errorf("expected 2 matches but matches length was %d", len(matches))
+ return
+ }
+ username = matches[1]
+ return
+}
+
+// ParseInboxPath returns the username from a path such as /users/example_username/inbox
+func ParseInboxPath(id *url.URL) (username string, err error) {
+ matches := inboxPathRegex.FindStringSubmatch(id.Path)
+ if len(matches) != 2 {
+ err = fmt.Errorf("expected 2 matches but matches length was %d", len(matches))
+ return
+ }
+ username = matches[1]
+ return
+}
+
+// ParseOutboxPath returns the username from a path such as /users/example_username/outbox
+func ParseOutboxPath(id *url.URL) (username string, err error) {
+ matches := outboxPathRegex.FindStringSubmatch(id.Path)
+ if len(matches) != 2 {
+ err = fmt.Errorf("expected 2 matches but matches length was %d", len(matches))
+ return
+ }
+ username = matches[1]
+ return
+}
diff --git a/internal/util/validation.go b/internal/util/validation.go
index acf0e68cd..d392231bb 100644
--- a/internal/util/validation.go
+++ b/internal/util/validation.go
@@ -22,45 +22,22 @@ import (
"errors"
"fmt"
"net/mail"
- "regexp"
pwv "github.com/wagslane/go-password-validator"
"golang.org/x/text/language"
)
-const (
- // MinimumPasswordEntropy dictates password strength. See https://github.com/wagslane/go-password-validator
- MinimumPasswordEntropy = 60
- // MinimumReasonLength is the length of chars we expect as a bare minimum effort
- MinimumReasonLength = 40
- // MaximumReasonLength is the maximum amount of chars we're happy to accept
- MaximumReasonLength = 500
- // MaximumEmailLength is the maximum length of an email address we're happy to accept
- MaximumEmailLength = 256
- // MaximumUsernameLength is the maximum length of a username we're happy to accept
- MaximumUsernameLength = 64
- // MaximumPasswordLength is the maximum length of a password we're happy to accept
- MaximumPasswordLength = 64
- // NewUsernameRegexString is string representation of the regular expression for validating usernames
- NewUsernameRegexString = `^[a-z0-9_]+$`
-)
-
-var (
- // NewUsernameRegex is the compiled regex for validating new usernames
- NewUsernameRegex = regexp.MustCompile(NewUsernameRegexString)
-)
-
// ValidateNewPassword returns an error if the given password is not sufficiently strong, or nil if it's ok.
func ValidateNewPassword(password string) error {
if password == "" {
return errors.New("no password provided")
}
- if len(password) > MaximumPasswordLength {
- return fmt.Errorf("password should be no more than %d chars", MaximumPasswordLength)
+ if len(password) > maximumPasswordLength {
+ return fmt.Errorf("password should be no more than %d chars", maximumPasswordLength)
}
- return pwv.Validate(password, MinimumPasswordEntropy)
+ return pwv.Validate(password, minimumPasswordEntropy)
}
// ValidateUsername makes sure that a given username is valid (ie., letters, numbers, underscores, check length).
@@ -70,11 +47,11 @@ func ValidateUsername(username string) error {
return errors.New("no username provided")
}
- if len(username) > MaximumUsernameLength {
- return fmt.Errorf("username should be no more than %d chars but '%s' was %d", MaximumUsernameLength, username, len(username))
+ if len(username) > maximumUsernameLength {
+ return fmt.Errorf("username should be no more than %d chars but '%s' was %d", maximumUsernameLength, username, len(username))
}
- if !NewUsernameRegex.MatchString(username) {
+ if !usernameValidationRegex.MatchString(username) {
return fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores", username)
}
@@ -88,8 +65,8 @@ func ValidateEmail(email string) error {
return errors.New("no email provided")
}
- if len(email) > MaximumEmailLength {
- return fmt.Errorf("email address should be no more than %d chars but '%s' was %d", MaximumEmailLength, email, len(email))
+ if len(email) > maximumEmailLength {
+ return fmt.Errorf("email address should be no more than %d chars but '%s' was %d", maximumEmailLength, email, len(email))
}
_, err := mail.ParseAddress(email)
@@ -118,12 +95,12 @@ func ValidateSignUpReason(reason string, reasonRequired bool) error {
return errors.New("no reason provided")
}
- if len(reason) < MinimumReasonLength {
- return fmt.Errorf("reason should be at least %d chars but '%s' was %d", MinimumReasonLength, reason, len(reason))
+ if len(reason) < minimumReasonLength {
+ return fmt.Errorf("reason should be at least %d chars but '%s' was %d", minimumReasonLength, reason, len(reason))
}
- if len(reason) > MaximumReasonLength {
- return fmt.Errorf("reason should be no more than %d chars but given reason was %d", MaximumReasonLength, len(reason))
+ if len(reason) > maximumReasonLength {
+ return fmt.Errorf("reason should be no more than %d chars but given reason was %d", maximumReasonLength, len(reason))
}
return nil
}
@@ -150,7 +127,7 @@ func ValidatePrivacy(privacy string) error {
// for emoji shortcodes, to figure out whether it's a valid shortcode, ie., 2-30 characters,
// lowercase a-z, numbers, and underscores.
func ValidateEmojiShortcode(shortcode string) error {
- if !emojiShortcodeRegex.MatchString(shortcode) {
+ if !emojiShortcodeValidationRegex.MatchString(shortcode) {
return fmt.Errorf("shortcode %s did not pass validation, must be between 2 and 30 characters, lowercase letters, numbers, and underscores only", shortcode)
}
return nil
diff --git a/internal/util/validation_test.go b/internal/util/validation_test.go
index dbac5e248..73f5cb977 100644
--- a/internal/util/validation_test.go
+++ b/internal/util/validation_test.go
@@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-package util
+package util_test
import (
"errors"
@@ -25,6 +25,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
)
type ValidationTestSuite struct {
@@ -42,42 +43,42 @@ func (suite *ValidationTestSuite) TestCheckPasswordStrength() {
strongPassword := "3dX5@Zc%mV*W2MBNEy$@"
var err error
- err = ValidateNewPassword(empty)
+ err = util.ValidateNewPassword(empty)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), errors.New("no password provided"), err)
}
- err = ValidateNewPassword(terriblePassword)
+ err = util.ValidateNewPassword(terriblePassword)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), errors.New("insecure password, try including more special characters, using uppercase letters, using numbers or using a longer password"), err)
}
- err = ValidateNewPassword(weakPassword)
+ err = util.ValidateNewPassword(weakPassword)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), errors.New("insecure password, try including more special characters, using numbers or using a longer password"), err)
}
- err = ValidateNewPassword(shortPassword)
+ err = util.ValidateNewPassword(shortPassword)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), errors.New("insecure password, try including more special characters or using a longer password"), err)
}
- err = ValidateNewPassword(specialPassword)
+ err = util.ValidateNewPassword(specialPassword)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), errors.New("insecure password, try including more special characters or using a longer password"), err)
}
- err = ValidateNewPassword(longPassword)
+ err = util.ValidateNewPassword(longPassword)
if assert.NoError(suite.T(), err) {
assert.Equal(suite.T(), nil, err)
}
- err = ValidateNewPassword(tooLong)
+ err = util.ValidateNewPassword(tooLong)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), errors.New("password should be no more than 64 chars"), err)
}
- err = ValidateNewPassword(strongPassword)
+ err = util.ValidateNewPassword(strongPassword)
if assert.NoError(suite.T(), err) {
assert.Equal(suite.T(), nil, err)
}
@@ -94,42 +95,42 @@ func (suite *ValidationTestSuite) TestValidateUsername() {
goodUsername := "this_is_a_good_username"
var err error
- err = ValidateUsername(empty)
+ err = util.ValidateUsername(empty)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), errors.New("no username provided"), err)
}
- err = ValidateUsername(tooLong)
+ err = util.ValidateUsername(tooLong)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), fmt.Errorf("username should be no more than 64 chars but '%s' was 66", tooLong), err)
}
- err = ValidateUsername(withSpaces)
+ err = util.ValidateUsername(withSpaces)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores", withSpaces), err)
}
- err = ValidateUsername(weirdChars)
+ err = util.ValidateUsername(weirdChars)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores", weirdChars), err)
}
- err = ValidateUsername(leadingSpace)
+ err = util.ValidateUsername(leadingSpace)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores", leadingSpace), err)
}
- err = ValidateUsername(trailingSpace)
+ err = util.ValidateUsername(trailingSpace)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores", trailingSpace), err)
}
- err = ValidateUsername(newlines)
+ err = util.ValidateUsername(newlines)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores", newlines), err)
}
- err = ValidateUsername(goodUsername)
+ err = util.ValidateUsername(goodUsername)
if assert.NoError(suite.T(), err) {
assert.Equal(suite.T(), nil, err)
}
@@ -144,32 +145,32 @@ func (suite *ValidationTestSuite) TestValidateEmail() {
emailAddress := "thisis.actually@anemail.address"
var err error
- err = ValidateEmail(empty)
+ err = util.ValidateEmail(empty)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), errors.New("no email provided"), err)
}
- err = ValidateEmail(notAnEmailAddress)
+ err = util.ValidateEmail(notAnEmailAddress)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), errors.New("mail: missing '@' or angle-addr"), err)
}
- err = ValidateEmail(almostAnEmailAddress)
+ err = util.ValidateEmail(almostAnEmailAddress)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), errors.New("mail: no angle-addr"), err)
}
- err = ValidateEmail(aWebsite)
+ err = util.ValidateEmail(aWebsite)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), errors.New("mail: missing '@' or angle-addr"), err)
}
- err = ValidateEmail(tooLong)
+ err = util.ValidateEmail(tooLong)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), fmt.Errorf("email address should be no more than 256 chars but '%s' was 286", tooLong), err)
}
- err = ValidateEmail(emailAddress)
+ err = util.ValidateEmail(emailAddress)
if assert.NoError(suite.T(), err) {
assert.Equal(suite.T(), nil, err)
}
@@ -187,47 +188,47 @@ func (suite *ValidationTestSuite) TestValidateLanguage() {
german := "de"
var err error
- err = ValidateLanguage(empty)
+ err = util.ValidateLanguage(empty)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), errors.New("no language provided"), err)
}
- err = ValidateLanguage(notALanguage)
+ err = util.ValidateLanguage(notALanguage)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), errors.New("language: tag is not well-formed"), err)
}
- err = ValidateLanguage(english)
+ err = util.ValidateLanguage(english)
if assert.NoError(suite.T(), err) {
assert.Equal(suite.T(), nil, err)
}
- err = ValidateLanguage(capitalEnglish)
+ err = util.ValidateLanguage(capitalEnglish)
if assert.NoError(suite.T(), err) {
assert.Equal(suite.T(), nil, err)
}
- err = ValidateLanguage(arabic3Letters)
+ err = util.ValidateLanguage(arabic3Letters)
if assert.NoError(suite.T(), err) {
assert.Equal(suite.T(), nil, err)
}
- err = ValidateLanguage(mixedCapsEnglish)
+ err = util.ValidateLanguage(mixedCapsEnglish)
if assert.NoError(suite.T(), err) {
assert.Equal(suite.T(), nil, err)
}
- err = ValidateLanguage(englishUS)
+ err = util.ValidateLanguage(englishUS)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), errors.New("language: tag is not well-formed"), err)
}
- err = ValidateLanguage(dutch)
+ err = util.ValidateLanguage(dutch)
if assert.NoError(suite.T(), err) {
assert.Equal(suite.T(), nil, err)
}
- err = ValidateLanguage(german)
+ err = util.ValidateLanguage(german)
if assert.NoError(suite.T(), err) {
assert.Equal(suite.T(), nil, err)
}
@@ -241,43 +242,43 @@ func (suite *ValidationTestSuite) TestValidateReason() {
var err error
// check with no reason required
- err = ValidateSignUpReason(empty, false)
+ err = util.ValidateSignUpReason(empty, false)
if assert.NoError(suite.T(), err) {
assert.Equal(suite.T(), nil, err)
}
- err = ValidateSignUpReason(badReason, false)
+ err = util.ValidateSignUpReason(badReason, false)
if assert.NoError(suite.T(), err) {
assert.Equal(suite.T(), nil, err)
}
- err = ValidateSignUpReason(tooLong, false)
+ err = util.ValidateSignUpReason(tooLong, false)
if assert.NoError(suite.T(), err) {
assert.Equal(suite.T(), nil, err)
}
- err = ValidateSignUpReason(goodReason, false)
+ err = util.ValidateSignUpReason(goodReason, false)
if assert.NoError(suite.T(), err) {
assert.Equal(suite.T(), nil, err)
}
// check with reason required
- err = ValidateSignUpReason(empty, true)
+ err = util.ValidateSignUpReason(empty, true)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), errors.New("no reason provided"), err)
}
- err = ValidateSignUpReason(badReason, true)
+ err = util.ValidateSignUpReason(badReason, true)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), errors.New("reason should be at least 40 chars but 'because' was 7"), err)
}
- err = ValidateSignUpReason(tooLong, true)
+ err = util.ValidateSignUpReason(tooLong, true)
if assert.Error(suite.T(), err) {
assert.Equal(suite.T(), errors.New("reason should be no more than 500 chars but given reason was 600"), err)
}
- err = ValidateSignUpReason(goodReason, true)
+ err = util.ValidateSignUpReason(goodReason, true)
if assert.NoError(suite.T(), err) {
assert.Equal(suite.T(), nil, err)
}