internal/redirects/validations.go (98 lines of code) (raw):
package redirects
import (
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
netlifyRedirects "github.com/tj/go-redirects"
"gitlab.com/gitlab-org/gitlab-pages/internal/feature"
"gitlab.com/gitlab-org/gitlab-pages/internal/utils"
)
var (
regexPlaceholder = regexp.MustCompile(`(?i)^:[a-z]+$`)
regexSplat = regexp.MustCompile(`^\*$`)
regexPlaceholderReplacement = regexp.MustCompile(`(?i):(?P<placeholder>[a-z]+)`)
)
// validateFromURL validates the from URL in a redirect rule.
// It checks for various invalid cases like unsupported schemes,
// relative URLs, domain redirects without scheme, etc.
// Returns `nil` if the URL is valid.
// nolint: gocyclo
func validateFromURL(urlText string) error {
fromURL, err := url.Parse(urlText)
if err != nil {
return errFailedToParseURL
}
// No support for domain level redirects starting with special characters without scheme:
// - `//google.com`
// - `/\google.com`
if (fromURL.Host == "") != (fromURL.Scheme == "") || strings.HasPrefix(fromURL.Path, "/\\") {
return errNoValidStartingInURLPath
}
if fromURL.Scheme != "" && fromURL.Scheme != "http" && fromURL.Scheme != "https" {
return errNoValidStartingInURLPath
}
if fromURL.Scheme == "" && fromURL.Host == "" {
// No parent traversing relative URL's with `./` or `../`
// No ambiguous URLs like bare domains `GitLab.com`
if !strings.HasPrefix(urlText, "/") {
return errNoValidStartingInURLPath
}
}
if feature.RedirectsPlaceholders.Enabled() && strings.Count(fromURL.Path, "/*") > 1 {
return errMoreThanOneSplats
}
return validateSplatAndPlaceholders(fromURL.Path)
}
// validateURL runs validations against a rule URL.
// Returns `nil` if the URL is valid.
func validateToURL(urlText string, status int) error {
toURL, err := url.Parse(urlText)
if err != nil {
return errFailedToParseURL
}
// No support for domain level redirects starting with // or special characters:
// - `//google.com`
// - `/\google.com`
if (toURL.Host == "") != (toURL.Scheme == "") || strings.HasPrefix(toURL.Path, "/\\") {
return errNoValidStartingInURLPath
}
// No support for domain level rewrite
if utils.IsDomainURL(urlText) && status == http.StatusOK {
return errNoDomainLevelRewrite
}
allowedPrefix := []string{"/", "http://", "https://"}
// No parent traversing relative URL's with `./` or `../`
// No ambiguous URLs like bare domains `GitLab.com`
if !startsWithAnyPrefix(urlText, allowedPrefix...) {
return errNoValidStartingInURLPath
}
return validateSplatAndPlaceholders(toURL.Path)
}
func validateSplatAndPlaceholders(path string) error {
if feature.RedirectsPlaceholders.Enabled() {
maxPathSegments := cfg.MaxPathSegments
// Limit the number of path segments a rule can contain.
// This prevents the matching logic from generating regular
// expressions that are too large/complex.
if strings.Count(path, "/") > maxPathSegments {
return fmt.Errorf("url path cannot contain more than %d forward slashes", cfg.MaxPathSegments)
}
} else {
// No support for splats, https://docs.netlify.com/routing/redirects/redirect-options/#splats
if strings.Contains(path, "*") {
return errNoSplats
}
// No support for placeholders, https://docs.netlify.com/routing/redirects/redirect-options/#placeholders
if regexpPlaceholder.MatchString(path) {
return errNoPlaceholders
}
}
return nil
}
// validateRule runs all validation rules on the provided rule.
// Returns `nil` if the rule is valid
func validateRule(r netlifyRedirects.Rule) error {
if err := validateFromURL(r.From); err != nil {
return err
}
if err := validateToURL(r.To, r.Status); err != nil {
return err
}
// No support for query parameters, https://docs.netlify.com/routing/redirects/redirect-options/#query-parameters
if r.Params != nil {
return errNoParams
}
// We strictly validate return status codes
switch r.Status {
case http.StatusOK, http.StatusMovedPermanently, http.StatusFound:
// noop
default:
return errUnsupportedStatus
}
// No support for rules that use ! force
if r.Force {
return errNoForce
}
return nil
}
func startsWithAnyPrefix(s string, prefixes ...string) bool {
for _, prefix := range prefixes {
if strings.HasPrefix(s, prefix) {
return true
}
}
return false
}