diff --git a/conformance/Makefile b/conformance/Makefile index 9e19179450..27819aefc3 100644 --- a/conformance/Makefile +++ b/conformance/Makefile @@ -3,7 +3,7 @@ PREFIX = nginx-gateway-fabric NGINX_PREFIX = $(PREFIX)/nginx GW_API_VERSION ?= 1.0.0 GATEWAY_CLASS = nginx -SUPPORTED_FEATURES = HTTPRoute,HTTPRouteQueryParamMatching,HTTPRouteMethodMatching,HTTPRoutePortRedirect,HTTPRouteSchemeRedirect,GatewayClassObservedGenerationBump +SUPPORTED_FEATURES = HTTPRouteQueryParamMatching,HTTPRouteMethodMatching,HTTPRoutePortRedirect,HTTPRouteSchemeRedirect,HTTPRouteHostRewrite,HTTPRoutePathRewrite,GatewayPort8080 KIND_IMAGE ?= $(shell grep -m1 'FROM kindest/node' /coffee) + InternalRewrite string + // MainRewrite rewrites the original URI to the new URI (ex: /coffee -> /beans) + MainRewrite string +} + func createLocations(pathRules []dataplane.PathRule, listenerPort int32) []http.Location { maxLocs, pathsAndTypes := getMaxLocationCountAndPathMap(pathRules) locs := make([]http.Location, 0, maxLocs) @@ -94,42 +123,7 @@ func createLocations(pathRules []dataplane.PathRule, listenerPort int32) []http. matches = append(matches, match) } - if r.Filters.InvalidFilter != nil { - for i := range buildLocations { - buildLocations[i].Return = &http.Return{Code: http.StatusInternalServerError} - } - locs = append(locs, buildLocations...) - continue - } - - // There could be a case when the filter has the type set but not the corresponding field. - // For example, type is v1.HTTPRouteFilterRequestRedirect, but RequestRedirect field is nil. - // The imported Webhook validation webhook catches that. - - // FIXME(pleshakov): Ensure dataplane.Configuration -related types don't include v1 types, so that - // we don't need to make any assumptions like above here. After fixing this, ensure that there is a test - // for checking the imported Webhook validation catches the case above. - // https://github.com/nginxinc/nginx-gateway-fabric/issues/660 - - // RequestRedirect and proxying are mutually exclusive. - if r.Filters.RequestRedirect != nil { - ret := createReturnValForRedirectFilter(r.Filters.RequestRedirect, listenerPort) - for i := range buildLocations { - buildLocations[i].Return = ret - } - locs = append(locs, buildLocations...) - continue - } - - proxySetHeaders := generateProxySetHeaders(r.Filters.RequestHeaderModifiers) - for i := range buildLocations { - buildLocations[i].ProxySetHeaders = proxySetHeaders - } - - proxyPass := createProxyPass(r.BackendGroup) - for i := range buildLocations { - buildLocations[i].ProxyPass = proxyPass - } + buildLocations = updateLocationsForFilters(r.Filters, buildLocations, r, listenerPort, rule.Path) locs = append(locs, buildLocations...) } @@ -230,6 +224,48 @@ func initializeInternalLocation( return createMatchLocation(path), createHTTPMatch(match, path) } +// updateLocationsForFilters updates the existing locations with any relevant filters. +func updateLocationsForFilters( + filters dataplane.HTTPFilters, + buildLocations []http.Location, + matchRule dataplane.MatchRule, + listenerPort int32, + path string, +) []http.Location { + if filters.InvalidFilter != nil { + for i := range buildLocations { + buildLocations[i].Return = &http.Return{Code: http.StatusInternalServerError} + } + return buildLocations + } + + if filters.RequestRedirect != nil { + ret := createReturnValForRedirectFilter(filters.RequestRedirect, listenerPort) + for i := range buildLocations { + buildLocations[i].Return = ret + } + return buildLocations + } + + rewrites := createRewritesValForRewriteFilter(filters.RequestURLRewrite, path) + proxySetHeaders := generateProxySetHeaders(&matchRule.Filters) + proxyPass := createProxyPass(matchRule.BackendGroup, matchRule.Filters.RequestURLRewrite) + for i := range buildLocations { + if rewrites != nil { + if buildLocations[i].Internal && rewrites.InternalRewrite != "" { + buildLocations[i].Rewrites = append(buildLocations[i].Rewrites, rewrites.InternalRewrite) + } + if rewrites.MainRewrite != "" { + buildLocations[i].Rewrites = append(buildLocations[i].Rewrites, rewrites.MainRewrite) + } + } + buildLocations[i].ProxySetHeaders = proxySetHeaders + buildLocations[i].ProxyPass = proxyPass + } + + return buildLocations +} + func createReturnValForRedirectFilter(filter *dataplane.HTTPRequestRedirectFilter, listenerPort int32) *http.Return { if filter == nil { return nil @@ -275,6 +311,49 @@ func createReturnValForRedirectFilter(filter *dataplane.HTTPRequestRedirectFilte } } +func createRewritesValForRewriteFilter(filter *dataplane.HTTPURLRewriteFilter, path string) *rewriteConfig { + if filter == nil { + return nil + } + + rewrites := &rewriteConfig{} + + if filter.Path != nil { + rewrites.InternalRewrite = "^ $request_uri" + switch filter.Path.Type { + case dataplane.ReplaceFullPath: + rewrites.MainRewrite = fmt.Sprintf("^ %s break", filter.Path.Replacement) + case dataplane.ReplacePrefixMatch: + filterPrefix := filter.Path.Replacement + if filterPrefix == "" { + filterPrefix = "/" + } + + // capture everything after the configured prefix + regex := fmt.Sprintf("^%s(.*)$", path) + // replace the configured prefix with the filter prefix and append what was captured + replacement := fmt.Sprintf("%s$1", filterPrefix) + + // if configured prefix does not end in /, but replacement prefix does end in /, + // then make sure that we *require* but *don't capture* a trailing slash in the request, + // otherwise we'll get duplicate slashes in the full replacement + if strings.HasSuffix(filterPrefix, "/") && !strings.HasSuffix(path, "/") { + regex = fmt.Sprintf("^%s(?:/(.*))?$", path) + } + + // if configured prefix ends in / we won't capture it for a request (since it's not in the regex), + // so append it to the replacement prefix if the replacement prefix doesn't already end in / + if strings.HasSuffix(path, "/") && !strings.HasSuffix(filterPrefix, "/") { + replacement = fmt.Sprintf("%s/$1", filterPrefix) + } + + rewrites.MainRewrite = fmt.Sprintf("%s %s break", regex, replacement) + } + } + + return rewrites +} + // httpMatch is an internal representation of an HTTPRouteMatch. // This struct is marshaled into a string and stored as a variable in the nginx location block for the route's path. // 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 { return match.Method == nil && len(match.Headers) == 0 && len(match.QueryParams) == 0 } -func createProxyPass(backendGroup dataplane.BackendGroup) string { +func createProxyPass(backendGroup dataplane.BackendGroup, filter *dataplane.HTTPURLRewriteFilter) string { + var requestURI string + if filter == nil || filter.Path == nil { + requestURI = "$request_uri" + } + backendName := backendGroupName(backendGroup) if backendGroupNeedsSplit(backendGroup) { - return "http://$" + convertStringToSafeVariableName(backendName) + return "http://$" + convertStringToSafeVariableName(backendName) + requestURI } - return "http://" + backendName + return "http://" + backendName + requestURI } func createMatchLocation(path string) http.Location { @@ -370,27 +454,44 @@ func createMatchLocation(path string) http.Location { } } -func generateProxySetHeaders(filters *dataplane.HTTPHeaderFilter) []http.Header { - if filters == nil { - return nil +func generateProxySetHeaders(filters *dataplane.HTTPFilters) []http.Header { + headers := make([]http.Header, len(baseHeaders)) + copy(headers, baseHeaders) + + if filters != nil && filters.RequestURLRewrite != nil && filters.RequestURLRewrite.Hostname != nil { + for i, header := range headers { + if header.Name == "Host" { + headers[i].Value = *filters.RequestURLRewrite.Hostname + break + } + } + } + + if filters == nil || filters.RequestHeaderModifiers == nil { + return headers } - proxySetHeaders := make([]http.Header, 0, len(filters.Add)+len(filters.Set)+len(filters.Remove)) - if len(filters.Add) > 0 { - addHeaders := convertAddHeaders(filters.Add) + + headerFilter := filters.RequestHeaderModifiers + + headerLen := len(headerFilter.Add) + len(headerFilter.Set) + len(headerFilter.Remove) + len(headers) + proxySetHeaders := make([]http.Header, 0, headerLen) + if len(headerFilter.Add) > 0 { + addHeaders := convertAddHeaders(headerFilter.Add) proxySetHeaders = append(proxySetHeaders, addHeaders...) } - if len(filters.Set) > 0 { - setHeaders := convertSetHeaders(filters.Set) + if len(headerFilter.Set) > 0 { + setHeaders := convertSetHeaders(headerFilter.Set) proxySetHeaders = append(proxySetHeaders, setHeaders...) } // If the value of a header field is an empty string then this field will not be passed to a proxied server - for _, h := range filters.Remove { + for _, h := range headerFilter.Remove { proxySetHeaders = append(proxySetHeaders, http.Header{ Name: h, Value: "", }) } - return proxySetHeaders + + return append(proxySetHeaders, headers...) } func convertAddHeaders(headers []dataplane.HTTPHeader) []http.Header { diff --git a/internal/mode/static/nginx/config/servers_template.go b/internal/mode/static/nginx/config/servers_template.go index 38321d9f42..7574efb1b8 100644 --- a/internal/mode/static/nginx/config/servers_template.go +++ b/internal/mode/static/nginx/config/servers_template.go @@ -37,6 +37,10 @@ server { internal; {{ end }} + {{- range $r := $l.Rewrites }} + rewrite {{ $r }}; + {{- end }} + {{- if $l.Return -}} return {{ $l.Return.Code }} "{{ $l.Return.Body }}"; {{ end }} @@ -50,12 +54,8 @@ server { {{ range $h := $l.ProxySetHeaders }} proxy_set_header {{ $h.Name }} "{{ $h.Value }}"; {{- end }} - proxy_set_header Host $gw_api_compliant_host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_pass {{ $l.ProxyPass }}$request_uri; + proxy_pass {{ $l.ProxyPass }}; {{- end }} } {{ end }} diff --git a/internal/mode/static/nginx/config/servers_test.go b/internal/mode/static/nginx/config/servers_test.go index 3cb1a1c5ca..91d3f9aae7 100644 --- a/internal/mode/static/nginx/config/servers_test.go +++ b/internal/mode/static/nginx/config/servers_test.go @@ -335,6 +335,51 @@ func TestCreateServers(t *testing.T) { }, }, }, + { + Path: "/rewrite", + PathType: dataplane.PathTypePrefix, + MatchRules: []dataplane.MatchRule{ + { + Match: dataplane.Match{}, + Filters: dataplane.HTTPFilters{ + RequestURLRewrite: &dataplane.HTTPURLRewriteFilter{ + Hostname: helpers.GetPointer("new.example.com"), + Path: &dataplane.HTTPPathModifier{ + Type: dataplane.ReplaceFullPath, + Replacement: "/replacement", + }, + }, + }, + BackendGroup: fooGroup, + }, + }, + }, + { + Path: "/rewrite-with-headers", + PathType: dataplane.PathTypePrefix, + MatchRules: []dataplane.MatchRule{ + { + Match: dataplane.Match{ + Headers: []dataplane.HTTPHeaderMatch{ + { + Name: "rewrite", + Value: "this", + }, + }, + }, + Filters: dataplane.HTTPFilters{ + RequestURLRewrite: &dataplane.HTTPURLRewriteFilter{ + Hostname: helpers.GetPointer("new.example.com"), + Path: &dataplane.HTTPPathModifier{ + Type: dataplane.ReplacePrefixMatch, + Replacement: "/prefix-replacement", + }, + }, + }, + BackendGroup: fooGroup, + }, + }, + }, { Path: "/invalid-filter", PathType: dataplane.PathTypePrefix, @@ -469,6 +514,30 @@ func TestCreateServers(t *testing.T) { RedirectPath: "/redirect-with-headers_prefix_route0", }, } + rewriteHeaderMatches := []httpMatch{ + { + Headers: []string{"rewrite:this"}, + RedirectPath: "/rewrite-with-headers_prefix_route0", + }, + } + rewriteProxySetHeaders := []http.Header{ + { + Name: "Host", + Value: "new.example.com", + }, + { + Name: "X-Forwarded-For", + Value: "$proxy_add_x_forwarded_for", + }, + { + Name: "Upgrade", + Value: "$http_upgrade", + }, + { + Name: "Connection", + Value: "$connection_upgrade", + }, + } invalidFilterHeaderMatches := []httpMatch{ { Headers: []string{"filter:this"}, @@ -484,40 +553,46 @@ func TestCreateServers(t *testing.T) { return []http.Location{ { - Path: "/_prefix_route0", - Internal: true, - ProxyPass: "http://test_foo_80", + Path: "/_prefix_route0", + Internal: true, + ProxyPass: "http://test_foo_80$request_uri", + ProxySetHeaders: baseHeaders, }, { - Path: "/_prefix_route1", - Internal: true, - ProxyPass: "http://test_foo_80", + Path: "/_prefix_route1", + Internal: true, + ProxyPass: "http://test_foo_80$request_uri", + ProxySetHeaders: baseHeaders, }, { - Path: "/_prefix_route2", - Internal: true, - ProxyPass: "http://test_foo_80", + Path: "/_prefix_route2", + Internal: true, + ProxyPass: "http://test_foo_80$request_uri", + ProxySetHeaders: baseHeaders, }, { Path: "/", HTTPMatchVar: expectedMatchString(slashMatches), }, { - Path: "/test_prefix_route0", - Internal: true, - ProxyPass: "http://$test__route1_rule1", + Path: "/test_prefix_route0", + Internal: true, + ProxyPass: "http://$test__route1_rule1$request_uri", + ProxySetHeaders: baseHeaders, }, { Path: "/test/", HTTPMatchVar: expectedMatchString(testMatches), }, { - Path: "/path-only/", - ProxyPass: "http://invalid-backend-ref", + Path: "/path-only/", + ProxyPass: "http://invalid-backend-ref$request_uri", + ProxySetHeaders: baseHeaders, }, { - Path: "= /path-only", - ProxyPass: "http://invalid-backend-ref", + Path: "= /path-only", + ProxyPass: "http://invalid-backend-ref$request_uri", + ProxySetHeaders: baseHeaders, }, { Path: "/redirect-implicit-port/", @@ -563,6 +638,33 @@ func TestCreateServers(t *testing.T) { Path: "= /redirect-with-headers", HTTPMatchVar: expectedMatchString(redirectHeaderMatches), }, + { + Path: "/rewrite/", + Rewrites: []string{"^ /replacement break"}, + ProxyPass: "http://test_foo_80", + ProxySetHeaders: rewriteProxySetHeaders, + }, + { + Path: "= /rewrite", + Rewrites: []string{"^ /replacement break"}, + ProxyPass: "http://test_foo_80", + ProxySetHeaders: rewriteProxySetHeaders, + }, + { + Path: "/rewrite-with-headers_prefix_route0", + Rewrites: []string{"^ $request_uri", "^/rewrite-with-headers(.*)$ /prefix-replacement$1 break"}, + Internal: true, + ProxyPass: "http://test_foo_80", + ProxySetHeaders: rewriteProxySetHeaders, + }, + { + Path: "/rewrite-with-headers/", + HTTPMatchVar: expectedMatchString(rewriteHeaderMatches), + }, + { + Path: "= /rewrite-with-headers", + HTTPMatchVar: expectedMatchString(rewriteHeaderMatches), + }, { Path: "/invalid-filter/", Return: &http.Return{ @@ -591,13 +693,15 @@ func TestCreateServers(t *testing.T) { HTTPMatchVar: expectedMatchString(invalidFilterHeaderMatches), }, { - Path: "= /exact", - ProxyPass: "http://test_foo_80", + Path: "= /exact", + ProxyPass: "http://test_foo_80$request_uri", + ProxySetHeaders: baseHeaders, }, { - Path: "/test_exact_route0", - ProxyPass: "http://test_foo_80", - Internal: true, + Path: "/test_exact_route0", + ProxyPass: "http://test_foo_80$request_uri", + ProxySetHeaders: baseHeaders, + Internal: true, }, { Path: "= /test", @@ -605,22 +709,54 @@ func TestCreateServers(t *testing.T) { }, { Path: "/proxy-set-headers/", - ProxyPass: "http://test_foo_80", + ProxyPass: "http://test_foo_80$request_uri", ProxySetHeaders: []http.Header{ { Name: "my-header", Value: "${my_header_header_var}some-value-123", }, + { + Name: "Host", + Value: "$gw_api_compliant_host", + }, + { + Name: "X-Forwarded-For", + Value: "$proxy_add_x_forwarded_for", + }, + { + Name: "Upgrade", + Value: "$http_upgrade", + }, + { + Name: "Connection", + Value: "$connection_upgrade", + }, }, }, { Path: "= /proxy-set-headers", - ProxyPass: "http://test_foo_80", + ProxyPass: "http://test_foo_80$request_uri", ProxySetHeaders: []http.Header{ { Name: "my-header", Value: "${my_header_header_var}some-value-123", }, + { + Name: "Host", + Value: "$gw_api_compliant_host", + }, + { + Name: "X-Forwarded-For", + Value: "$proxy_add_x_forwarded_for", + }, + { + Name: "Upgrade", + Value: "$http_upgrade", + }, + { + Name: "Connection", + Value: "$connection_upgrade", + }, }, }, } @@ -725,12 +861,14 @@ func TestCreateServersConflicts(t *testing.T) { }, expLocs: []http.Location{ { - Path: "/coffee/", - ProxyPass: "http://test_foo_80", + Path: "/coffee/", + ProxyPass: "http://test_foo_80$request_uri", + ProxySetHeaders: baseHeaders, }, { - Path: "= /coffee", - ProxyPass: "http://test_bar_80", + Path: "= /coffee", + ProxyPass: "http://test_bar_80$request_uri", + ProxySetHeaders: baseHeaders, }, createDefaultRootLocation(), }, @@ -761,12 +899,14 @@ func TestCreateServersConflicts(t *testing.T) { }, expLocs: []http.Location{ { - Path: "= /coffee", - ProxyPass: "http://test_foo_80", + Path: "= /coffee", + ProxyPass: "http://test_foo_80$request_uri", + ProxySetHeaders: baseHeaders, }, { - Path: "/coffee/", - ProxyPass: "http://test_bar_80", + Path: "/coffee/", + ProxyPass: "http://test_bar_80$request_uri", + ProxySetHeaders: baseHeaders, }, createDefaultRootLocation(), }, @@ -807,12 +947,14 @@ func TestCreateServersConflicts(t *testing.T) { }, expLocs: []http.Location{ { - Path: "/coffee/", - ProxyPass: "http://test_bar_80", + Path: "/coffee/", + ProxyPass: "http://test_bar_80$request_uri", + ProxySetHeaders: baseHeaders, }, { - Path: "= /coffee", - ProxyPass: "http://test_baz_80", + Path: "= /coffee", + ProxyPass: "http://test_baz_80$request_uri", + ProxySetHeaders: baseHeaders, }, createDefaultRootLocation(), }, @@ -916,12 +1058,14 @@ func TestCreateLocationsRootPath(t *testing.T) { pathRules: getPathRules(false /* rootPath */), expLocations: []http.Location{ { - Path: "/path-1", - ProxyPass: "http://test_foo_80", + Path: "/path-1", + ProxyPass: "http://test_foo_80$request_uri", + ProxySetHeaders: baseHeaders, }, { - Path: "/path-2", - ProxyPass: "http://test_foo_80", + Path: "/path-2", + ProxyPass: "http://test_foo_80$request_uri", + ProxySetHeaders: baseHeaders, }, { Path: "/", @@ -936,16 +1080,19 @@ func TestCreateLocationsRootPath(t *testing.T) { pathRules: getPathRules(true /* rootPath */), expLocations: []http.Location{ { - Path: "/path-1", - ProxyPass: "http://test_foo_80", + Path: "/path-1", + ProxyPass: "http://test_foo_80$request_uri", + ProxySetHeaders: baseHeaders, }, { - Path: "/path-2", - ProxyPass: "http://test_foo_80", + Path: "/path-2", + ProxyPass: "http://test_foo_80$request_uri", + ProxySetHeaders: baseHeaders, }, { - Path: "/", - ProxyPass: "http://test_foo_80", + Path: "/", + ProxyPass: "http://test_foo_80$request_uri", + ProxySetHeaders: baseHeaders, }, }, }, @@ -1102,6 +1249,132 @@ func TestCreateReturnValForRedirectFilter(t *testing.T) { } } +func TestCreateRewritesValForRewriteFilter(t *testing.T) { + tests := []struct { + filter *dataplane.HTTPURLRewriteFilter + expected *rewriteConfig + msg string + path string + }{ + { + filter: nil, + expected: nil, + msg: "filter is nil", + }, + { + filter: &dataplane.HTTPURLRewriteFilter{}, + expected: &rewriteConfig{}, + msg: "all fields are empty", + }, + { + filter: &dataplane.HTTPURLRewriteFilter{ + Path: &dataplane.HTTPPathModifier{ + Type: dataplane.ReplaceFullPath, + Replacement: "/full-path", + }, + }, + expected: &rewriteConfig{ + InternalRewrite: "^ $request_uri", + MainRewrite: "^ /full-path break", + }, + msg: "full path", + }, + { + path: "/original", + filter: &dataplane.HTTPURLRewriteFilter{ + Path: &dataplane.HTTPPathModifier{ + Type: dataplane.ReplacePrefixMatch, + Replacement: "/prefix-path", + }, + }, + expected: &rewriteConfig{ + InternalRewrite: "^ $request_uri", + MainRewrite: "^/original(.*)$ /prefix-path$1 break", + }, + msg: "prefix path no trailing slashes", + }, + { + path: "/original", + filter: &dataplane.HTTPURLRewriteFilter{ + Path: &dataplane.HTTPPathModifier{ + Type: dataplane.ReplacePrefixMatch, + Replacement: "", + }, + }, + expected: &rewriteConfig{ + InternalRewrite: "^ $request_uri", + MainRewrite: "^/original(?:/(.*))?$ /$1 break", + }, + msg: "prefix path empty string", + }, + { + path: "/original", + filter: &dataplane.HTTPURLRewriteFilter{ + Path: &dataplane.HTTPPathModifier{ + Type: dataplane.ReplacePrefixMatch, + Replacement: "/", + }, + }, + expected: &rewriteConfig{ + InternalRewrite: "^ $request_uri", + MainRewrite: "^/original(?:/(.*))?$ /$1 break", + }, + msg: "prefix path /", + }, + { + path: "/original", + filter: &dataplane.HTTPURLRewriteFilter{ + Path: &dataplane.HTTPPathModifier{ + Type: dataplane.ReplacePrefixMatch, + Replacement: "/trailing/", + }, + }, + expected: &rewriteConfig{ + InternalRewrite: "^ $request_uri", + MainRewrite: "^/original(?:/(.*))?$ /trailing/$1 break", + }, + msg: "prefix path replacement with trailing /", + }, + { + path: "/original/", + filter: &dataplane.HTTPURLRewriteFilter{ + Path: &dataplane.HTTPPathModifier{ + Type: dataplane.ReplacePrefixMatch, + Replacement: "/prefix-path", + }, + }, + expected: &rewriteConfig{ + InternalRewrite: "^ $request_uri", + MainRewrite: "^/original/(.*)$ /prefix-path/$1 break", + }, + msg: "prefix path original with trailing /", + }, + { + path: "/original/", + filter: &dataplane.HTTPURLRewriteFilter{ + Path: &dataplane.HTTPPathModifier{ + Type: dataplane.ReplacePrefixMatch, + Replacement: "/trailing/", + }, + }, + expected: &rewriteConfig{ + InternalRewrite: "^ $request_uri", + MainRewrite: "^/original/(.*)$ /trailing/$1 break", + }, + msg: "prefix path both with trailing slashes", + }, + } + + for _, test := range tests { + t.Run(test.msg, func(t *testing.T) { + g := NewWithT(t) + + result := createRewritesValForRewriteFilter(test.filter, test.path) + g.Expect(helpers.Diff(test.expected, result)).To(BeEmpty()) + }) + } +} + func TestCreateHTTPMatch(t *testing.T) { testPath := "/internal_loc" @@ -1360,11 +1633,12 @@ func TestCreateProxyPass(t *testing.T) { g := NewWithT(t) tests := []struct { + rewrite *dataplane.HTTPURLRewriteFilter expected string grp dataplane.BackendGroup }{ { - expected: "http://10.0.0.1:80", + expected: "http://10.0.0.1:80$request_uri", grp: dataplane.BackendGroup{ Backends: []dataplane.Backend{ { @@ -1376,7 +1650,7 @@ func TestCreateProxyPass(t *testing.T) { }, }, { - expected: "http://$ns1__bg_rule0", + expected: "http://$ns1__bg_rule0$request_uri", grp: dataplane.BackendGroup{ Source: types.NamespacedName{Namespace: "ns1", Name: "bg"}, Backends: []dataplane.Backend{ @@ -1393,10 +1667,25 @@ func TestCreateProxyPass(t *testing.T) { }, }, }, + { + expected: "http://10.0.0.1:80", + rewrite: &dataplane.HTTPURLRewriteFilter{ + Path: &dataplane.HTTPPathModifier{}, + }, + grp: dataplane.BackendGroup{ + Backends: []dataplane.Backend{ + { + UpstreamName: "10.0.0.1:80", + Valid: true, + Weight: 1, + }, + }, + }, + }, } for _, tc := range tests { - result := createProxyPass(tc.grp) + result := createProxyPass(tc.grp, tc.rewrite) g.Expect(result).To(Equal(tc.expected)) } } @@ -1438,38 +1727,107 @@ func TestCreatePathForMatch(t *testing.T) { } func TestGenerateProxySetHeaders(t *testing.T) { - g := NewWithT(t) - - filters := dataplane.HTTPHeaderFilter{ - Add: []dataplane.HTTPHeader{ - { - Name: "Authorization", - Value: "my-auth", + tests := []struct { + filters *dataplane.HTTPFilters + msg string + expectedHeaders []http.Header + }{ + { + msg: "header filter", + filters: &dataplane.HTTPFilters{ + RequestHeaderModifiers: &dataplane.HTTPHeaderFilter{ + Add: []dataplane.HTTPHeader{ + { + Name: "Authorization", + Value: "my-auth", + }, + }, + Set: []dataplane.HTTPHeader{ + { + Name: "Accept-Encoding", + Value: "gzip", + }, + }, + Remove: []string{"my-header"}, + }, }, - }, - Set: []dataplane.HTTPHeader{ - { - Name: "Accept-Encoding", - Value: "gzip", + expectedHeaders: []http.Header{ + { + Name: "Authorization", + Value: "${authorization_header_var}my-auth", + }, + { + Name: "Accept-Encoding", + Value: "gzip", + }, + { + Name: "my-header", + Value: "", + }, + { + Name: "Host", + Value: "$gw_api_compliant_host", + }, + { + Name: "X-Forwarded-For", + Value: "$proxy_add_x_forwarded_for", + }, + { + Name: "Upgrade", + Value: "$http_upgrade", + }, + { + Name: "Connection", + Value: "$connection_upgrade", + }, }, }, - Remove: []string{"my-header"}, - } - expectedHeaders := []http.Header{ - { - Name: "Authorization", - Value: "${authorization_header_var}my-auth", - }, - { - Name: "Accept-Encoding", - Value: "gzip", - }, { - Name: "my-header", - Value: "", + msg: "with url rewrite hostname", + filters: &dataplane.HTTPFilters{ + RequestHeaderModifiers: &dataplane.HTTPHeaderFilter{ + Add: []dataplane.HTTPHeader{ + { + Name: "Authorization", + Value: "my-auth", + }, + }, + }, + RequestURLRewrite: &dataplane.HTTPURLRewriteFilter{ + Hostname: helpers.GetPointer("rewrite-hostname"), + }, + }, + expectedHeaders: []http.Header{ + { + Name: "Authorization", + Value: "${authorization_header_var}my-auth", + }, + { + Name: "Host", + Value: "rewrite-hostname", + }, + { + Name: "X-Forwarded-For", + Value: "$proxy_add_x_forwarded_for", + }, + { + Name: "Upgrade", + Value: "$http_upgrade", + }, + { + Name: "Connection", + Value: "$connection_upgrade", + }, + }, }, } - headers := generateProxySetHeaders(&filters) - g.Expect(headers).To(Equal(expectedHeaders)) + for _, tc := range tests { + t.Run(tc.msg, func(t *testing.T) { + g := NewWithT(t) + + headers := generateProxySetHeaders(tc.filters) + g.Expect(headers).To(Equal(tc.expectedHeaders)) + }) + } } diff --git a/internal/mode/static/nginx/config/validation/common.go b/internal/mode/static/nginx/config/validation/common.go index fc98931da6..bca6a97095 100644 --- a/internal/mode/static/nginx/config/validation/common.go +++ b/internal/mode/static/nginx/config/validation/common.go @@ -8,6 +8,16 @@ import ( k8svalidation "k8s.io/apimachinery/pkg/util/validation" ) +const ( + pathFmt = `/[^\s{};]*` + pathErrMsg = "must start with / and must not include any whitespace character, `{`, `}` or `;`" +) + +var ( + pathRegexp = regexp.MustCompile("^" + pathFmt + "$") + pathExamples = []string{"/", "/path", "/path/subpath-123"} +) + const ( escapedStringsFmt = `([^"\\]|\\.)*` escapedStringsErrMsg = `must have all '"' (double quotes) escaped and must not end with an unescaped '\' ` + @@ -30,7 +40,7 @@ func validateEscapedString(value string, examples []string) error { const ( escapedStringsNoVarExpansionFmt = `([^"$\\]|\\[^$])*` - escapedStringsNoVarExpansionErrMsg string = `a valid header must have all '"' escaped and must not contain any ` + + escapedStringsNoVarExpansionErrMsg string = `a valid value must have all '"' escaped and must not contain any ` + `'$' or end with an unescaped '\'` ) diff --git a/internal/mode/static/nginx/config/validation/http_filters.go b/internal/mode/static/nginx/config/validation/http_filters.go index e505f94276..3fc638108e 100644 --- a/internal/mode/static/nginx/config/validation/http_filters.go +++ b/internal/mode/static/nginx/config/validation/http_filters.go @@ -1,9 +1,19 @@ package validation +import ( + "errors" + "strings" + + k8svalidation "k8s.io/apimachinery/pkg/util/validation" +) + // HTTPRedirectValidator validates values for a redirect, which in NGINX is done with the return directive. // For example, return 302 "https://example.com:8080"; type HTTPRedirectValidator struct{} +// HTTPURLRewriteValidator validates values for a URL rewrite. +type HTTPURLRewriteValidator struct{} + // HTTPRequestHeaderValidator validates values for request headers, // which in NGINX is done with the proxy_set_header directive. type HTTPRequestHeaderValidator struct{} @@ -20,12 +30,6 @@ func (HTTPRedirectValidator) ValidateRedirectScheme(scheme string) (valid bool, return validateInSupportedValues(scheme, supportedRedirectSchemes) } -var redirectHostnameExamples = []string{"host", "example.com"} - -func (HTTPRedirectValidator) ValidateRedirectHostname(hostname string) error { - return validateEscapedStringNoVarExpansion(hostname, redirectHostnameExamples) -} - func (HTTPRedirectValidator) ValidateRedirectPort(_ int32) error { // any value is allowed return nil @@ -44,6 +48,30 @@ func (HTTPRedirectValidator) ValidateRedirectStatusCode(statusCode int) (valid b return validateInSupportedValues(statusCode, supportedRedirectStatusCodes) } +var hostnameExamples = []string{"host", "example.com"} + +func (HTTPRedirectValidator) ValidateHostname(hostname string) error { + return validateEscapedStringNoVarExpansion(hostname, hostnameExamples) +} + +// ValidateRewritePath validates a path used in a URL Rewrite filter. +func (HTTPURLRewriteValidator) ValidateRewritePath(path string) error { + if path == "" { + return nil + } + + if !pathRegexp.MatchString(path) { + msg := k8svalidation.RegexError(pathErrMsg, pathFmt, pathExamples...) + return errors.New(msg) + } + + if strings.Contains(path, "$") { + return errors.New("cannot contain $") + } + + return nil +} + func (HTTPRequestHeaderValidator) ValidateRequestHeaderName(name string) error { return validateHeaderName(name) } diff --git a/internal/mode/static/nginx/config/validation/http_filters_test.go b/internal/mode/static/nginx/config/validation/http_filters_test.go index 9ebc269469..c216a30224 100644 --- a/internal/mode/static/nginx/config/validation/http_filters_test.go +++ b/internal/mode/static/nginx/config/validation/http_filters_test.go @@ -23,22 +23,6 @@ func TestValidateRedirectScheme(t *testing.T) { ) } -func TestValidateRedirectHostname(t *testing.T) { - validator := HTTPRedirectValidator{} - - testValidValuesForSimpleValidator( - t, - validator.ValidateRedirectHostname, - "example.com", - ) - - testInvalidValuesForSimpleValidator( - t, - validator.ValidateRedirectHostname, - "example.com$", - ) -} - func TestValidateRedirectPort(t *testing.T) { validator := HTTPRedirectValidator{} @@ -67,6 +51,43 @@ func TestValidateRedirectStatusCode(t *testing.T) { ) } +func TestValidateHostname(t *testing.T) { + validator := HTTPRedirectValidator{} + + testValidValuesForSimpleValidator( + t, + validator.ValidateHostname, + "example.com", + ) + + testInvalidValuesForSimpleValidator( + t, + validator.ValidateHostname, + "example.com$", + ) +} + +func TestValidateRewritePath(t *testing.T) { + validator := HTTPURLRewriteValidator{} + + testValidValuesForSimpleValidator( + t, + validator.ValidateRewritePath, + "", + "/path", + "/longer/path", + "/trailing/", + ) + + testInvalidValuesForSimpleValidator( + t, + validator.ValidateRewritePath, + "path", + "$path", + "/path$", + ) +} + func TestValidateRequestHeaderName(t *testing.T) { validator := HTTPRequestHeaderValidator{} diff --git a/internal/mode/static/nginx/config/validation/http_njs_match.go b/internal/mode/static/nginx/config/validation/http_njs_match.go index dc224c38e7..0db9ce6d9e 100644 --- a/internal/mode/static/nginx/config/validation/http_njs_match.go +++ b/internal/mode/static/nginx/config/validation/http_njs_match.go @@ -3,7 +3,6 @@ package validation import ( "errors" "fmt" - "regexp" "strings" k8svalidation "k8s.io/apimachinery/pkg/util/validation" @@ -16,16 +15,6 @@ import ( // so changes to the implementation change the validation rules here. type HTTPNJSMatchValidator struct{} -const ( - pathFmt = `/[^\s{};]*` - pathErrMsg = "must start with / and must not include any whitespace character, `{`, `}` or `;`" -) - -var ( - pathRegexp = regexp.MustCompile("^" + pathFmt + "$") - pathExamples = []string{"/", "/path", "/path/subpath-123"} -) - // ValidatePathInMatch a path used in the location directive. func (HTTPNJSMatchValidator) ValidatePathInMatch(path string) error { if path == "" { diff --git a/internal/mode/static/nginx/config/validation/http_validator.go b/internal/mode/static/nginx/config/validation/http_validator.go index 10c98aec34..f33adb8434 100644 --- a/internal/mode/static/nginx/config/validation/http_validator.go +++ b/internal/mode/static/nginx/config/validation/http_validator.go @@ -10,6 +10,7 @@ import ( type HTTPValidator struct { HTTPNJSMatchValidator HTTPRedirectValidator + HTTPURLRewriteValidator HTTPRequestHeaderValidator } diff --git a/internal/mode/static/nginx/modules/src/httpmatches.js b/internal/mode/static/nginx/modules/src/httpmatches.js index 436a0391fd..1f36ae57ec 100644 --- a/internal/mode/static/nginx/modules/src/httpmatches.js +++ b/internal/mode/static/nginx/modules/src/httpmatches.js @@ -1,3 +1,5 @@ +import qs from 'querystring'; + const MATCHES_VARIABLE = 'http_matches'; const HTTP_CODES = { notFound: 404, @@ -44,7 +46,14 @@ function redirect(r) { return; } - r.internalRedirect(match.redirectPath); + // If performing a rewrite, $request_uri won't be used, + // so we have to preserve args in the internal redirect. + let args = qs.stringify(r.args); + if (args) { + args = '?' + args; + } + + r.internalRedirect(match.redirectPath + args); } function extractMatchesFromRequest(r) { diff --git a/internal/mode/static/nginx/modules/test/httpmatches.test.js b/internal/mode/static/nginx/modules/test/httpmatches.test.js index 669638b819..ac020abba9 100644 --- a/internal/mode/static/nginx/modules/test/httpmatches.test.js +++ b/internal/mode/static/nginx/modules/test/httpmatches.test.js @@ -439,7 +439,7 @@ describe('redirect', () => { params: { Arg1: 'value1', arg2: 'value2=SOME=other=value' }, }), matches: [testHeaderMatches, testQueryParamMatches, testAllMatchTypes, testAnyMatch], // request matches testAllMatchTypes and testAnyMatch. But first match should win. - expectedRedirect: '/a-match', + expectedRedirect: '/a-match?Arg1=value1&arg2=value2%3DSOME%3Dother%3Dvalue', }, ]; diff --git a/internal/mode/static/state/dataplane/configuration.go b/internal/mode/static/state/dataplane/configuration.go index dab0323bce..81ebb817c3 100644 --- a/internal/mode/static/state/dataplane/configuration.go +++ b/internal/mode/static/state/dataplane/configuration.go @@ -455,6 +455,11 @@ func createHTTPFilters(filters []v1.HTTPRouteFilter) HTTPFilters { // using the first filter result.RequestRedirect = convertHTTPRequestRedirectFilter(f.RequestRedirect) } + case v1.HTTPRouteFilterURLRewrite: + if result.RequestURLRewrite == nil { + // using the first filter + result.RequestURLRewrite = convertHTTPURLRewriteFilter(f.URLRewrite) + } case v1.HTTPRouteFilterRequestHeaderModifier: if result.RequestHeaderModifiers == nil { // using the first filter diff --git a/internal/mode/static/state/dataplane/configuration_test.go b/internal/mode/static/state/dataplane/configuration_test.go index f23e9ec9f7..d4d8a3a7bf 100644 --- a/internal/mode/static/state/dataplane/configuration_test.go +++ b/internal/mode/static/state/dataplane/configuration_test.go @@ -1649,6 +1649,18 @@ func TestCreateFilters(t *testing.T) { Hostname: helpers.GetPointer[v1.PreciseHostname]("bar.example.com"), }, } + rewrite1 := v1.HTTPRouteFilter{ + Type: v1.HTTPRouteFilterURLRewrite, + URLRewrite: &v1.HTTPURLRewriteFilter{ + Hostname: helpers.GetPointer[v1.PreciseHostname]("foo.example.com"), + }, + } + rewrite2 := v1.HTTPRouteFilter{ + Type: v1.HTTPRouteFilterURLRewrite, + URLRewrite: &v1.HTTPURLRewriteFilter{ + Hostname: helpers.GetPointer[v1.PreciseHostname]("bar.example.com"), + }, + } requestHeaderModifiers1 := v1.HTTPRouteFilter{ Type: v1.HTTPRouteFilterRequestHeaderModifier, RequestHeaderModifier: &v1.HTTPHeaderFilter{ @@ -1675,6 +1687,9 @@ func TestCreateFilters(t *testing.T) { expectedRedirect1 := HTTPRequestRedirectFilter{ Hostname: helpers.GetPointer("foo.example.com"), } + expectedRewrite1 := HTTPURLRewriteFilter{ + Hostname: helpers.GetPointer("foo.example.com"), + } expectedHeaderModifier1 := HTTPHeaderFilter{ Set: []HTTPHeader{ { @@ -1729,14 +1744,17 @@ func TestCreateFilters(t *testing.T) { filters: []v1.HTTPRouteFilter{ redirect1, redirect2, + rewrite1, + rewrite2, requestHeaderModifiers1, requestHeaderModifiers2, }, expected: HTTPFilters{ RequestRedirect: &expectedRedirect1, + RequestURLRewrite: &expectedRewrite1, RequestHeaderModifiers: &expectedHeaderModifier1, }, - msg: "two redirect filters, two request header modifier, first value for each wins", + msg: "two of each filter, first value for each wins", }, } diff --git a/internal/mode/static/state/dataplane/convert.go b/internal/mode/static/state/dataplane/convert.go index 2ca8713e85..4337a0d787 100644 --- a/internal/mode/static/state/dataplane/convert.go +++ b/internal/mode/static/state/dataplane/convert.go @@ -46,6 +46,13 @@ func convertHTTPRequestRedirectFilter(filter *v1.HTTPRequestRedirectFilter) *HTT } } +func convertHTTPURLRewriteFilter(filter *v1.HTTPURLRewriteFilter) *HTTPURLRewriteFilter { + return &HTTPURLRewriteFilter{ + Hostname: (*string)(filter.Hostname), + Path: convertPathModifier(filter.Path), + } +} + func convertHTTPHeaderFilter(filter *v1.HTTPHeaderFilter) *HTTPHeaderFilter { result := &HTTPHeaderFilter{ Remove: filter.Remove, @@ -78,3 +85,22 @@ func convertPathType(pathType v1.PathMatchType) PathType { panic(fmt.Sprintf("unsupported path type: %s", pathType)) } } + +func convertPathModifier(path *v1.HTTPPathModifier) *HTTPPathModifier { + if path != nil { + switch path.Type { + case v1.FullPathHTTPPathModifier: + return &HTTPPathModifier{ + Type: ReplaceFullPath, + Replacement: *path.ReplaceFullPath, + } + case v1.PrefixMatchHTTPPathModifier: + return &HTTPPathModifier{ + Type: ReplacePrefixMatch, + Replacement: *path.ReplacePrefixMatch, + } + } + } + + return nil +} diff --git a/internal/mode/static/state/dataplane/convert_test.go b/internal/mode/static/state/dataplane/convert_test.go index 27feeec417..1688c1591f 100644 --- a/internal/mode/static/state/dataplane/convert_test.go +++ b/internal/mode/static/state/dataplane/convert_test.go @@ -161,6 +161,63 @@ func TestConvertHTTPRequestRedirectFilter(t *testing.T) { } } +func TestConvertHTTPURLRewriteFilter(t *testing.T) { + tests := []struct { + filter *v1.HTTPURLRewriteFilter + expected *HTTPURLRewriteFilter + name string + }{ + { + filter: &v1.HTTPURLRewriteFilter{}, + expected: &HTTPURLRewriteFilter{}, + name: "empty", + }, + { + filter: &v1.HTTPURLRewriteFilter{ + Hostname: helpers.GetPointer[v1.PreciseHostname]("example.com"), + Path: &v1.HTTPPathModifier{ + Type: v1.FullPathHTTPPathModifier, + ReplaceFullPath: helpers.GetPointer("/path"), + }, + }, + expected: &HTTPURLRewriteFilter{ + Hostname: helpers.GetPointer("example.com"), + Path: &HTTPPathModifier{ + Type: ReplaceFullPath, + Replacement: "/path", + }, + }, + name: "full path modifier", + }, + { + filter: &v1.HTTPURLRewriteFilter{ + Hostname: helpers.GetPointer[v1.PreciseHostname]("example.com"), + Path: &v1.HTTPPathModifier{ + Type: v1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: helpers.GetPointer("/path"), + }, + }, + expected: &HTTPURLRewriteFilter{ + Hostname: helpers.GetPointer("example.com"), + Path: &HTTPPathModifier{ + Type: ReplacePrefixMatch, + Replacement: "/path", + }, + }, + name: "prefix path modifier", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + result := convertHTTPURLRewriteFilter(test.filter) + g.Expect(result).To(Equal(test.expected)) + }) + } +} + func TestConvertHTTPHeaderFilter(t *testing.T) { tests := []struct { filter *v1.HTTPHeaderFilter diff --git a/internal/mode/static/state/dataplane/types.go b/internal/mode/static/state/dataplane/types.go index 98d61c73f5..465e421ea3 100644 --- a/internal/mode/static/state/dataplane/types.go +++ b/internal/mode/static/state/dataplane/types.go @@ -97,6 +97,8 @@ type HTTPFilters struct { InvalidFilter *InvalidHTTPFilter // RequestRedirect holds the HTTPRequestRedirectFilter. RequestRedirect *HTTPRequestRedirectFilter + // RequestURLRewrite holds the HTTPURLRewriteFilter. + RequestURLRewrite *HTTPURLRewriteFilter // RequestHeaderModifiers holds the HTTPHeaderFilter. RequestHeaderModifiers *HTTPHeaderFilter } @@ -131,6 +133,33 @@ type HTTPRequestRedirectFilter struct { StatusCode *int } +// HTTPURLRewriteFilter rewrites HTTP requests. +type HTTPURLRewriteFilter struct { + // Hostname is the hostname of the rewrite. + Hostname *string + // Path is the path of the rewrite. + Path *HTTPPathModifier +} + +// PathModifierType is the type of the PathModifier in a redirect or rewrite rule. +type PathModifierType string + +const ( + // ReplaceFullPath indicates that we replace the full path. + ReplaceFullPath PathModifierType = "ReplaceFullPath" + // ReplacePrefixMatch indicates that we replace a prefix match. + ReplacePrefixMatch PathModifierType = "ReplacePrefixMatch" +) + +// HTTPPathModifier defines configuration for path modifiers. +type HTTPPathModifier struct { + // Replacement specifies the value with which to replace the full path or prefix match of a request during + // a rewrite or redirect. + Replacement string + // Type indicates the type of path modifier. + Type PathModifierType +} + // HTTPHeaderMatch matches an HTTP header. type HTTPHeaderMatch struct { // Name is the name of the header to match. diff --git a/internal/mode/static/state/graph/httproute.go b/internal/mode/static/state/graph/httproute.go index 65079a0c83..c8f42997e2 100644 --- a/internal/mode/static/state/graph/httproute.go +++ b/internal/mode/static/state/graph/httproute.go @@ -693,6 +693,8 @@ func validateFilter( switch filter.Type { case v1.HTTPRouteFilterRequestRedirect: return validateFilterRedirect(validator, filter, filterPath) + case v1.HTTPRouteFilterURLRewrite: + return validateFilterRewrite(validator, filter, filterPath) case v1.HTTPRouteFilterRequestHeaderModifier: return validateFilterHeaderModifier(validator, filter, filterPath) default: @@ -701,6 +703,7 @@ func validateFilter( filter.Type, []string{ string(v1.HTTPRouteFilterRequestRedirect), + string(v1.HTTPRouteFilterURLRewrite), string(v1.HTTPRouteFilterRequestHeaderModifier), }, ) @@ -721,7 +724,6 @@ func validateFilterRedirect( } redirect := filter.RequestRedirect - redirectPath := filterPath.Child("requestRedirect") if redirect.Scheme != nil { @@ -732,7 +734,7 @@ func validateFilterRedirect( } if redirect.Hostname != nil { - if err := validator.ValidateRedirectHostname(string(*redirect.Hostname)); err != nil { + if err := validator.ValidateHostname(string(*redirect.Hostname)); err != nil { valErr := field.Invalid(redirectPath.Child("hostname"), *redirect.Hostname, err.Error()) allErrs = append(allErrs, valErr) } @@ -760,6 +762,49 @@ func validateFilterRedirect( return allErrs } +func validateFilterRewrite( + validator validation.HTTPFieldsValidator, + filter v1.HTTPRouteFilter, + filterPath *field.Path, +) field.ErrorList { + var allErrs field.ErrorList + + if filter.URLRewrite == nil { + panicForBrokenWebhookAssumption(errors.New("urlRewrite cannot be nil")) + } + + rewrite := filter.URLRewrite + rewritePath := filterPath.Child("urlRewrite") + + if rewrite.Hostname != nil { + if err := validator.ValidateHostname(string(*rewrite.Hostname)); err != nil { + valErr := field.Invalid(rewritePath.Child("hostname"), *rewrite.Hostname, err.Error()) + allErrs = append(allErrs, valErr) + } + } + + if rewrite.Path != nil { + var path string + switch rewrite.Path.Type { + case v1.FullPathHTTPPathModifier: + path = *rewrite.Path.ReplaceFullPath + case v1.PrefixMatchHTTPPathModifier: + path = *rewrite.Path.ReplacePrefixMatch + default: + msg := fmt.Sprintf("urlRewrite path type %s not supported", rewrite.Path.Type) + valErr := field.Invalid(rewritePath.Child("path"), *rewrite.Path, msg) + return append(allErrs, valErr) + } + + if err := validator.ValidateRewritePath(path); err != nil { + valErr := field.Invalid(rewritePath.Child("path"), *rewrite.Path, err.Error()) + allErrs = append(allErrs, valErr) + } + } + + return allErrs +} + func validateFilterHeaderModifier( validator validation.HTTPFieldsValidator, filter v1.HTTPRouteFilter, diff --git a/internal/mode/static/state/graph/httproute_test.go b/internal/mode/static/state/graph/httproute_test.go index c8d7c4d7e0..ec3b5ea521 100644 --- a/internal/mode/static/state/graph/httproute_test.go +++ b/internal/mode/static/state/graph/httproute_test.go @@ -375,7 +375,7 @@ func TestBuildRoute(t *testing.T) { } return nil }, - ValidateRedirectHostnameStub: func(h string) error { + ValidateHostnameStub: func(h string) error { if h == invalidRedirectHostname { return errors.New("invalid hostname") } @@ -1856,6 +1856,14 @@ func TestValidateFilter(t *testing.T) { expectErrCount: 0, name: "valid redirect filter", }, + { + filter: gatewayv1.HTTPRouteFilter{ + Type: gatewayv1.HTTPRouteFilterURLRewrite, + URLRewrite: &gatewayv1.HTTPURLRewriteFilter{}, + }, + expectErrCount: 0, + name: "valid rewrite filter", + }, { filter: gatewayv1.HTTPRouteFilter{ Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, @@ -1866,7 +1874,7 @@ func TestValidateFilter(t *testing.T) { }, { filter: gatewayv1.HTTPRouteFilter{ - Type: gatewayv1.HTTPRouteFilterURLRewrite, + Type: gatewayv1.HTTPRouteFilterRequestMirror, }, expectErrCount: 1, name: "unsupported filter", @@ -1899,7 +1907,17 @@ func TestValidateFilterRedirect(t *testing.T) { validator *validationfakes.FakeHTTPFieldsValidator name string expectErrCount int + panic bool }{ + { + validator: &validationfakes.FakeHTTPFieldsValidator{}, + filter: gatewayv1.HTTPRouteFilter{ + Type: gatewayv1.HTTPRouteFilterRequestRedirect, + RequestRedirect: nil, + }, + panic: true, + name: "nil filter", + }, { validator: createAllValidValidator(), filter: gatewayv1.HTTPRouteFilter{ @@ -1941,7 +1959,7 @@ func TestValidateFilterRedirect(t *testing.T) { { validator: func() *validationfakes.FakeHTTPFieldsValidator { validator := createAllValidValidator() - validator.ValidateRedirectHostnameReturns(errors.New("invalid hostname")) + validator.ValidateHostnameReturns(errors.New("invalid hostname")) return validator }(), filter: gatewayv1.HTTPRouteFilter{ @@ -1999,7 +2017,7 @@ func TestValidateFilterRedirect(t *testing.T) { { validator: func() *validationfakes.FakeHTTPFieldsValidator { validator := createAllValidValidator() - validator.ValidateRedirectHostnameReturns(errors.New("invalid hostname")) + validator.ValidateHostnameReturns(errors.New("invalid hostname")) validator.ValidateRedirectPortReturns(errors.New("invalid port")) return validator }(), @@ -2024,8 +2042,164 @@ func TestValidateFilterRedirect(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { g := NewWithT(t) - allErrs := validateFilterRedirect(test.validator, test.filter, filterPath) - g.Expect(allErrs).To(HaveLen(test.expectErrCount)) + if test.panic { + validate := func() { + _ = validateFilterRedirect(test.validator, test.filter, filterPath) + } + g.Expect(validate).To(Panic()) + } else { + allErrs := validateFilterRedirect(test.validator, test.filter, filterPath) + g.Expect(allErrs).To(HaveLen(test.expectErrCount)) + } + }) + } +} + +func TestValidateFilterRewrite(t *testing.T) { + tests := []struct { + filter gatewayv1.HTTPRouteFilter + validator *validationfakes.FakeHTTPFieldsValidator + name string + expectErrCount int + panic bool + }{ + { + validator: &validationfakes.FakeHTTPFieldsValidator{}, + filter: gatewayv1.HTTPRouteFilter{ + Type: gatewayv1.HTTPRouteFilterURLRewrite, + URLRewrite: nil, + }, + panic: true, + name: "nil filter", + }, + { + validator: &validationfakes.FakeHTTPFieldsValidator{}, + filter: gatewayv1.HTTPRouteFilter{ + Type: gatewayv1.HTTPRouteFilterURLRewrite, + URLRewrite: &gatewayv1.HTTPURLRewriteFilter{ + Hostname: helpers.GetPointer[gatewayv1.PreciseHostname]("example.com"), + Path: &gatewayv1.HTTPPathModifier{ + Type: gatewayv1.FullPathHTTPPathModifier, + ReplaceFullPath: helpers.GetPointer("/path"), + }, + }, + }, + expectErrCount: 0, + name: "valid rewrite filter", + }, + { + validator: &validationfakes.FakeHTTPFieldsValidator{}, + filter: gatewayv1.HTTPRouteFilter{ + Type: gatewayv1.HTTPRouteFilterURLRewrite, + URLRewrite: &gatewayv1.HTTPURLRewriteFilter{}, + }, + expectErrCount: 0, + name: "valid rewrite filter with no fields set", + }, + { + validator: func() *validationfakes.FakeHTTPFieldsValidator { + validator := &validationfakes.FakeHTTPFieldsValidator{} + validator.ValidateHostnameReturns(errors.New("invalid hostname")) + return validator + }(), + filter: gatewayv1.HTTPRouteFilter{ + Type: gatewayv1.HTTPRouteFilterURLRewrite, + URLRewrite: &gatewayv1.HTTPURLRewriteFilter{ + Hostname: helpers.GetPointer[gatewayv1.PreciseHostname]( + "example.com", + ), // any value is invalid by the validator + }, + }, + expectErrCount: 1, + name: "rewrite filter with invalid hostname", + }, + { + validator: &validationfakes.FakeHTTPFieldsValidator{}, + filter: gatewayv1.HTTPRouteFilter{ + Type: gatewayv1.HTTPRouteFilterURLRewrite, + URLRewrite: &gatewayv1.HTTPURLRewriteFilter{ + Path: &gatewayv1.HTTPPathModifier{ + Type: "bad-type", + }, + }, + }, + expectErrCount: 1, + name: "rewrite filter with invalid path type", + }, + { + validator: func() *validationfakes.FakeHTTPFieldsValidator { + validator := &validationfakes.FakeHTTPFieldsValidator{} + validator.ValidateRewritePathReturns(errors.New("invalid path value")) + return validator + }(), + filter: gatewayv1.HTTPRouteFilter{ + Type: gatewayv1.HTTPRouteFilterURLRewrite, + URLRewrite: &gatewayv1.HTTPURLRewriteFilter{ + Path: &gatewayv1.HTTPPathModifier{ + Type: gatewayv1.FullPathHTTPPathModifier, + ReplaceFullPath: helpers.GetPointer("/path"), + }, // any value is invalid by the validator + }, + }, + expectErrCount: 1, + name: "rewrite filter with invalid full path", + }, + { + validator: func() *validationfakes.FakeHTTPFieldsValidator { + validator := &validationfakes.FakeHTTPFieldsValidator{} + validator.ValidateRewritePathReturns(errors.New("invalid path")) + return validator + }(), + filter: gatewayv1.HTTPRouteFilter{ + Type: gatewayv1.HTTPRouteFilterURLRewrite, + URLRewrite: &gatewayv1.HTTPURLRewriteFilter{ + Path: &gatewayv1.HTTPPathModifier{ + Type: gatewayv1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: helpers.GetPointer("/path"), + }, // any value is invalid by the validator + }, + }, + expectErrCount: 1, + name: "rewrite filter with invalid prefix path", + }, + { + validator: func() *validationfakes.FakeHTTPFieldsValidator { + validator := &validationfakes.FakeHTTPFieldsValidator{} + validator.ValidateHostnameReturns(errors.New("invalid hostname")) + validator.ValidateRewritePathReturns(errors.New("invalid path")) + return validator + }(), + filter: gatewayv1.HTTPRouteFilter{ + Type: gatewayv1.HTTPRouteFilterURLRewrite, + URLRewrite: &gatewayv1.HTTPURLRewriteFilter{ + Hostname: helpers.GetPointer[gatewayv1.PreciseHostname]( + "example.com", + ), // any value is invalid by the validator + Path: &gatewayv1.HTTPPathModifier{ + Type: gatewayv1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: helpers.GetPointer("/path"), + }, // any value is invalid by the validator + }, + }, + expectErrCount: 2, + name: "rewrite filter with multiple errors", + }, + } + + filterPath := field.NewPath("test") + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + if test.panic { + validate := func() { + _ = validateFilterRewrite(test.validator, test.filter, filterPath) + } + g.Expect(validate).To(Panic()) + } else { + allErrs := validateFilterRewrite(test.validator, test.filter, filterPath) + g.Expect(allErrs).To(HaveLen(test.expectErrCount)) + } }) } } diff --git a/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go b/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go index 4a82f1c08c..05b4c620b6 100644 --- a/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go +++ b/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go @@ -30,6 +30,17 @@ type FakeHTTPFieldsValidator struct { validateHeaderValueInMatchReturnsOnCall map[int]struct { result1 error } + ValidateHostnameStub func(string) error + validateHostnameMutex sync.RWMutex + validateHostnameArgsForCall []struct { + arg1 string + } + validateHostnameReturns struct { + result1 error + } + validateHostnameReturnsOnCall map[int]struct { + result1 error + } ValidateMethodInMatchStub func(string) (bool, []string) validateMethodInMatchMutex sync.RWMutex validateMethodInMatchArgsForCall []struct { @@ -76,17 +87,6 @@ type FakeHTTPFieldsValidator struct { validateQueryParamValueInMatchReturnsOnCall map[int]struct { result1 error } - ValidateRedirectHostnameStub func(string) error - validateRedirectHostnameMutex sync.RWMutex - validateRedirectHostnameArgsForCall []struct { - arg1 string - } - validateRedirectHostnameReturns struct { - result1 error - } - validateRedirectHostnameReturnsOnCall map[int]struct { - result1 error - } ValidateRedirectPortStub func(int32) error validateRedirectPortMutex sync.RWMutex validateRedirectPortArgsForCall []struct { @@ -146,6 +146,17 @@ type FakeHTTPFieldsValidator struct { validateRequestHeaderValueReturnsOnCall map[int]struct { result1 error } + ValidateRewritePathStub func(string) error + validateRewritePathMutex sync.RWMutex + validateRewritePathArgsForCall []struct { + arg1 string + } + validateRewritePathReturns struct { + result1 error + } + validateRewritePathReturnsOnCall map[int]struct { + result1 error + } invocations map[string][][]interface{} invocationsMutex sync.RWMutex } @@ -272,6 +283,67 @@ func (fake *FakeHTTPFieldsValidator) ValidateHeaderValueInMatchReturnsOnCall(i i }{result1} } +func (fake *FakeHTTPFieldsValidator) ValidateHostname(arg1 string) error { + fake.validateHostnameMutex.Lock() + ret, specificReturn := fake.validateHostnameReturnsOnCall[len(fake.validateHostnameArgsForCall)] + fake.validateHostnameArgsForCall = append(fake.validateHostnameArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.ValidateHostnameStub + fakeReturns := fake.validateHostnameReturns + fake.recordInvocation("ValidateHostname", []interface{}{arg1}) + fake.validateHostnameMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeHTTPFieldsValidator) ValidateHostnameCallCount() int { + fake.validateHostnameMutex.RLock() + defer fake.validateHostnameMutex.RUnlock() + return len(fake.validateHostnameArgsForCall) +} + +func (fake *FakeHTTPFieldsValidator) ValidateHostnameCalls(stub func(string) error) { + fake.validateHostnameMutex.Lock() + defer fake.validateHostnameMutex.Unlock() + fake.ValidateHostnameStub = stub +} + +func (fake *FakeHTTPFieldsValidator) ValidateHostnameArgsForCall(i int) string { + fake.validateHostnameMutex.RLock() + defer fake.validateHostnameMutex.RUnlock() + argsForCall := fake.validateHostnameArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeHTTPFieldsValidator) ValidateHostnameReturns(result1 error) { + fake.validateHostnameMutex.Lock() + defer fake.validateHostnameMutex.Unlock() + fake.ValidateHostnameStub = nil + fake.validateHostnameReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeHTTPFieldsValidator) ValidateHostnameReturnsOnCall(i int, result1 error) { + fake.validateHostnameMutex.Lock() + defer fake.validateHostnameMutex.Unlock() + fake.ValidateHostnameStub = nil + if fake.validateHostnameReturnsOnCall == nil { + fake.validateHostnameReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.validateHostnameReturnsOnCall[i] = struct { + result1 error + }{result1} +} + func (fake *FakeHTTPFieldsValidator) ValidateMethodInMatch(arg1 string) (bool, []string) { fake.validateMethodInMatchMutex.Lock() ret, specificReturn := fake.validateMethodInMatchReturnsOnCall[len(fake.validateMethodInMatchArgsForCall)] @@ -519,67 +591,6 @@ func (fake *FakeHTTPFieldsValidator) ValidateQueryParamValueInMatchReturnsOnCall }{result1} } -func (fake *FakeHTTPFieldsValidator) ValidateRedirectHostname(arg1 string) error { - fake.validateRedirectHostnameMutex.Lock() - ret, specificReturn := fake.validateRedirectHostnameReturnsOnCall[len(fake.validateRedirectHostnameArgsForCall)] - fake.validateRedirectHostnameArgsForCall = append(fake.validateRedirectHostnameArgsForCall, struct { - arg1 string - }{arg1}) - stub := fake.ValidateRedirectHostnameStub - fakeReturns := fake.validateRedirectHostnameReturns - fake.recordInvocation("ValidateRedirectHostname", []interface{}{arg1}) - fake.validateRedirectHostnameMutex.Unlock() - if stub != nil { - return stub(arg1) - } - if specificReturn { - return ret.result1 - } - return fakeReturns.result1 -} - -func (fake *FakeHTTPFieldsValidator) ValidateRedirectHostnameCallCount() int { - fake.validateRedirectHostnameMutex.RLock() - defer fake.validateRedirectHostnameMutex.RUnlock() - return len(fake.validateRedirectHostnameArgsForCall) -} - -func (fake *FakeHTTPFieldsValidator) ValidateRedirectHostnameCalls(stub func(string) error) { - fake.validateRedirectHostnameMutex.Lock() - defer fake.validateRedirectHostnameMutex.Unlock() - fake.ValidateRedirectHostnameStub = stub -} - -func (fake *FakeHTTPFieldsValidator) ValidateRedirectHostnameArgsForCall(i int) string { - fake.validateRedirectHostnameMutex.RLock() - defer fake.validateRedirectHostnameMutex.RUnlock() - argsForCall := fake.validateRedirectHostnameArgsForCall[i] - return argsForCall.arg1 -} - -func (fake *FakeHTTPFieldsValidator) ValidateRedirectHostnameReturns(result1 error) { - fake.validateRedirectHostnameMutex.Lock() - defer fake.validateRedirectHostnameMutex.Unlock() - fake.ValidateRedirectHostnameStub = nil - fake.validateRedirectHostnameReturns = struct { - result1 error - }{result1} -} - -func (fake *FakeHTTPFieldsValidator) ValidateRedirectHostnameReturnsOnCall(i int, result1 error) { - fake.validateRedirectHostnameMutex.Lock() - defer fake.validateRedirectHostnameMutex.Unlock() - fake.ValidateRedirectHostnameStub = nil - if fake.validateRedirectHostnameReturnsOnCall == nil { - fake.validateRedirectHostnameReturnsOnCall = make(map[int]struct { - result1 error - }) - } - fake.validateRedirectHostnameReturnsOnCall[i] = struct { - result1 error - }{result1} -} - func (fake *FakeHTTPFieldsValidator) ValidateRedirectPort(arg1 int32) error { fake.validateRedirectPortMutex.Lock() ret, specificReturn := fake.validateRedirectPortReturnsOnCall[len(fake.validateRedirectPortArgsForCall)] @@ -891,6 +902,67 @@ func (fake *FakeHTTPFieldsValidator) ValidateRequestHeaderValueReturnsOnCall(i i }{result1} } +func (fake *FakeHTTPFieldsValidator) ValidateRewritePath(arg1 string) error { + fake.validateRewritePathMutex.Lock() + ret, specificReturn := fake.validateRewritePathReturnsOnCall[len(fake.validateRewritePathArgsForCall)] + fake.validateRewritePathArgsForCall = append(fake.validateRewritePathArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.ValidateRewritePathStub + fakeReturns := fake.validateRewritePathReturns + fake.recordInvocation("ValidateRewritePath", []interface{}{arg1}) + fake.validateRewritePathMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeHTTPFieldsValidator) ValidateRewritePathCallCount() int { + fake.validateRewritePathMutex.RLock() + defer fake.validateRewritePathMutex.RUnlock() + return len(fake.validateRewritePathArgsForCall) +} + +func (fake *FakeHTTPFieldsValidator) ValidateRewritePathCalls(stub func(string) error) { + fake.validateRewritePathMutex.Lock() + defer fake.validateRewritePathMutex.Unlock() + fake.ValidateRewritePathStub = stub +} + +func (fake *FakeHTTPFieldsValidator) ValidateRewritePathArgsForCall(i int) string { + fake.validateRewritePathMutex.RLock() + defer fake.validateRewritePathMutex.RUnlock() + argsForCall := fake.validateRewritePathArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeHTTPFieldsValidator) ValidateRewritePathReturns(result1 error) { + fake.validateRewritePathMutex.Lock() + defer fake.validateRewritePathMutex.Unlock() + fake.ValidateRewritePathStub = nil + fake.validateRewritePathReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeHTTPFieldsValidator) ValidateRewritePathReturnsOnCall(i int, result1 error) { + fake.validateRewritePathMutex.Lock() + defer fake.validateRewritePathMutex.Unlock() + fake.ValidateRewritePathStub = nil + if fake.validateRewritePathReturnsOnCall == nil { + fake.validateRewritePathReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.validateRewritePathReturnsOnCall[i] = struct { + result1 error + }{result1} +} + func (fake *FakeHTTPFieldsValidator) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() @@ -898,6 +970,8 @@ func (fake *FakeHTTPFieldsValidator) Invocations() map[string][][]interface{} { defer fake.validateHeaderNameInMatchMutex.RUnlock() fake.validateHeaderValueInMatchMutex.RLock() defer fake.validateHeaderValueInMatchMutex.RUnlock() + fake.validateHostnameMutex.RLock() + defer fake.validateHostnameMutex.RUnlock() fake.validateMethodInMatchMutex.RLock() defer fake.validateMethodInMatchMutex.RUnlock() fake.validatePathInMatchMutex.RLock() @@ -906,8 +980,6 @@ func (fake *FakeHTTPFieldsValidator) Invocations() map[string][][]interface{} { defer fake.validateQueryParamNameInMatchMutex.RUnlock() fake.validateQueryParamValueInMatchMutex.RLock() defer fake.validateQueryParamValueInMatchMutex.RUnlock() - fake.validateRedirectHostnameMutex.RLock() - defer fake.validateRedirectHostnameMutex.RUnlock() fake.validateRedirectPortMutex.RLock() defer fake.validateRedirectPortMutex.RUnlock() fake.validateRedirectSchemeMutex.RLock() @@ -918,6 +990,8 @@ func (fake *FakeHTTPFieldsValidator) Invocations() map[string][][]interface{} { defer fake.validateRequestHeaderNameMutex.RUnlock() fake.validateRequestHeaderValueMutex.RLock() defer fake.validateRequestHeaderValueMutex.RUnlock() + fake.validateRewritePathMutex.RLock() + defer fake.validateRewritePathMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} for key, value := range fake.invocations { copiedInvocations[key] = value diff --git a/internal/mode/static/state/validation/validator.go b/internal/mode/static/state/validation/validator.go index d1ff623853..d6433ad363 100644 --- a/internal/mode/static/state/validation/validator.go +++ b/internal/mode/static/state/validation/validator.go @@ -20,9 +20,10 @@ type HTTPFieldsValidator interface { ValidateQueryParamValueInMatch(name string) error ValidateMethodInMatch(method string) (valid bool, supportedValues []string) ValidateRedirectScheme(scheme string) (valid bool, supportedValues []string) - ValidateRedirectHostname(hostname string) error ValidateRedirectPort(port int32) error ValidateRedirectStatusCode(statusCode int) (valid bool, supportedValues []string) + ValidateHostname(hostname string) error + ValidateRewritePath(path string) error ValidateRequestHeaderName(name string) error ValidateRequestHeaderValue(value string) error } diff --git a/site/content/how-to/traffic-management/https-termination.md b/site/content/how-to/traffic-management/https-termination.md new file mode 100644 index 0000000000..1ca50f025f --- /dev/null +++ b/site/content/how-to/traffic-management/https-termination.md @@ -0,0 +1,245 @@ +--- +title: "HTTPS Termination" +description: "Learn how to terminate HTTPS traffic using NGINX Gateway Fabric." +weight: 500 +toc: true +docs: "DOCS-000" +--- + +In this guide, we will show how to configure HTTPS termination for your application, using an [HTTPRoute](https://gateway-api.sigs.k8s.io/api-types/httproute/) redirect filter, secret, and [ReferenceGrant](https://gateway-api.sigs.k8s.io/api-types/referencegrant/). + +## Prerequisites + +- [Install]({{< relref "installation/" >}}) NGINX Gateway Fabric. +- [Expose NGINX Gateway Fabric]({{< relref "installation/expose-nginx-gateway-fabric.md" >}}) and save the public IP address and port of NGINX Gateway Fabric into shell variables: + + ```text + GW_IP=XXX.YYY.ZZZ.III + GW_PORT= + ``` + + Save the ports of NGINX Gateway Fabric: + + ```text + GW_HTTP_PORT= + GW_HTTPS_PORT= + ``` + +{{< note >}}In a production environment, you should have a DNS record for the external IP address that is exposed, and it should refer to the hostname that the gateway will forward for.{{< /note >}} + +## Set up + +Create the **coffee** application in Kubernetes by copying and pasting the following block into your terminal: + +```yaml +kubectl apply -f - < 80/TCP 40s +``` + +## Configure HTTPS Termination and Routing + +For the HTTPS, we need a certificate and key that are stored in a secret. This secret will live in a separate namespace, so we will need a ReferenceGrant in order to access it. + +To create the **certificate** namespace and secret, copy and paste the following into your terminal: + +```yaml +kubectl apply -f - <}}If you have a DNS record allocated for `cafe.example.com`, you can send the request directly to that hostname, without needing to resolve.{{< /note >}} + +To test that NGINX sends an HTTPS redirect, we will send requests to the `coffee` service on the HTTP port. We +will use curl's `--include` option to print the response headers (we are interested in the `Location` header). + +```shell +curl --resolve cafe.example.com:$GW_HTTP_PORT:$GW_IP http://cafe.example.com:$GW_HTTP_PORT/coffee --include +``` + +```text +HTTP/1.1 302 Moved Temporarily +... +Location: https://cafe.example.com/coffee +... +``` + +Now we will access the application over HTTPS. Since our certificate is self-signed, we will use curl's `--insecure` +option to turn off certificate verification. + +```shell +curl --resolve cafe.example.com:$GW_HTTPS_PORT:$GW_IP https://cafe.example.com:$GW_HTTPS_PORT/coffee --insecure +``` + +```text +Server address: 10.244.0.6:80 +Server name: coffee-6b8b6d6486-7fc78 +``` + +## Further Reading + +To learn more about redirects using the Gateway API, see the following resource: + +- [Gateway API Redirects](https://gateway-api.sigs.k8s.io/guides/http-redirect-rewrite/) diff --git a/site/content/how-to/traffic-management/redirects-and-rewrites.md b/site/content/how-to/traffic-management/redirects-and-rewrites.md new file mode 100644 index 0000000000..712986575b --- /dev/null +++ b/site/content/how-to/traffic-management/redirects-and-rewrites.md @@ -0,0 +1,203 @@ +--- +title: "HTTP Redirects and Rewrites" +description: "Learn how to redirect or rewrite your HTTP traffic using NGINX Gateway Fabric." +weight: 400 +toc: true +docs: "DOCS-000" +--- + +[HTTPRoute](https://gateway-api.sigs.k8s.io/api-types/httproute/) filters can be used to configure HTTP redirects or rewrites. Redirects return HTTP 3XX responses to a client, instructing it to retrieve a different resource. Rewrites modify components of a client request (such as hostname and/or path) before proxying it upstream. + +{{< note >}}NGINX Gateway Fabric currently does not support path-based redirects.{{< /note >}} + +To see an example of a redirect using scheme and port, see the [HTTPS Termination]({{< relref "/how-to/traffic-management/https-termination.md" >}}) guide. + +In this guide, we will be configuring a path URL rewrite. + +## Prerequisites + +- [Install]({{< relref "installation/" >}}) NGINX Gateway Fabric. +- [Expose NGINX Gateway Fabric]({{< relref "installation/expose-nginx-gateway-fabric.md" >}}) and save the public IP address and port of NGINX Gateway Fabric into shell variables: + + ```text + GW_IP=XXX.YYY.ZZZ.III + GW_PORT= + ``` + +{{< note >}}In a production environment, you should have a DNS record for the external IP address that is exposed, and it should refer to the hostname that the gateway will forward for.{{< /note >}} + +## Set up + +Create the **coffee** application in Kubernetes by copying and pasting the following block into your terminal: + +```yaml +kubectl apply -f - < 80/TCP 40s +``` + +## Configure a Path Rewrite + +To create the **cafe** gateway, copy and paste the following into your terminal: + +```yaml +kubectl apply -f - <}}If you have a DNS record allocated for `cafe.example.com`, you can send the request directly to that hostname, without needing to resolve.{{< /note >}} + +```shell +curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee/flavors +``` + +Notice in the output that the URI has been rewritten: + +```text +Server address: 10.244.0.6:8080 +Server name: coffee-6b8b6d6486-7fc78 +... +URI: /beans +``` + +Other examples: + +```shell +curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee +``` + +```text +Server address: 10.244.0.6:8080 +Server name: coffee-6b8b6d6486-7fc78 +... +URI: /beans +``` + +```shell +curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/latte/prices +``` + +```text +Server address: 10.244.0.6:8080 +Server name: coffee-6b8b6d6486-7fc78 +... +URI: /prices +``` + +## Further Reading + +To learn more about redirects and rewrites using the Gateway API, see the following resource: + +- [Gateway API Redirects and Rewrites](https://gateway-api.sigs.k8s.io/guides/http-redirect-rewrite/) diff --git a/site/content/how-to/traffic-management/routing-traffic-to-your-app.md b/site/content/how-to/traffic-management/routing-traffic-to-your-app.md index 0ac01e1462..fe9c778305 100644 --- a/site/content/how-to/traffic-management/routing-traffic-to-your-app.md +++ b/site/content/how-to/traffic-management/routing-traffic-to-your-app.md @@ -8,8 +8,6 @@ docs: "DOCS-000" {{}} -## Overview - You can route traffic to your Kubernetes applications using the Gateway API and NGINX Gateway Fabric. Whether you're managing a web application or a REST backend API, you can use NGINX Gateway Fabric to expose your application outside the cluster. ## Prerequisites @@ -119,7 +117,7 @@ To create the **cafe** gateway, copy and paste the following into your terminal: ```yaml kubectl apply -f - <}}) comman - `filters` - `type`: Supported. - `requestRedirect`: Supported except for the experimental `path` field. If multiple filters are configured, NGINX Gateway Fabric will choose the first and ignore the rest. - - `requestHeaderModifier`: Supported. If multiple filters are configured, NGINX Gateway Fabric will choose the first and ignore the rest. - - `responseHeaderModifier`, `requestMirror`, `urlRewrite`, `extensionRef`: Not supported. + - `requestHeaderModifier`: Supported. If multiple filters are configured, NGINX Gateway Fabric will choose the first and ignore the rest. Incompatible with `urlRewrite`. + - `urlRewrite`: Supported. If multiple filters are configured, NGINX Gateway Fabric will choose the first and ignore the rest. Incompatible with `requestHeaderModifier`. + - `responseHeaderModifier`, `requestMirror`, `extensionRef`: Not supported. - `backendRefs`: Partially supported. Backend ref `filters` are not supported. - `status` - `parents`