1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-19 05:19:39 +02:00

fix(govalidator): replace govalidator dependency [BE-11574] (#673)

This commit is contained in:
Devon Steenberg 2025-04-23 13:59:51 +12:00 committed by GitHub
parent 3edacee59b
commit 1a3df54c04
18 changed files with 571 additions and 43 deletions

View file

@ -3,9 +3,9 @@ package update
import ( import (
"time" "time"
"github.com/asaskevich/govalidator"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
httperrors "github.com/portainer/portainer/api/http/errors" httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/pkg/validate"
) )
func ValidateAutoUpdateSettings(autoUpdate *portainer.AutoUpdateSettings) error { func ValidateAutoUpdateSettings(autoUpdate *portainer.AutoUpdateSettings) error {
@ -17,7 +17,7 @@ func ValidateAutoUpdateSettings(autoUpdate *portainer.AutoUpdateSettings) error
return httperrors.NewInvalidPayloadError("Webhook or Interval must be provided") return httperrors.NewInvalidPayloadError("Webhook or Interval must be provided")
} }
if autoUpdate.Webhook != "" && !govalidator.IsUUID(autoUpdate.Webhook) { if autoUpdate.Webhook != "" && !validate.IsUUID(autoUpdate.Webhook) {
return httperrors.NewInvalidPayloadError("invalid Webhook format") return httperrors.NewInvalidPayloadError("invalid Webhook format")
} }

View file

@ -1,19 +1,17 @@
package git package git
import ( import (
"github.com/asaskevich/govalidator"
gittypes "github.com/portainer/portainer/api/git/types" gittypes "github.com/portainer/portainer/api/git/types"
httperrors "github.com/portainer/portainer/api/http/errors" httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/pkg/validate"
) )
func ValidateRepoConfig(repoConfig *gittypes.RepoConfig) error { func ValidateRepoConfig(repoConfig *gittypes.RepoConfig) error {
if len(repoConfig.URL) == 0 || !govalidator.IsURL(repoConfig.URL) { if len(repoConfig.URL) == 0 || !validate.IsURL(repoConfig.URL) {
return httperrors.NewInvalidPayloadError("Invalid repository URL. Must correspond to a valid URL format") return httperrors.NewInvalidPayloadError("Invalid repository URL. Must correspond to a valid URL format")
} }
return ValidateRepoAuthentication(repoConfig.Authentication) return ValidateRepoAuthentication(repoConfig.Authentication)
} }
func ValidateRepoAuthentication(auth *gittypes.GitAuthentication) error { func ValidateRepoAuthentication(auth *gittypes.GitAuthentication) error {

View file

@ -16,8 +16,8 @@ import (
httperror "github.com/portainer/portainer/pkg/libhttp/error" httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request" "github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response" "github.com/portainer/portainer/pkg/libhttp/response"
"github.com/portainer/portainer/pkg/validate"
"github.com/asaskevich/govalidator"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/segmentio/encoding/json" "github.com/segmentio/encoding/json"
) )
@ -228,7 +228,7 @@ func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request)
if len(payload.Description) == 0 { if len(payload.Description) == 0 {
return errors.New("Invalid custom template description") return errors.New("Invalid custom template description")
} }
if len(payload.RepositoryURL) == 0 || !govalidator.IsURL(payload.RepositoryURL) { if len(payload.RepositoryURL) == 0 || !validate.IsURL(payload.RepositoryURL) {
return errors.New("Invalid repository URL. Must correspond to a valid URL format") return errors.New("Invalid repository URL. Must correspond to a valid URL format")
} }
if payload.RepositoryAuthentication && (len(payload.RepositoryUsername) == 0 || len(payload.RepositoryPassword) == 0) { if payload.RepositoryAuthentication && (len(payload.RepositoryUsername) == 0 || len(payload.RepositoryPassword) == 0) {

View file

@ -15,8 +15,7 @@ import (
httperror "github.com/portainer/portainer/pkg/libhttp/error" httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request" "github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response" "github.com/portainer/portainer/pkg/libhttp/response"
"github.com/portainer/portainer/pkg/validate"
"github.com/asaskevich/govalidator"
) )
type customTemplateUpdatePayload struct { type customTemplateUpdatePayload struct {
@ -170,7 +169,7 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
customTemplate.EdgeTemplate = payload.EdgeTemplate customTemplate.EdgeTemplate = payload.EdgeTemplate
if payload.RepositoryURL != "" { if payload.RepositoryURL != "" {
if !govalidator.IsURL(payload.RepositoryURL) { if !validate.IsURL(payload.RepositoryURL) {
return httperror.BadRequest("Invalid repository URL. Must correspond to a valid URL format", err) return httperror.BadRequest("Invalid repository URL. Must correspond to a valid URL format", err)
} }

View file

@ -15,8 +15,7 @@ import (
"github.com/portainer/portainer/api/internal/endpointutils" "github.com/portainer/portainer/api/internal/endpointutils"
httperror "github.com/portainer/portainer/pkg/libhttp/error" httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request" "github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/validate"
"github.com/asaskevich/govalidator"
) )
type edgeJobBasePayload struct { type edgeJobBasePayload struct {
@ -53,7 +52,7 @@ func (payload *edgeJobCreateFromFileContentPayload) Validate(r *http.Request) er
return errors.New("invalid Edge job name") return errors.New("invalid Edge job name")
} }
if !govalidator.Matches(payload.Name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]*$`) { if !validate.Matches(payload.Name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]*$`) {
return errors.New("invalid Edge job name format. Allowed characters are: [a-zA-Z0-9_.-]") return errors.New("invalid Edge job name format. Allowed characters are: [a-zA-Z0-9_.-]")
} }
@ -136,7 +135,7 @@ func (payload *edgeJobCreateFromFilePayload) Validate(r *http.Request) error {
return errors.New("invalid Edge job name") return errors.New("invalid Edge job name")
} }
if !govalidator.Matches(name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`) { if !validate.Matches(name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`) {
return errors.New("invalid Edge job name format. Allowed characters are: [a-zA-Z0-9_.-]") return errors.New("invalid Edge job name format. Allowed characters are: [a-zA-Z0-9_.-]")
} }
payload.Name = name payload.Name = name

View file

@ -14,8 +14,7 @@ import (
"github.com/portainer/portainer/api/internal/endpointutils" "github.com/portainer/portainer/api/internal/endpointutils"
httperror "github.com/portainer/portainer/pkg/libhttp/error" httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request" "github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/validate"
"github.com/asaskevich/govalidator"
) )
type edgeJobUpdatePayload struct { type edgeJobUpdatePayload struct {
@ -28,7 +27,7 @@ type edgeJobUpdatePayload struct {
} }
func (payload *edgeJobUpdatePayload) Validate(r *http.Request) error { func (payload *edgeJobUpdatePayload) Validate(r *http.Request) error {
if payload.Name != nil && !govalidator.Matches(*payload.Name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`) { if payload.Name != nil && !validate.Matches(*payload.Name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`) {
return errors.New("invalid Edge job name format. Allowed characters are: [a-zA-Z0-9_.-]") return errors.New("invalid Edge job name format. Allowed characters are: [a-zA-Z0-9_.-]")
} }

View file

@ -11,8 +11,8 @@ import (
httperrors "github.com/portainer/portainer/api/http/errors" httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/pkg/edge" "github.com/portainer/portainer/pkg/edge"
"github.com/portainer/portainer/pkg/libhttp/request" "github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/validate"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -59,7 +59,7 @@ func (payload *edgeStackFromGitRepositoryPayload) Validate(r *http.Request) erro
return httperrors.NewInvalidPayloadError("Invalid stack name. Stack name must only consist of lowercase alpha characters, numbers, hyphens, or underscores as well as start with a lowercase character or number") return httperrors.NewInvalidPayloadError("Invalid stack name. Stack name must only consist of lowercase alpha characters, numbers, hyphens, or underscores as well as start with a lowercase character or number")
} }
if len(payload.RepositoryURL) == 0 || !govalidator.IsURL(payload.RepositoryURL) { if len(payload.RepositoryURL) == 0 || !validate.IsURL(payload.RepositoryURL) {
return httperrors.NewInvalidPayloadError("Invalid repository URL. Must correspond to a valid URL format") return httperrors.NewInvalidPayloadError("Invalid repository URL. Must correspond to a valid URL format")
} }

View file

@ -9,8 +9,7 @@ import (
httperror "github.com/portainer/portainer/pkg/libhttp/error" httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request" "github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response" "github.com/portainer/portainer/pkg/libhttp/response"
"github.com/portainer/portainer/pkg/validate"
"github.com/asaskevich/govalidator"
) )
type fileResponse struct { type fileResponse struct {
@ -29,7 +28,7 @@ type repositoryFilePreviewPayload struct {
} }
func (payload *repositoryFilePreviewPayload) Validate(r *http.Request) error { func (payload *repositoryFilePreviewPayload) Validate(r *http.Request) error {
if len(payload.Repository) == 0 || !govalidator.IsURL(payload.Repository) { if len(payload.Repository) == 0 || !validate.IsURL(payload.Repository) {
return errors.New("invalid repository URL. Must correspond to a valid URL format") return errors.New("invalid repository URL. Must correspond to a valid URL format")
} }

View file

@ -14,8 +14,8 @@ import (
httperror "github.com/portainer/portainer/pkg/libhttp/error" httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request" "github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response" "github.com/portainer/portainer/pkg/libhttp/response"
"github.com/portainer/portainer/pkg/validate"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors" "github.com/pkg/errors"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
@ -62,15 +62,15 @@ func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
return errors.New("Invalid authentication method value. Value must be one of: 1 (internal), 2 (LDAP/AD) or 3 (OAuth)") return errors.New("Invalid authentication method value. Value must be one of: 1 (internal), 2 (LDAP/AD) or 3 (OAuth)")
} }
if payload.LogoURL != nil && *payload.LogoURL != "" && !govalidator.IsURL(*payload.LogoURL) { if payload.LogoURL != nil && *payload.LogoURL != "" && !validate.IsURL(*payload.LogoURL) {
return errors.New("Invalid logo URL. Must correspond to a valid URL format") return errors.New("Invalid logo URL. Must correspond to a valid URL format")
} }
if payload.TemplatesURL != nil && *payload.TemplatesURL != "" && !govalidator.IsURL(*payload.TemplatesURL) { if payload.TemplatesURL != nil && *payload.TemplatesURL != "" && !validate.IsURL(*payload.TemplatesURL) {
return errors.New("Invalid external templates URL. Must correspond to a valid URL format") return errors.New("Invalid external templates URL. Must correspond to a valid URL format")
} }
if payload.HelmRepositoryURL != nil && *payload.HelmRepositoryURL != "" && !govalidator.IsURL(*payload.HelmRepositoryURL) { if payload.HelmRepositoryURL != nil && *payload.HelmRepositoryURL != "" && !validate.IsURL(*payload.HelmRepositoryURL) {
return errors.New("Invalid Helm repository URL. Must correspond to a valid URL format") return errors.New("Invalid Helm repository URL. Must correspond to a valid URL format")
} }

View file

@ -14,8 +14,8 @@ import (
"github.com/portainer/portainer/api/stacks/stackutils" "github.com/portainer/portainer/api/stacks/stackutils"
httperror "github.com/portainer/portainer/pkg/libhttp/error" httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request" "github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/validate"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -205,7 +205,7 @@ func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) e
if len(payload.Name) == 0 { if len(payload.Name) == 0 {
return errors.New("Invalid stack name") return errors.New("Invalid stack name")
} }
if len(payload.RepositoryURL) == 0 || !govalidator.IsURL(payload.RepositoryURL) { if len(payload.RepositoryURL) == 0 || !validate.IsURL(payload.RepositoryURL) {
return errors.New("Invalid repository URL. Must correspond to a valid URL format") return errors.New("Invalid repository URL. Must correspond to a valid URL format")
} }
if payload.RepositoryAuthentication && len(payload.RepositoryPassword) == 0 { if payload.RepositoryAuthentication && len(payload.RepositoryPassword) == 0 {

View file

@ -15,8 +15,8 @@ import (
httperror "github.com/portainer/portainer/pkg/libhttp/error" httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request" "github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response" "github.com/portainer/portainer/pkg/libhttp/response"
"github.com/portainer/portainer/pkg/validate"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -96,7 +96,7 @@ func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) erro
} }
func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error { func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error {
if len(payload.RepositoryURL) == 0 || !govalidator.IsURL(payload.RepositoryURL) { if len(payload.RepositoryURL) == 0 || !validate.IsURL(payload.RepositoryURL) {
return errors.New("Invalid repository URL. Must correspond to a valid URL format") return errors.New("Invalid repository URL. Must correspond to a valid URL format")
} }
@ -112,7 +112,7 @@ func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error {
} }
func (payload *kubernetesManifestURLDeploymentPayload) Validate(r *http.Request) error { func (payload *kubernetesManifestURLDeploymentPayload) Validate(r *http.Request) error {
if len(payload.ManifestURL) == 0 || !govalidator.IsURL(payload.ManifestURL) { if len(payload.ManifestURL) == 0 || !validate.IsURL(payload.ManifestURL) {
return errors.New("Invalid manifest URL") return errors.New("Invalid manifest URL")
} }

View file

@ -11,8 +11,8 @@ import (
"github.com/portainer/portainer/api/stacks/stackutils" "github.com/portainer/portainer/api/stacks/stackutils"
httperror "github.com/portainer/portainer/pkg/libhttp/error" httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request" "github.com/portainer/portainer/pkg/libhttp/request"
valid "github.com/portainer/portainer/pkg/validate"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -142,7 +142,7 @@ func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) err
if len(payload.SwarmID) == 0 { if len(payload.SwarmID) == 0 {
return errors.New("Invalid Swarm ID") return errors.New("Invalid Swarm ID")
} }
if len(payload.RepositoryURL) == 0 || !govalidator.IsURL(payload.RepositoryURL) { if len(payload.RepositoryURL) == 0 || !valid.IsURL(payload.RepositoryURL) {
return errors.New("Invalid repository URL. Must correspond to a valid URL format") return errors.New("Invalid repository URL. Must correspond to a valid URL format")
} }
if payload.RepositoryAuthentication && len(payload.RepositoryPassword) == 0 { if payload.RepositoryAuthentication && len(payload.RepositoryPassword) == 0 {

View file

@ -11,8 +11,7 @@ import (
httperror "github.com/portainer/portainer/pkg/libhttp/error" httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request" "github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response" "github.com/portainer/portainer/pkg/libhttp/response"
"github.com/portainer/portainer/pkg/validate"
"github.com/asaskevich/govalidator"
) )
type userAccessTokenCreatePayload struct { type userAccessTokenCreatePayload struct {
@ -24,10 +23,10 @@ func (payload *userAccessTokenCreatePayload) Validate(r *http.Request) error {
if len(payload.Description) == 0 { if len(payload.Description) == 0 {
return errors.New("invalid description: cannot be empty") return errors.New("invalid description: cannot be empty")
} }
if govalidator.HasWhitespaceOnly(payload.Description) { if validate.HasWhitespaceOnly(payload.Description) {
return errors.New("invalid description: cannot contain only whitespaces") return errors.New("invalid description: cannot contain only whitespaces")
} }
if govalidator.MinStringLength(payload.Description, "128") { if validate.MinStringLength(payload.Description, 128) {
return errors.New("invalid description: cannot be longer than 128 characters") return errors.New("invalid description: cannot be longer than 128 characters")
} }
return nil return nil

View file

@ -9,8 +9,8 @@ import (
"github.com/portainer/portainer/api/ws" "github.com/portainer/portainer/api/ws"
httperror "github.com/portainer/portainer/pkg/libhttp/error" httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request" "github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/validate"
"github.com/asaskevich/govalidator"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )
@ -38,7 +38,7 @@ func (handler *Handler) websocketAttach(w http.ResponseWriter, r *http.Request)
if err != nil { if err != nil {
return httperror.BadRequest("Invalid query parameter: id", err) return httperror.BadRequest("Invalid query parameter: id", err)
} }
if !govalidator.IsHexadecimal(attachID) { if !validate.IsHexadecimal(attachID) {
return httperror.BadRequest("Invalid query parameter: id (must be hexadecimal identifier)", err) return httperror.BadRequest("Invalid query parameter: id (must be hexadecimal identifier)", err)
} }

View file

@ -8,8 +8,8 @@ import (
"github.com/portainer/portainer/api/ws" "github.com/portainer/portainer/api/ws"
httperror "github.com/portainer/portainer/pkg/libhttp/error" httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request" "github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/validate"
"github.com/asaskevich/govalidator"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/segmentio/encoding/json" "github.com/segmentio/encoding/json"
) )
@ -42,7 +42,7 @@ func (handler *Handler) websocketExec(w http.ResponseWriter, r *http.Request) *h
if err != nil { if err != nil {
return httperror.BadRequest("Invalid query parameter: id", err) return httperror.BadRequest("Invalid query parameter: id", err)
} }
if !govalidator.IsHexadecimal(execID) { if !validate.IsHexadecimal(execID) {
return httperror.BadRequest("Invalid query parameter: id (must be hexadecimal identifier)", err) return httperror.BadRequest("Invalid query parameter: id (must be hexadecimal identifier)", err)
} }

2
go.mod
View file

@ -6,7 +6,6 @@ require (
github.com/Masterminds/semver v1.5.0 github.com/Masterminds/semver v1.5.0
github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/go-winio v0.6.2
github.com/VictoriaMetrics/fastcache v1.12.0 github.com/VictoriaMetrics/fastcache v1.12.0
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/aws/aws-sdk-go-v2 v1.24.1 github.com/aws/aws-sdk-go-v2 v1.24.1
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 github.com/aws/aws-sdk-go-v2/credentials v1.16.16
github.com/aws/aws-sdk-go-v2/service/ecr v1.24.1 github.com/aws/aws-sdk-go-v2/service/ecr v1.24.1
@ -85,6 +84,7 @@ require (
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 // indirect github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 // indirect
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 // indirect github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2/config v1.26.6 // indirect github.com/aws/aws-sdk-go-v2/config v1.26.6 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect

111
pkg/validate/validate.go Normal file
View file

@ -0,0 +1,111 @@
package validate
import (
"net"
"net/url"
"regexp"
"strings"
"unicode/utf8"
)
const (
minURLRuneCount = 3
maxURLRuneCount = 2083
ipPattern = `(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))`
urlSchemaPattern = `((ftp|tcp|udp|wss?|https?):\/\/)`
urlUsernamePattern = `(\S+(:\S*)?@)`
urlPathPattern = `((\/|\?|#)[^\s]*)`
urlPortPattern = `(:(\d{1,5}))`
urlIPPattern = `([1-9]\d?|1\d\d|2[01]\d|22[0-3]|24\d|25[0-5])(\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])){2}(?:\.([0-9]\d?|1\d\d|2[0-4]\d|25[0-5]))`
urlSubdomainPattern = `((www\.)|([a-zA-Z0-9]+([-_\.]?[a-zA-Z0-9])*[a-zA-Z0-9]\.[a-zA-Z0-9]+))`
urlPattern = `^` + urlSchemaPattern + `?` + urlUsernamePattern + `?` + `((` + urlIPPattern + `|(\[` + ipPattern + `\])|(([a-zA-Z0-9]([a-zA-Z0-9-_]+)?[a-zA-Z0-9]([-\.][a-zA-Z0-9]+)*)|(` + urlSubdomainPattern + `?))?(([a-zA-Z\x{00a1}-\x{ffff}0-9]+-?-?)*[a-zA-Z\x{00a1}-\x{ffff}0-9]+)(?:\.([a-zA-Z\x{00a1}-\x{ffff}]{1,}))?))\.?` + urlPortPattern + `?` + urlPathPattern + `?$`
)
var (
urlRegex = regexp.MustCompile(urlPattern)
uuidRegex = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
hexadecimalRegex = regexp.MustCompile(`^[0-9a-fA-F]+$`)
whitespaceRegex = regexp.MustCompile(`^[[:space:]]+$`)
dnsNameRegex = regexp.MustCompile(`^([a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62}){1}(\.[a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62})*[\._]?$`)
)
func IsURL(urlString string) bool {
if urlString == "" ||
utf8.RuneCountInString(urlString) >= maxURLRuneCount ||
len(urlString) <= minURLRuneCount ||
strings.HasPrefix(urlString, ".") {
return false
}
strTemp := urlString
if strings.Contains(urlString, ":") && !strings.Contains(urlString, "://") {
// support no indicated urlscheme but with colon for port number
// http:// is appended so url.Parse will succeed, strTemp used so it does not impact rxURL.MatchString
strTemp = "http://" + urlString
}
u, err := url.Parse(strTemp)
if err != nil {
return false
}
if strings.HasPrefix(u.Host, ".") {
return false
}
if u.Host == "" && (u.Path != "" && !strings.Contains(u.Path, ".")) {
return false
}
return urlRegex.MatchString(urlString)
}
func IsUUID(uuidString string) bool {
return uuidRegex.MatchString(uuidString)
}
func IsHexadecimal(hexString string) bool {
return hexadecimalRegex.MatchString(hexString)
}
func HasWhitespaceOnly(s string) bool {
return len(s) > 0 && whitespaceRegex.MatchString(s)
}
func MinStringLength(s string, len int) bool {
return utf8.RuneCountInString(s) >= len
}
func Matches(s, pattern string) bool {
match, err := regexp.MatchString(pattern, s)
return err == nil && match
}
func IsNonPositive(f float64) bool {
return f <= 0
}
func InRange(val, left, right float64) bool {
if left > right {
left, right = right, left
}
return val >= left && val <= right
}
func IsHost(s string) bool {
return IsIP(s) || IsDNSName(s)
}
func IsIP(s string) bool {
return net.ParseIP(s) != nil
}
func IsDNSName(s string) bool {
if s == "" || len(strings.ReplaceAll(s, ".", "")) > 255 {
// constraints already violated
return false
}
return !IsIP(s) && dnsNameRegex.MatchString(s)
}

View file

@ -0,0 +1,424 @@
package validate
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test_IsURL(t *testing.T) {
testCases := []struct {
name string
url string
expectedResult bool
}{
{
name: "simple url",
url: "https://google.com",
expectedResult: true,
},
{
name: "no schema",
url: "google.com",
expectedResult: true,
},
{
name: "path",
url: "https://google.com/some/thing",
expectedResult: true,
},
{
name: "query params",
url: "https://google.com/some/thing?a=5&b=6",
expectedResult: true,
},
{
name: "invalid",
url: "google",
expectedResult: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := IsURL(tc.url)
require.Equal(t, tc.expectedResult, result)
})
}
}
func Test_IsUUID(t *testing.T) {
testCases := []struct {
name string
uuid string
expectedResult bool
}{
{
name: "empty",
uuid: "",
expectedResult: false,
},
{
name: "version 3 UUID",
uuid: "060507eb-3b9a-362e-b850-d5f065eea403",
expectedResult: true,
},
{
name: "version 4 UUID",
uuid: "63e695ee-48a9-498a-98b3-9472ff75e09f",
expectedResult: true,
},
{
name: "version 5 UUID",
uuid: "5daabcd8-f17e-568c-aa6f-da9d92c7032c",
expectedResult: true,
},
{
name: "text",
uuid: "something like this",
expectedResult: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := IsUUID(tc.uuid)
require.Equal(t, tc.expectedResult, result)
})
}
}
func Test_IsHexadecimal(t *testing.T) {
testCases := []struct {
name string
hex string
expectedResult bool
}{
{
name: "empty",
hex: "",
expectedResult: false,
},
{
name: "hex",
hex: "48656C6C6F20736F6D657468696E67",
expectedResult: true,
},
{
name: "text",
hex: "something like this",
expectedResult: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := IsHexadecimal(tc.hex)
require.Equal(t, tc.expectedResult, result)
})
}
}
func Test_HasWhitespaceOnly(t *testing.T) {
testCases := []struct {
name string
s string
expectedResult bool
}{
{
name: "empty",
s: "",
expectedResult: false,
},
{
name: "space",
s: " ",
expectedResult: true,
},
{
name: "tab",
s: "\t",
expectedResult: true,
},
{
name: "text",
s: "something like this",
expectedResult: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := HasWhitespaceOnly(tc.s)
require.Equal(t, tc.expectedResult, result)
})
}
}
func Test_MinStringLength(t *testing.T) {
testCases := []struct {
name string
s string
len int
expectedResult bool
}{
{
name: "empty + zero len",
s: "",
len: 0,
expectedResult: true,
},
{
name: "empty + non zero len",
s: "",
len: 10,
expectedResult: false,
},
{
name: "long text + non zero len",
s: "something else",
len: 10,
expectedResult: true,
},
{
name: "multibyte characters - enough",
s: "X生",
len: 2,
expectedResult: true,
},
{
name: "multibyte characters - not enough",
s: "X生",
len: 3,
expectedResult: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := MinStringLength(tc.s, tc.len)
require.Equal(t, tc.expectedResult, result)
})
}
}
func Test_Matches(t *testing.T) {
testCases := []struct {
name string
s string
pattern string
expectedResult bool
}{
{
name: "empty",
s: "",
pattern: "",
expectedResult: true,
},
{
name: "space",
s: "something else",
pattern: " ",
expectedResult: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := Matches(tc.s, tc.pattern)
require.Equal(t, tc.expectedResult, result)
})
}
}
func Test_IsNonPositive(t *testing.T) {
testCases := []struct {
name string
f float64
expectedResult bool
}{
{
name: "zero",
f: 0,
expectedResult: true,
},
{
name: "positive",
f: 1,
expectedResult: false,
},
{
name: "negative",
f: -1,
expectedResult: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := IsNonPositive(tc.f)
require.Equal(t, tc.expectedResult, result)
})
}
}
func Test_InRange(t *testing.T) {
testCases := []struct {
name string
f float64
left float64
right float64
expectedResult bool
}{
{
name: "zero",
f: 0,
left: 0,
right: 0,
expectedResult: true,
},
{
name: "equal left",
f: 1,
left: 1,
right: 2,
expectedResult: true,
},
{
name: "equal right",
f: 2,
left: 1,
right: 2,
expectedResult: true,
},
{
name: "above",
f: 3,
left: 1,
right: 2,
expectedResult: false,
},
{
name: "below",
f: 0,
left: 1,
right: 2,
expectedResult: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := InRange(tc.f, tc.left, tc.right)
require.Equal(t, tc.expectedResult, result)
})
}
}
func Test_IsHost(t *testing.T) {
testCases := []struct {
name string
s string
expectedResult bool
}{
{
name: "empty",
s: "",
expectedResult: false,
},
{
name: "ip address",
s: "192.168.1.1",
expectedResult: true,
},
{
name: "hostname",
s: "google.com",
expectedResult: true,
},
{
name: "text",
s: "Something like this",
expectedResult: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := IsHost(tc.s)
require.Equal(t, tc.expectedResult, result)
})
}
}
func Test_IsIP(t *testing.T) {
testCases := []struct {
name string
s string
expectedResult bool
}{
{
name: "empty",
s: "",
expectedResult: false,
},
{
name: "ip address",
s: "192.168.1.1",
expectedResult: true,
},
{
name: "hostname",
s: "google.com",
expectedResult: false,
},
{
name: "text",
s: "Something like this",
expectedResult: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := IsIP(tc.s)
require.Equal(t, tc.expectedResult, result)
})
}
}
func Test_IsDNSName(t *testing.T) {
testCases := []struct {
name string
s string
expectedResult bool
}{
{
name: "empty",
s: "",
expectedResult: false,
},
{
name: "ip address",
s: "192.168.1.1",
expectedResult: false,
},
{
name: "hostname",
s: "google.com",
expectedResult: true,
},
{
name: "text",
s: "Something like this",
expectedResult: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := IsDNSName(tc.s)
require.Equal(t, tc.expectedResult, result)
})
}
}