Welcome to mirror list, hosted at ThFree Co, Russian Federation.

matching.go « redirects « internal - gitlab.com/gitlab-org/gitlab-pages.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: b5bceb71a9bf1050aa0cce27b340143f644df9e2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
package redirects

import (
	"fmt"
	"regexp"
	"strings"

	netlifyRedirects "github.com/tj/go-redirects"
	"gitlab.com/gitlab-org/labkit/log"
)

var (
	regexMultipleSlashes     = regexp.MustCompile(`//+`)
	regexPlaceholderOrSplats = regexp.MustCompile(`(?i)\*|:[a-z]+`)
)

// matchesRule returns `true` if the rule's "from" pattern matches the requested URL.
//
// For example, given a "from" URL like this:
//
//    /a/*/url/with/:placeholders
//
// this function would match URLs like this:
//
//    /a/nice/url/with/text
//    /a/super/extra/nice/url/with/matches
//
// If the first return value is `true`, the second return value is the path that this
// rule should redirect/rewrite to. This path is effectively the rule's "to" path that
// has been templated with all the placeholders (if any) from the originally requested URL.
//
// TODO: Likely these should include host comparison once we have domain-level redirects
// https://gitlab.com/gitlab-org/gitlab-pages/-/issues/601
func matchesRule(rule *netlifyRedirects.Rule, path string) (bool, string) {
	// If the requested URL exactly matches this rule's "from" path,
	// exit early and return the rule's "to" path to avoid building
	// and compiling the regex below.
	// However, only do this if there's nothing to template in the "to" path,
	// to avoid redirect/rewriting to a url with a literal `:placeholder` in it.
	if normalizePath(rule.From) == normalizePath(path) && !regexPlaceholderOrSplats.MatchString(rule.To) {
		return true, rule.To
	}

	// Any logic beyond this point handles placeholders and splats.
	// If the FF_ENABLE_PLACEHOLDERS feature flag isn't enabled, exit now.
	if !placeholdersEnabled() {
		return false, ""
	}

	var regexSegments []string
	for _, segment := range strings.Split(rule.From, "/") {
		if segment == "" {
			continue
		} else if regexSplat.MatchString(segment) {
			regexSegments = append(regexSegments, `/*(?P<splat>.*)/*`)
		} else if regexPlaceholder.MatchString(segment) {
			segmentName := strings.Replace(segment, ":", "", 1)
			regexSegments = append(regexSegments, fmt.Sprintf(`/+(?P<%s>[^/]+)`, segmentName))
		} else {
			regexSegments = append(regexSegments, "/+"+regexp.QuoteMeta(segment))
		}
	}

	fromRegexString := `(?i)^` + strings.Join(regexSegments, "") + `/*$`
	fromRegex, err := regexp.Compile(fromRegexString)
	if err != nil {
		log.WithFields(log.Fields{
			"fromRegexString": fromRegexString,
			"rule.From":       rule.From,
			"rule.To":         rule.To,
			"rule.Status":     rule.Status,
			"path":            path,
		}).WithError(err).Warnf("matchesRule generated an invalid regex: %q", fromRegexString)

		return false, ""
	}

	template := regexPlaceholderReplacement.ReplaceAllString(rule.To, `${$placeholder}`)
	submatchIndex := fromRegex.FindStringSubmatchIndex(path)

	if submatchIndex == nil {
		return false, ""
	}

	templatedToPath := []byte{}
	templatedToPath = fromRegex.ExpandString(templatedToPath, template, path, submatchIndex)

	// Some replacements result in subsequent slashes. For example, a rule with a "to"
	// like `foo/:splat/bar` will result in a path like `foo//bar` if the splat
	// character matches nothing. To avoid this, replace all instances
	// of multiple subsequent forward slashes with a single forward slash.
	templatedToPath = regexMultipleSlashes.ReplaceAll(templatedToPath, []byte("/"))

	return true, string(templatedToPath)
}

// `match` returns:
// 1. The first valid redirect or rewrite rule that matches the requested URL
// 2. The URL to redirect/rewrite to
//
// If no rule matches, this function returns `nil` and an empty string
func (r *Redirects) match(path string) (*netlifyRedirects.Rule, string) {
	for i := range r.rules {
		if i >= maxRuleCount {
			// do not process any more rules
			return nil, ""
		}

		// assign rule to a new var to prevent the following gosec error
		// G601: Implicit memory aliasing in for loop
		rule := r.rules[i]

		if validateRule(rule) != nil {
			continue
		}

		if isMatch, path := matchesRule(&rule, path); isMatch {
			return &rule, path
		}
	}

	return nil, ""
}