Skip to content

Commit 9bcfef0

Browse files
authored
Ensure Prefix matching requires trailing slash (#817)
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 9bcfef0

File tree

4 files changed

+634
-89
lines changed

4 files changed

+634
-89
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: 149 additions & 62 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+
if len(rule.MatchRules) != 1 || !isPathOnlyMatch(m) {
96+
intLocation, match := initializeInternalLocation(rule, matchRuleIdx, m)
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,40 +117,32 @@ 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 {
157-
// FIXME(sberman): De-dupe matches and associated locations
158-
// so we don't need nginx/njs to perform unnecessary matching.
159-
// https://github.com/nginxinc/nginx-kubernetes-gateway/issues/662
160-
b, err := json.Marshal(matches)
161-
if err != nil {
162-
// panic is safe here because we should never fail to marshal the match unless we constructed it incorrectly.
163-
panic(fmt.Errorf("could not marshal http match: %w", err))
141+
matchesStr := convertMatchesToString(matches)
142+
for i := range extLocations {
143+
extLocations[i].HTTPMatchVar = matchesStr
164144
}
165-
166-
pathLoc := http.Location{
167-
Path: rule.Path,
168-
Exact: rule.PathType == dataplane.PathTypeExact,
169-
HTTPMatchVar: string(b),
170-
}
171-
172-
locs = append(locs, pathLoc)
145+
locs = append(locs, extLocations...)
173146
}
174147
}
175148

@@ -180,6 +153,87 @@ func createLocations(pathRules []dataplane.PathRule, listenerPort int32) []http.
180153
return locs
181154
}
182155

156+
// pathAndTypeMap contains a map of paths and any path types defined for that path
157+
// for example, {/foo: {exact: {}, prefix: {}}}
158+
type pathAndTypeMap map[string]map[dataplane.PathType]struct{}
159+
160+
// To calculate the maximum number of locations, we need to take into account the following:
161+
// 1. Each match rule for a path rule will have one location.
162+
// 2. Each path rule may have an additional location if it contains non-path-only matches.
163+
// 3. Each prefix path rule may have an additional location if it doesn't contain trailing slash.
164+
// 4. There may be an additional location for the default root path.
165+
// We also return a map of all paths and their types.
166+
func getMaxLocationCountAndPathMap(pathRules []dataplane.PathRule) (int, pathAndTypeMap) {
167+
maxLocs := 1
168+
pathsAndTypes := make(pathAndTypeMap)
169+
for _, rule := range pathRules {
170+
maxLocs += len(rule.MatchRules) + 2
171+
if pathsAndTypes[rule.Path] == nil {
172+
pathsAndTypes[rule.Path] = map[dataplane.PathType]struct{}{
173+
rule.PathType: {},
174+
}
175+
} else {
176+
pathsAndTypes[rule.Path][rule.PathType] = struct{}{}
177+
}
178+
}
179+
180+
return maxLocs, pathsAndTypes
181+
}
182+
183+
func initializeExternalLocations(
184+
rule dataplane.PathRule,
185+
pathsAndTypes pathAndTypeMap,
186+
) []http.Location {
187+
extLocations := make([]http.Location, 0, 2)
188+
externalLocPath := createPath(rule)
189+
// If the path type is Prefix and doesn't contain a trailing slash, then we need a second location
190+
// that handles the Exact prefix case (if it doesn't already exist), and the first location is updated
191+
// to handle the trailing slash prefix case (if it doesn't already exist)
192+
if isNonSlashedPrefixPath(rule.PathType, externalLocPath) {
193+
// if Exact path and/or trailing slash Prefix path already exists, this means some routing rule
194+
// configures it. The routing rule location has priority over this location, so we don't try to
195+
// overwrite it and we don't add a duplicate location to NGINX because that will cause an NGINX config error.
196+
_, exactPathExists := pathsAndTypes[rule.Path][dataplane.PathTypeExact]
197+
var trailingSlashPrefixPathExists bool
198+
if pathTypes, exists := pathsAndTypes[rule.Path+"/"]; exists {
199+
_, trailingSlashPrefixPathExists = pathTypes[dataplane.PathTypePrefix]
200+
}
201+
202+
if exactPathExists && trailingSlashPrefixPathExists {
203+
return []http.Location{}
204+
}
205+
206+
if !trailingSlashPrefixPathExists {
207+
externalLocTrailing := http.Location{
208+
Path: externalLocPath + "/",
209+
}
210+
extLocations = append(extLocations, externalLocTrailing)
211+
}
212+
if !exactPathExists {
213+
externalLocExact := http.Location{
214+
Path: exactPath(externalLocPath),
215+
}
216+
extLocations = append(extLocations, externalLocExact)
217+
}
218+
} else {
219+
externalLoc := http.Location{
220+
Path: externalLocPath,
221+
}
222+
extLocations = []http.Location{externalLoc}
223+
}
224+
225+
return extLocations
226+
}
227+
228+
func initializeInternalLocation(
229+
rule dataplane.PathRule,
230+
matchRuleIdx int,
231+
match v1beta1.HTTPRouteMatch,
232+
) (http.Location, httpMatch) {
233+
path := createPathForMatch(rule.Path, rule.PathType, matchRuleIdx)
234+
return createMatchLocation(path), createHTTPMatch(match, path)
235+
}
236+
183237
func createReturnValForRedirectFilter(filter *v1beta1.HTTPRequestRedirectFilter, listenerPort int32) *http.Return {
184238
if filter == nil {
185239
return nil
@@ -308,12 +362,13 @@ func isPathOnlyMatch(match v1beta1.HTTPRouteMatch) bool {
308362
return match.Method == nil && match.Headers == nil && match.QueryParams == nil
309363
}
310364

311-
func createProxyPass(address string) string {
312-
return "http://" + address
313-
}
365+
func createProxyPass(backendGroup dataplane.BackendGroup) string {
366+
backendName := backendGroupName(backendGroup)
367+
if backendGroupNeedsSplit(backendGroup) {
368+
return "http://$" + convertStringToSafeVariableName(backendName)
369+
}
314370

315-
func createProxyPassForVar(variable string) string {
316-
return "http://$" + convertStringToSafeVariableName(variable)
371+
return "http://" + backendName
317372
}
318373

319374
func createMatchLocation(path string) http.Location {
@@ -369,6 +424,33 @@ func convertSetHeaders(headers []dataplane.HTTPHeader) []http.Header {
369424
return locHeaders
370425
}
371426

427+
func convertMatchesToString(matches []httpMatch) string {
428+
// FIXME(sberman): De-dupe matches and associated locations
429+
// so we don't need nginx/njs to perform unnecessary matching.
430+
// https://github.com/nginxinc/nginx-kubernetes-gateway/issues/662
431+
b, err := json.Marshal(matches)
432+
if err != nil {
433+
// panic is safe here because we should never fail to marshal the match unless we constructed it incorrectly.
434+
panic(fmt.Errorf("could not marshal http match: %w", err))
435+
}
436+
437+
return string(b)
438+
}
439+
440+
func exactPath(path string) string {
441+
return fmt.Sprintf("= %s", path)
442+
}
443+
444+
// createPath builds the location path depending on the path type.
445+
func createPath(rule dataplane.PathRule) string {
446+
switch rule.PathType {
447+
case dataplane.PathTypeExact:
448+
return exactPath(rule.Path)
449+
default:
450+
return rule.Path
451+
}
452+
}
453+
372454
func createPathForMatch(path string, pathType dataplane.PathType, routeIdx int) string {
373455
return fmt.Sprintf("%s_%s_route%d", path, pathType, routeIdx)
374456
}
@@ -379,3 +461,8 @@ func createDefaultRootLocation() http.Location {
379461
Return: &http.Return{Code: http.StatusNotFound},
380462
}
381463
}
464+
465+
// isNonSlashedPrefixPath returns whether or not a path is of type Prefix and does not contain a trailing slash
466+
func isNonSlashedPrefixPath(pathType dataplane.PathType, path string) bool {
467+
return pathType == dataplane.PathTypePrefix && !strings.HasSuffix(path, "/")
468+
}

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)