Skip to content

Commit 0f465a7

Browse files
authored
Support for URL Rewrite filter (#1396)
Problem: As a user, I want to be able to configure URL rewrites for hostname and/or path rewrites on the server side. Solution: Using the HTTPRoute filter API, a user can now configure a hostname and/or path-based rewrite. Hostname rewrite will update the Host header, while a path rewrite utilizes nginx's `rewrite` directive. Enabled conformance tests for these features and added how-to guides.
1 parent b207f0e commit 0f465a7

25 files changed

+1650
-256
lines changed

conformance/Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ PREFIX = nginx-gateway-fabric
33
NGINX_PREFIX = $(PREFIX)/nginx
44
GW_API_VERSION ?= 1.0.0
55
GATEWAY_CLASS = nginx
6-
SUPPORTED_FEATURES = HTTPRoute,HTTPRouteQueryParamMatching,HTTPRouteMethodMatching,HTTPRoutePortRedirect,HTTPRouteSchemeRedirect,GatewayClassObservedGenerationBump
6+
SUPPORTED_FEATURES = HTTPRouteQueryParamMatching,HTTPRouteMethodMatching,HTTPRoutePortRedirect,HTTPRouteSchemeRedirect,HTTPRouteHostRewrite,HTTPRoutePathRewrite,GatewayPort8080
77
KIND_IMAGE ?= $(shell grep -m1 'FROM kindest/node' <tests/Dockerfile | awk -F'[ ]' '{print $$2}')
88
KIND_KUBE_CONFIG=$${HOME}/.kube/kind/config
99
CONFORMANCE_TAG = latest

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

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type Server struct {
1313
// Location holds all configuration for an HTTP location.
1414
type Location struct {
1515
Return *Return
16+
Rewrites []string
1617
Path string
1718
ProxyPass string
1819
HTTPMatchVar string

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

+150-49
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,26 @@ const (
1818
rootPath = "/"
1919
)
2020

21+
// baseHeaders contains the constant headers set in each server block
22+
var baseHeaders = []http.Header{
23+
{
24+
Name: "Host",
25+
Value: "$gw_api_compliant_host",
26+
},
27+
{
28+
Name: "X-Forwarded-For",
29+
Value: "$proxy_add_x_forwarded_for",
30+
},
31+
{
32+
Name: "Upgrade",
33+
Value: "$http_upgrade",
34+
},
35+
{
36+
Name: "Connection",
37+
Value: "$connection_upgrade",
38+
},
39+
}
40+
2141
func executeServers(conf dataplane.Configuration) []byte {
2242
servers := createServers(conf.HTTPServers, conf.SSLServers)
2343

@@ -72,6 +92,15 @@ func createServer(virtualServer dataplane.VirtualServer) http.Server {
7292
}
7393
}
7494

95+
// rewriteConfig contains the configuration for a location to rewrite paths,
96+
// as specified in a URLRewrite filter
97+
type rewriteConfig struct {
98+
// InternalRewrite rewrites an internal URI to the original URI (ex: /coffee_prefix_route0 -> /coffee)
99+
InternalRewrite string
100+
// MainRewrite rewrites the original URI to the new URI (ex: /coffee -> /beans)
101+
MainRewrite string
102+
}
103+
75104
func createLocations(pathRules []dataplane.PathRule, listenerPort int32) []http.Location {
76105
maxLocs, pathsAndTypes := getMaxLocationCountAndPathMap(pathRules)
77106
locs := make([]http.Location, 0, maxLocs)
@@ -94,42 +123,7 @@ func createLocations(pathRules []dataplane.PathRule, listenerPort int32) []http.
94123
matches = append(matches, match)
95124
}
96125

97-
if r.Filters.InvalidFilter != nil {
98-
for i := range buildLocations {
99-
buildLocations[i].Return = &http.Return{Code: http.StatusInternalServerError}
100-
}
101-
locs = append(locs, buildLocations...)
102-
continue
103-
}
104-
105-
// There could be a case when the filter has the type set but not the corresponding field.
106-
// For example, type is v1.HTTPRouteFilterRequestRedirect, but RequestRedirect field is nil.
107-
// The imported Webhook validation webhook catches that.
108-
109-
// FIXME(pleshakov): Ensure dataplane.Configuration -related types don't include v1 types, so that
110-
// we don't need to make any assumptions like above here. After fixing this, ensure that there is a test
111-
// for checking the imported Webhook validation catches the case above.
112-
// https://github.com/nginxinc/nginx-gateway-fabric/issues/660
113-
114-
// RequestRedirect and proxying are mutually exclusive.
115-
if r.Filters.RequestRedirect != nil {
116-
ret := createReturnValForRedirectFilter(r.Filters.RequestRedirect, listenerPort)
117-
for i := range buildLocations {
118-
buildLocations[i].Return = ret
119-
}
120-
locs = append(locs, buildLocations...)
121-
continue
122-
}
123-
124-
proxySetHeaders := generateProxySetHeaders(r.Filters.RequestHeaderModifiers)
125-
for i := range buildLocations {
126-
buildLocations[i].ProxySetHeaders = proxySetHeaders
127-
}
128-
129-
proxyPass := createProxyPass(r.BackendGroup)
130-
for i := range buildLocations {
131-
buildLocations[i].ProxyPass = proxyPass
132-
}
126+
buildLocations = updateLocationsForFilters(r.Filters, buildLocations, r, listenerPort, rule.Path)
133127
locs = append(locs, buildLocations...)
134128
}
135129

@@ -230,6 +224,48 @@ func initializeInternalLocation(
230224
return createMatchLocation(path), createHTTPMatch(match, path)
231225
}
232226

227+
// updateLocationsForFilters updates the existing locations with any relevant filters.
228+
func updateLocationsForFilters(
229+
filters dataplane.HTTPFilters,
230+
buildLocations []http.Location,
231+
matchRule dataplane.MatchRule,
232+
listenerPort int32,
233+
path string,
234+
) []http.Location {
235+
if filters.InvalidFilter != nil {
236+
for i := range buildLocations {
237+
buildLocations[i].Return = &http.Return{Code: http.StatusInternalServerError}
238+
}
239+
return buildLocations
240+
}
241+
242+
if filters.RequestRedirect != nil {
243+
ret := createReturnValForRedirectFilter(filters.RequestRedirect, listenerPort)
244+
for i := range buildLocations {
245+
buildLocations[i].Return = ret
246+
}
247+
return buildLocations
248+
}
249+
250+
rewrites := createRewritesValForRewriteFilter(filters.RequestURLRewrite, path)
251+
proxySetHeaders := generateProxySetHeaders(&matchRule.Filters)
252+
proxyPass := createProxyPass(matchRule.BackendGroup, matchRule.Filters.RequestURLRewrite)
253+
for i := range buildLocations {
254+
if rewrites != nil {
255+
if buildLocations[i].Internal && rewrites.InternalRewrite != "" {
256+
buildLocations[i].Rewrites = append(buildLocations[i].Rewrites, rewrites.InternalRewrite)
257+
}
258+
if rewrites.MainRewrite != "" {
259+
buildLocations[i].Rewrites = append(buildLocations[i].Rewrites, rewrites.MainRewrite)
260+
}
261+
}
262+
buildLocations[i].ProxySetHeaders = proxySetHeaders
263+
buildLocations[i].ProxyPass = proxyPass
264+
}
265+
266+
return buildLocations
267+
}
268+
233269
func createReturnValForRedirectFilter(filter *dataplane.HTTPRequestRedirectFilter, listenerPort int32) *http.Return {
234270
if filter == nil {
235271
return nil
@@ -275,6 +311,49 @@ func createReturnValForRedirectFilter(filter *dataplane.HTTPRequestRedirectFilte
275311
}
276312
}
277313

314+
func createRewritesValForRewriteFilter(filter *dataplane.HTTPURLRewriteFilter, path string) *rewriteConfig {
315+
if filter == nil {
316+
return nil
317+
}
318+
319+
rewrites := &rewriteConfig{}
320+
321+
if filter.Path != nil {
322+
rewrites.InternalRewrite = "^ $request_uri"
323+
switch filter.Path.Type {
324+
case dataplane.ReplaceFullPath:
325+
rewrites.MainRewrite = fmt.Sprintf("^ %s break", filter.Path.Replacement)
326+
case dataplane.ReplacePrefixMatch:
327+
filterPrefix := filter.Path.Replacement
328+
if filterPrefix == "" {
329+
filterPrefix = "/"
330+
}
331+
332+
// capture everything after the configured prefix
333+
regex := fmt.Sprintf("^%s(.*)$", path)
334+
// replace the configured prefix with the filter prefix and append what was captured
335+
replacement := fmt.Sprintf("%s$1", filterPrefix)
336+
337+
// if configured prefix does not end in /, but replacement prefix does end in /,
338+
// then make sure that we *require* but *don't capture* a trailing slash in the request,
339+
// otherwise we'll get duplicate slashes in the full replacement
340+
if strings.HasSuffix(filterPrefix, "/") && !strings.HasSuffix(path, "/") {
341+
regex = fmt.Sprintf("^%s(?:/(.*))?$", path)
342+
}
343+
344+
// if configured prefix ends in / we won't capture it for a request (since it's not in the regex),
345+
// so append it to the replacement prefix if the replacement prefix doesn't already end in /
346+
if strings.HasSuffix(path, "/") && !strings.HasSuffix(filterPrefix, "/") {
347+
replacement = fmt.Sprintf("%s/$1", filterPrefix)
348+
}
349+
350+
rewrites.MainRewrite = fmt.Sprintf("%s %s break", regex, replacement)
351+
}
352+
}
353+
354+
return rewrites
355+
}
356+
278357
// httpMatch is an internal representation of an HTTPRouteMatch.
279358
// This struct is marshaled into a string and stored as a variable in the nginx location block for the route's path.
280359
// The NJS httpmatches module will look up this variable on the request object and compare the request against the
@@ -354,13 +433,18 @@ func isPathOnlyMatch(match dataplane.Match) bool {
354433
return match.Method == nil && len(match.Headers) == 0 && len(match.QueryParams) == 0
355434
}
356435

357-
func createProxyPass(backendGroup dataplane.BackendGroup) string {
436+
func createProxyPass(backendGroup dataplane.BackendGroup, filter *dataplane.HTTPURLRewriteFilter) string {
437+
var requestURI string
438+
if filter == nil || filter.Path == nil {
439+
requestURI = "$request_uri"
440+
}
441+
358442
backendName := backendGroupName(backendGroup)
359443
if backendGroupNeedsSplit(backendGroup) {
360-
return "http://$" + convertStringToSafeVariableName(backendName)
444+
return "http://$" + convertStringToSafeVariableName(backendName) + requestURI
361445
}
362446

363-
return "http://" + backendName
447+
return "http://" + backendName + requestURI
364448
}
365449

366450
func createMatchLocation(path string) http.Location {
@@ -370,27 +454,44 @@ func createMatchLocation(path string) http.Location {
370454
}
371455
}
372456

373-
func generateProxySetHeaders(filters *dataplane.HTTPHeaderFilter) []http.Header {
374-
if filters == nil {
375-
return nil
457+
func generateProxySetHeaders(filters *dataplane.HTTPFilters) []http.Header {
458+
headers := make([]http.Header, len(baseHeaders))
459+
copy(headers, baseHeaders)
460+
461+
if filters != nil && filters.RequestURLRewrite != nil && filters.RequestURLRewrite.Hostname != nil {
462+
for i, header := range headers {
463+
if header.Name == "Host" {
464+
headers[i].Value = *filters.RequestURLRewrite.Hostname
465+
break
466+
}
467+
}
468+
}
469+
470+
if filters == nil || filters.RequestHeaderModifiers == nil {
471+
return headers
376472
}
377-
proxySetHeaders := make([]http.Header, 0, len(filters.Add)+len(filters.Set)+len(filters.Remove))
378-
if len(filters.Add) > 0 {
379-
addHeaders := convertAddHeaders(filters.Add)
473+
474+
headerFilter := filters.RequestHeaderModifiers
475+
476+
headerLen := len(headerFilter.Add) + len(headerFilter.Set) + len(headerFilter.Remove) + len(headers)
477+
proxySetHeaders := make([]http.Header, 0, headerLen)
478+
if len(headerFilter.Add) > 0 {
479+
addHeaders := convertAddHeaders(headerFilter.Add)
380480
proxySetHeaders = append(proxySetHeaders, addHeaders...)
381481
}
382-
if len(filters.Set) > 0 {
383-
setHeaders := convertSetHeaders(filters.Set)
482+
if len(headerFilter.Set) > 0 {
483+
setHeaders := convertSetHeaders(headerFilter.Set)
384484
proxySetHeaders = append(proxySetHeaders, setHeaders...)
385485
}
386486
// If the value of a header field is an empty string then this field will not be passed to a proxied server
387-
for _, h := range filters.Remove {
487+
for _, h := range headerFilter.Remove {
388488
proxySetHeaders = append(proxySetHeaders, http.Header{
389489
Name: h,
390490
Value: "",
391491
})
392492
}
393-
return proxySetHeaders
493+
494+
return append(proxySetHeaders, headers...)
394495
}
395496

396497
func convertAddHeaders(headers []dataplane.HTTPHeader) []http.Header {

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ server {
3737
internal;
3838
{{ end }}
3939
40+
{{- range $r := $l.Rewrites }}
41+
rewrite {{ $r }};
42+
{{- end }}
43+
4044
{{- if $l.Return -}}
4145
return {{ $l.Return.Code }} "{{ $l.Return.Body }}";
4246
{{ end }}
@@ -50,12 +54,8 @@ server {
5054
{{ range $h := $l.ProxySetHeaders }}
5155
proxy_set_header {{ $h.Name }} "{{ $h.Value }}";
5256
{{- end }}
53-
proxy_set_header Host $gw_api_compliant_host;
54-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
5557
proxy_http_version 1.1;
56-
proxy_set_header Upgrade $http_upgrade;
57-
proxy_set_header Connection $connection_upgrade;
58-
proxy_pass {{ $l.ProxyPass }}$request_uri;
58+
proxy_pass {{ $l.ProxyPass }};
5959
{{- end }}
6060
}
6161
{{ end }}

0 commit comments

Comments
 (0)