Skip to content

Commit e9f9058

Browse files
committed
Ensure Prefix matching requires trailing slash
Problem: When using a prefix path match, a path such as /foobar would match for the prefix /foo, which should not happen. Prefixes need a "/" delimiter between path segments. Solution: In order to allow just the prefix without a trailing slash, as well as more path segments using a slash delimiter, we now create two location blocks. One uses exact matching for the configured prefix path, and the other uses prefix matching with a trailing slash.
1 parent e065469 commit e9f9058

File tree

4 files changed

+645
-78
lines changed

4 files changed

+645
-78
lines changed

internal/mode/static/nginx/config/http/config.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ type Location struct {
1818
HTTPMatchVar string
1919
ProxySetHeaders []Header
2020
Internal bool
21-
Exact bool
2221
}
2322

2423
// Header defines a HTTP header to be passed to the proxied server.

internal/mode/static/nginx/config/servers.go

Lines changed: 141 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -75,24 +75,9 @@ func createServer(virtualServer dataplane.VirtualServer) http.Server {
7575
}
7676

7777
func createLocations(pathRules []dataplane.PathRule, listenerPort int32) []http.Location {
78-
lenPathRules := len(pathRules)
79-
80-
if lenPathRules == 0 {
81-
return []http.Location{createDefaultRootLocation()}
82-
}
83-
84-
// To calculate the maximum number of locations, we need to take into account the following:
85-
// 1. Each match rule for a path rule will have one location.
86-
// 2. Each path rule may have an additional location if it contains non-path-only matches.
87-
// 3. There may be an additional location for the default root path.
88-
maxLocs := 1
89-
for _, rules := range pathRules {
90-
maxLocs += len(rules.MatchRules) + 1
91-
}
92-
78+
maxLocs, pathsAndTypes := getMaxLocationCountAndPathMap(pathRules)
9379
locs := make([]http.Location, 0, maxLocs)
94-
95-
rootPathExists := false
80+
var rootPathExists bool
9681

9782
for _, rule := range pathRules {
9883
matches := make([]httpMatch, 0, len(rule.MatchRules))
@@ -101,27 +86,23 @@ func createLocations(pathRules []dataplane.PathRule, listenerPort int32) []http.
10186
rootPathExists = true
10287
}
10388

89+
extLocations := initializeExternalLocations(rule, pathsAndTypes)
90+
10491
for matchRuleIdx, r := range rule.MatchRules {
10592
m := r.GetMatch()
10693

107-
var loc http.Location
108-
109-
// handle case where the only route is a path-only match
110-
// generate a standard location block without http_matches.
111-
if len(rule.MatchRules) == 1 && isPathOnlyMatch(m) {
112-
loc = http.Location{
113-
Path: rule.Path,
114-
Exact: rule.PathType == dataplane.PathTypeExact,
115-
}
116-
} else {
117-
path := createPathForMatch(rule.Path, rule.PathType, matchRuleIdx)
118-
loc = createMatchLocation(path)
119-
matches = append(matches, createHTTPMatch(m, path))
94+
buildLocations := extLocations
95+
intLocation, match, intLocationExists := initializeInternalLocation(rule, matchRuleIdx, m)
96+
if intLocationExists {
97+
buildLocations = []http.Location{intLocation}
98+
matches = append(matches, match)
12099
}
121100

122101
if r.Filters.InvalidFilter != nil {
123-
loc.Return = &http.Return{Code: http.StatusInternalServerError}
124-
locs = append(locs, loc)
102+
for i := range buildLocations {
103+
buildLocations[i].Return = &http.Return{Code: http.StatusInternalServerError}
104+
}
105+
locs = append(locs, buildLocations...)
125106
continue
126107
}
127108

@@ -136,21 +117,24 @@ func createLocations(pathRules []dataplane.PathRule, listenerPort int32) []http.
136117

137118
// RequestRedirect and proxying are mutually exclusive.
138119
if r.Filters.RequestRedirect != nil {
139-
loc.Return = createReturnValForRedirectFilter(r.Filters.RequestRedirect, listenerPort)
140-
locs = append(locs, loc)
120+
ret := createReturnValForRedirectFilter(r.Filters.RequestRedirect, listenerPort)
121+
for i := range buildLocations {
122+
buildLocations[i].Return = ret
123+
}
124+
locs = append(locs, buildLocations...)
141125
continue
142126
}
143127

144-
backendName := backendGroupName(r.BackendGroup)
145-
loc.ProxySetHeaders = generateProxySetHeaders(r.Filters.RequestHeaderModifiers)
146-
147-
if backendGroupNeedsSplit(r.BackendGroup) {
148-
loc.ProxyPass = createProxyPassForVar(backendName)
149-
} else {
150-
loc.ProxyPass = createProxyPass(backendName)
128+
proxySetHeaders := generateProxySetHeaders(r.Filters.RequestHeaderModifiers)
129+
for i := range buildLocations {
130+
buildLocations[i].ProxySetHeaders = proxySetHeaders
151131
}
152132

153-
locs = append(locs, loc)
133+
proxyPass := createProxyPass(r.BackendGroup)
134+
for i := range buildLocations {
135+
buildLocations[i].ProxyPass = proxyPass
136+
}
137+
locs = append(locs, buildLocations...)
154138
}
155139

156140
if len(matches) > 0 {
@@ -163,13 +147,10 @@ func createLocations(pathRules []dataplane.PathRule, listenerPort int32) []http.
163147
panic(fmt.Errorf("could not marshal http match: %w", err))
164148
}
165149

166-
pathLoc := http.Location{
167-
Path: rule.Path,
168-
Exact: rule.PathType == dataplane.PathTypeExact,
169-
HTTPMatchVar: string(b),
150+
for i := range extLocations {
151+
extLocations[i].HTTPMatchVar = string(b)
170152
}
171-
172-
locs = append(locs, pathLoc)
153+
locs = append(locs, extLocations...)
173154
}
174155
}
175156

@@ -180,6 +161,93 @@ func createLocations(pathRules []dataplane.PathRule, listenerPort int32) []http.
180161
return locs
181162
}
182163

164+
// pathAndTypeMap contains a map of paths and any path types defined for that path
165+
// for example, {/foo: {exact: {}, prefix: {}}}
166+
type pathAndTypeMap map[string]map[dataplane.PathType]struct{}
167+
168+
// To calculate the maximum number of locations, we need to take into account the following:
169+
// 1. Each match rule for a path rule will have one location.
170+
// 2. Each path rule may have an additional location if it contains non-path-only matches.
171+
// 3. Each prefix path rule may have an additional location if it doesn't contain trailing slash.
172+
// 4. There may be an additional location for the default root path.
173+
// We also return a map of all paths and their types.
174+
func getMaxLocationCountAndPathMap(pathRules []dataplane.PathRule) (int, pathAndTypeMap) {
175+
maxLocs := 1
176+
pathsAndTypes := make(pathAndTypeMap)
177+
for _, rule := range pathRules {
178+
maxLocs += len(rule.MatchRules) + 2
179+
if pathsAndTypes[rule.Path] == nil {
180+
pathsAndTypes[rule.Path] = map[dataplane.PathType]struct{}{
181+
rule.PathType: {},
182+
}
183+
} else {
184+
pathsAndTypes[rule.Path][rule.PathType] = struct{}{}
185+
}
186+
}
187+
188+
return maxLocs, pathsAndTypes
189+
}
190+
191+
func initializeExternalLocations(
192+
rule dataplane.PathRule,
193+
pathsAndTypes pathAndTypeMap,
194+
) []http.Location {
195+
extLocations := make([]http.Location, 0, 2)
196+
externalLocPath := createPath(rule)
197+
// If the path type is Prefix and doesn't contain a trailing slash, then we need a second location
198+
// that handles the Exact prefix case (if it doesn't already exist), and the first location is updated
199+
// to handle the trailing slash prefix case (if it doesn't already exist)
200+
if isNonSlashedPrefixPath(rule.PathType, externalLocPath) {
201+
// if Exact path and trailing slash Prefix path already exist, then we don't need to build anything
202+
_, exactPathExists := pathsAndTypes[rule.Path][dataplane.PathTypeExact]
203+
var trailingSlashPrefixPathExists bool
204+
if pathTypes, exists := pathsAndTypes[rule.Path+"/"]; exists {
205+
_, trailingSlashPrefixPathExists = pathTypes[dataplane.PathTypePrefix]
206+
}
207+
208+
if exactPathExists && trailingSlashPrefixPathExists {
209+
return []http.Location{}
210+
}
211+
212+
if !trailingSlashPrefixPathExists {
213+
externalLocTrailing := http.Location{
214+
Path: externalLocPath + "/",
215+
}
216+
extLocations = append(extLocations, externalLocTrailing)
217+
}
218+
if !exactPathExists {
219+
externalLocExact := http.Location{
220+
Path: exactPath(externalLocPath),
221+
}
222+
extLocations = append(extLocations, externalLocExact)
223+
}
224+
} else {
225+
externalLoc := http.Location{
226+
Path: externalLocPath,
227+
}
228+
extLocations = []http.Location{externalLoc}
229+
}
230+
231+
return extLocations
232+
}
233+
234+
func initializeInternalLocation(
235+
rule dataplane.PathRule,
236+
matchRuleIdx int,
237+
match v1beta1.HTTPRouteMatch,
238+
) (http.Location, httpMatch, bool) {
239+
var intLocation http.Location
240+
var hm httpMatch
241+
intLocationNeeded := len(rule.MatchRules) != 1 || !isPathOnlyMatch(match)
242+
if intLocationNeeded {
243+
path := createPathForMatch(rule.Path, rule.PathType, matchRuleIdx)
244+
hm = createHTTPMatch(match, path)
245+
intLocation = createMatchLocation(path)
246+
}
247+
248+
return intLocation, hm, intLocationNeeded
249+
}
250+
183251
func createReturnValForRedirectFilter(filter *v1beta1.HTTPRequestRedirectFilter, listenerPort int32) *http.Return {
184252
if filter == nil {
185253
return nil
@@ -308,12 +376,13 @@ func isPathOnlyMatch(match v1beta1.HTTPRouteMatch) bool {
308376
return match.Method == nil && match.Headers == nil && match.QueryParams == nil
309377
}
310378

311-
func createProxyPass(address string) string {
312-
return "http://" + address
313-
}
379+
func createProxyPass(backendGroup dataplane.BackendGroup) string {
380+
backendName := backendGroupName(backendGroup)
381+
if backendGroupNeedsSplit(backendGroup) {
382+
return "http://$" + convertStringToSafeVariableName(backendName)
383+
}
314384

315-
func createProxyPassForVar(variable string) string {
316-
return "http://$" + convertStringToSafeVariableName(variable)
385+
return "http://" + backendName
317386
}
318387

319388
func createMatchLocation(path string) http.Location {
@@ -369,6 +438,20 @@ func convertSetHeaders(headers []dataplane.HTTPHeader) []http.Header {
369438
return locHeaders
370439
}
371440

441+
func exactPath(path string) string {
442+
return fmt.Sprintf("= %s", path)
443+
}
444+
445+
// createPath builds the location path depending on the path type.
446+
func createPath(rule dataplane.PathRule) string {
447+
switch rule.PathType {
448+
case dataplane.PathTypeExact:
449+
return exactPath(rule.Path)
450+
default:
451+
return rule.Path
452+
}
453+
}
454+
372455
func createPathForMatch(path string, pathType dataplane.PathType, routeIdx int) string {
373456
return fmt.Sprintf("%s_%s_route%d", path, pathType, routeIdx)
374457
}
@@ -379,3 +462,8 @@ func createDefaultRootLocation() http.Location {
379462
Return: &http.Return{Code: http.StatusNotFound},
380463
}
381464
}
465+
466+
// returns whether or not a path is of type Prefix and does not contain a trailing slash
467+
func isNonSlashedPrefixPath(pathType dataplane.PathType, path string) bool {
468+
return pathType == dataplane.PathTypePrefix && !strings.HasSuffix(path, "/")
469+
}

internal/mode/static/nginx/config/servers_template.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ server {
3232
server_name {{ $s.ServerName }};
3333
3434
{{ range $l := $s.Locations }}
35-
location {{ if $l.Exact }}= {{ end }}{{ $l.Path }} {
35+
location {{ $l.Path }} {
3636
{{ if $l.Internal -}}
3737
internal;
3838
{{ end }}

0 commit comments

Comments
 (0)