diff --git a/management/internals/modules/reverseproxy/reverseproxy.go b/management/internals/modules/reverseproxy/reverseproxy.go index e48b4d4ed..a832d799e 100644 --- a/management/internals/modules/reverseproxy/reverseproxy.go +++ b/management/internals/modules/reverseproxy/reverseproxy.go @@ -95,6 +95,7 @@ type ReverseProxy struct { Targets []Target `gorm:"serializer:json"` Enabled bool PassHostHeader bool + RewriteRedirects bool Auth AuthConfig `gorm:"serializer:json"` Meta ReverseProxyMeta `gorm:"embedded;embeddedPrefix:meta_"` SessionPrivateKey string `gorm:"column:session_private_key"` @@ -174,14 +175,15 @@ func (r *ReverseProxy) ToAPIResponse() *api.ReverseProxy { } resp := &api.ReverseProxy{ - Id: r.ID, - Name: r.Name, - Domain: r.Domain, - Targets: apiTargets, - Enabled: r.Enabled, - PassHostHeader: &r.PassHostHeader, - Auth: authConfig, - Meta: meta, + Id: r.ID, + Name: r.Name, + Domain: r.Domain, + Targets: apiTargets, + Enabled: r.Enabled, + PassHostHeader: &r.PassHostHeader, + RewriteRedirects: &r.RewriteRedirects, + Auth: authConfig, + Meta: meta, } if r.ProxyCluster != "" { @@ -203,6 +205,9 @@ func (r *ReverseProxy) ToProtoMapping(operation Operation, authToken string, oid path = *target.Path } + // TODO: Make path prefix stripping configurable per-target. + // Currently the matching prefix is baked into the target URL path, + // so the proxy strips-then-re-adds it (effectively a no-op). targetURL := url.URL{ Scheme: target.Protocol, Host: target.Host, @@ -236,14 +241,15 @@ func (r *ReverseProxy) ToProtoMapping(operation Operation, authToken string, oid } return &proto.ProxyMapping{ - Type: operationToProtoType(operation), - Id: r.ID, - Domain: r.Domain, - Path: pathMappings, - AuthToken: authToken, - Auth: auth, - AccountId: r.AccountID, - PassHostHeader: r.PassHostHeader, + Type: operationToProtoType(operation), + Id: r.ID, + Domain: r.Domain, + Path: pathMappings, + AuthToken: authToken, + Auth: auth, + AccountId: r.AccountID, + PassHostHeader: r.PassHostHeader, + RewriteRedirects: r.RewriteRedirects, } } @@ -288,6 +294,10 @@ func (r *ReverseProxy) FromAPIRequest(req *api.ReverseProxyRequest, accountID st r.PassHostHeader = *req.PassHostHeader } + if req.RewriteRedirects != nil { + r.RewriteRedirects = *req.RewriteRedirects + } + if req.Auth.PasswordAuth != nil { r.Auth.PasswordAuth = &PasswordAuthConfig{ Enabled: req.Auth.PasswordAuth.Enabled, @@ -358,6 +368,7 @@ func (r *ReverseProxy) Copy() *ReverseProxy { Targets: targets, Enabled: r.Enabled, PassHostHeader: r.PassHostHeader, + RewriteRedirects: r.RewriteRedirects, Auth: r.Auth, Meta: r.Meta, SessionPrivateKey: r.SessionPrivateKey, diff --git a/proxy/internal/proxy/reverseproxy.go b/proxy/internal/proxy/reverseproxy.go index 8f73e9d59..72cc6c03b 100644 --- a/proxy/internal/proxy/reverseproxy.go +++ b/proxy/internal/proxy/reverseproxy.go @@ -3,6 +3,7 @@ package proxy import ( "context" "errors" + "fmt" "net" "net/http" "net/http/httputil" @@ -84,6 +85,9 @@ func (p *ReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { Transport: p.transport, ErrorHandler: proxyErrorHandler, } + if result.rewriteRedirects { + rp.ModifyResponse = p.rewriteLocationFunc(result.url, result.matchedPath, r) + } rp.ServeHTTP(w, r.WithContext(ctx)) } @@ -124,6 +128,62 @@ func (p *ReverseProxy) rewriteFunc(target *url.URL, matchedPath string, passHost } } +// rewriteLocationFunc returns a ModifyResponse function that rewrites Location +// headers in backend responses when they point to the backend's address, +// replacing them with the public-facing host and scheme. +func (p *ReverseProxy) rewriteLocationFunc(target *url.URL, matchedPath string, inReq *http.Request) func(*http.Response) error { + publicHost := inReq.Host + publicScheme := auth.ResolveProto(p.forwardedProto, inReq.TLS) + + return func(resp *http.Response) error { + location := resp.Header.Get("Location") + if location == "" { + return nil + } + + locURL, err := url.Parse(location) + if err != nil { + return fmt.Errorf("parse Location header %q: %w", location, err) + } + + // Only rewrite absolute URLs that point to the backend. + if locURL.Host == "" || !hostsEqual(locURL, target) { + return nil + } + + locURL.Host = publicHost + locURL.Scheme = publicScheme + + // Re-add the stripped path prefix so the client reaches the correct route. + // TrimRight prevents double slashes when matchedPath has a trailing slash. + if matchedPath != "" && matchedPath != "/" { + locURL.Path = strings.TrimRight(matchedPath, "/") + "/" + strings.TrimLeft(locURL.Path, "/") + } + + resp.Header.Set("Location", locURL.String()) + return nil + } +} + +// hostsEqual compares two URL authorities, normalizing default ports per +// RFC 3986 Section 6.2.3 (https://443 == https, http://80 == http). +func hostsEqual(a, b *url.URL) bool { + return normalizeHost(a) == normalizeHost(b) +} + +// normalizeHost strips the port from a URL's Host field if it matches the +// scheme's default port (443 for https, 80 for http). +func normalizeHost(u *url.URL) string { + host, port, err := net.SplitHostPort(u.Host) + if err != nil { + return u.Host + } + if (u.Scheme == "https" && port == "443") || (u.Scheme == "http" && port == "80") { + return host + } + return u.Host +} + // setTrustedForwardingHeaders appends to the existing forwarding header chain // and preserves upstream-provided headers when the direct connection is from // a trusted proxy. diff --git a/proxy/internal/proxy/reverseproxy_test.go b/proxy/internal/proxy/reverseproxy_test.go index 06a2c6a26..1b0c00688 100644 --- a/proxy/internal/proxy/reverseproxy_test.go +++ b/proxy/internal/proxy/reverseproxy_test.go @@ -470,6 +470,329 @@ func TestRewriteFunc_TrustedProxy(t *testing.T) { }) } +// TestRewriteFunc_PathForwarding verifies what path the backend actually +// receives given different configurations. This simulates the full pipeline: +// management builds a target URL (with matching prefix baked into the path), +// then the proxy strips the prefix and SetURL re-joins with the target path. +func TestRewriteFunc_PathForwarding(t *testing.T) { + p := &ReverseProxy{forwardedProto: "auto"} + + // Simulate what ToProtoMapping does: target URL includes the matching + // prefix as its path component, so the proxy strips-then-re-adds. + t.Run("path prefix baked into target URL is a no-op", func(t *testing.T) { + // Management builds: path="/heise", target="https://heise.de:443/heise" + target, _ := url.Parse("https://heise.de:443/heise") + rewrite := p.rewriteFunc(target, "/heise", false) + pr := newProxyRequest(t, "http://external.test/heise", "1.2.3.4:5000") + + rewrite(pr) + + assert.Equal(t, "/heise/", pr.Out.URL.Path, + "backend sees /heise/ because prefix is stripped then re-added by SetURL") + }) + + t.Run("subpath under prefix also preserved", func(t *testing.T) { + target, _ := url.Parse("https://heise.de:443/heise") + rewrite := p.rewriteFunc(target, "/heise", false) + pr := newProxyRequest(t, "http://external.test/heise/article/123", "1.2.3.4:5000") + + rewrite(pr) + + assert.Equal(t, "/heise/article/123", pr.Out.URL.Path, + "subpath is preserved on top of the re-added prefix") + }) + + // What the behavior WOULD be if target URL had no path (true stripping) + t.Run("target without path prefix gives true stripping", func(t *testing.T) { + target, _ := url.Parse("https://heise.de:443") + rewrite := p.rewriteFunc(target, "/heise", false) + pr := newProxyRequest(t, "http://external.test/heise", "1.2.3.4:5000") + + rewrite(pr) + + assert.Equal(t, "/", pr.Out.URL.Path, + "without path in target URL, backend sees / (true prefix stripping)") + }) + + t.Run("target without path prefix strips and preserves subpath", func(t *testing.T) { + target, _ := url.Parse("https://heise.de:443") + rewrite := p.rewriteFunc(target, "/heise", false) + pr := newProxyRequest(t, "http://external.test/heise/article/123", "1.2.3.4:5000") + + rewrite(pr) + + assert.Equal(t, "/article/123", pr.Out.URL.Path, + "without path in target URL, prefix is truly stripped") + }) + + // Root path "/" — no stripping expected + t.Run("root path forwards full request path unchanged", func(t *testing.T) { + target, _ := url.Parse("https://backend.example.com:443/") + rewrite := p.rewriteFunc(target, "/", false) + pr := newProxyRequest(t, "http://external.test/heise", "1.2.3.4:5000") + + rewrite(pr) + + assert.Equal(t, "/heise", pr.Out.URL.Path, + "root path match must not strip anything") + }) +} + +func TestRewriteLocationFunc(t *testing.T) { + target, _ := url.Parse("http://backend.internal:8080") + newProxy := func(proto string) *ReverseProxy { return &ReverseProxy{forwardedProto: proto} } + newReq := func(rawURL string) *http.Request { + t.Helper() + r := httptest.NewRequest(http.MethodGet, rawURL, nil) + parsed, _ := url.Parse(rawURL) + r.Host = parsed.Host + return r + } + run := func(p *ReverseProxy, matchedPath string, inReq *http.Request, location string) (*http.Response, error) { + t.Helper() + modifyResp := p.rewriteLocationFunc(target, matchedPath, inReq) + resp := &http.Response{Header: http.Header{}} + if location != "" { + resp.Header.Set("Location", location) + } + err := modifyResp(resp) + return resp, err + } + + t.Run("rewrites Location pointing to backend", func(t *testing.T) { + resp, err := run(newProxy("https"), "", newReq("https://public.example.com/page"), + "http://backend.internal:8080/login") + + require.NoError(t, err) + assert.Equal(t, "https://public.example.com/login", resp.Header.Get("Location")) + }) + + t.Run("does not rewrite Location pointing to other host", func(t *testing.T) { + resp, err := run(newProxy("https"), "", newReq("https://public.example.com/"), + "https://other.example.com/path") + + require.NoError(t, err) + assert.Equal(t, "https://other.example.com/path", resp.Header.Get("Location")) + }) + + t.Run("does not rewrite relative Location", func(t *testing.T) { + resp, err := run(newProxy("https"), "", newReq("https://public.example.com/"), + "/dashboard") + + require.NoError(t, err) + assert.Equal(t, "/dashboard", resp.Header.Get("Location")) + }) + + t.Run("re-adds stripped path prefix", func(t *testing.T) { + resp, err := run(newProxy("https"), "/api", newReq("https://public.example.com/api/users"), + "http://backend.internal:8080/users") + + require.NoError(t, err) + assert.Equal(t, "https://public.example.com/api/users", resp.Header.Get("Location")) + }) + + t.Run("uses resolved proto for scheme", func(t *testing.T) { + resp, err := run(newProxy("auto"), "", newReq("http://public.example.com/"), + "http://backend.internal:8080/path") + + require.NoError(t, err) + assert.Equal(t, "http://public.example.com/path", resp.Header.Get("Location")) + }) + + t.Run("no-op when Location header is empty", func(t *testing.T) { + resp, err := run(newProxy("https"), "", newReq("https://public.example.com/"), "") + + require.NoError(t, err) + assert.Empty(t, resp.Header.Get("Location")) + }) + + t.Run("does not prepend root path prefix", func(t *testing.T) { + resp, err := run(newProxy("https"), "/", newReq("https://public.example.com/login"), + "http://backend.internal:8080/login") + + require.NoError(t, err) + assert.Equal(t, "https://public.example.com/login", resp.Header.Get("Location")) + }) + + // --- Edge cases: query parameters and fragments --- + + t.Run("preserves query parameters", func(t *testing.T) { + resp, err := run(newProxy("https"), "", newReq("https://public.example.com/"), + "http://backend.internal:8080/login?redirect=%2Fdashboard&lang=en") + + require.NoError(t, err) + assert.Equal(t, "https://public.example.com/login?redirect=%2Fdashboard&lang=en", resp.Header.Get("Location")) + }) + + t.Run("preserves fragment", func(t *testing.T) { + resp, err := run(newProxy("https"), "", newReq("https://public.example.com/"), + "http://backend.internal:8080/docs#section-2") + + require.NoError(t, err) + assert.Equal(t, "https://public.example.com/docs#section-2", resp.Header.Get("Location")) + }) + + t.Run("preserves query parameters and fragment together", func(t *testing.T) { + resp, err := run(newProxy("https"), "", newReq("https://public.example.com/"), + "http://backend.internal:8080/search?q=test&page=1#results") + + require.NoError(t, err) + assert.Equal(t, "https://public.example.com/search?q=test&page=1#results", resp.Header.Get("Location")) + }) + + t.Run("preserves query parameters with path prefix re-added", func(t *testing.T) { + resp, err := run(newProxy("https"), "/api", newReq("https://public.example.com/api/search"), + "http://backend.internal:8080/search?q=hello") + + require.NoError(t, err) + assert.Equal(t, "https://public.example.com/api/search?q=hello", resp.Header.Get("Location")) + }) + + // --- Edge cases: slash handling --- + + t.Run("no double slash when matchedPath has trailing slash", func(t *testing.T) { + resp, err := run(newProxy("https"), "/api/", newReq("https://public.example.com/api/users"), + "http://backend.internal:8080/users") + + require.NoError(t, err) + assert.Equal(t, "https://public.example.com/api/users", resp.Header.Get("Location")) + }) + + t.Run("backend redirect to root with path prefix", func(t *testing.T) { + resp, err := run(newProxy("https"), "/app", newReq("https://public.example.com/app/"), + "http://backend.internal:8080/") + + require.NoError(t, err) + assert.Equal(t, "https://public.example.com/app/", resp.Header.Get("Location")) + }) + + t.Run("backend redirect to root with trailing-slash path prefix", func(t *testing.T) { + resp, err := run(newProxy("https"), "/app/", newReq("https://public.example.com/app/"), + "http://backend.internal:8080/") + + require.NoError(t, err) + assert.Equal(t, "https://public.example.com/app/", resp.Header.Get("Location")) + }) + + t.Run("preserves trailing slash on redirect path", func(t *testing.T) { + resp, err := run(newProxy("https"), "", newReq("https://public.example.com/"), + "http://backend.internal:8080/path/") + + require.NoError(t, err) + assert.Equal(t, "https://public.example.com/path/", resp.Header.Get("Location")) + }) + + t.Run("backend redirect to bare root", func(t *testing.T) { + resp, err := run(newProxy("https"), "", newReq("https://public.example.com/page"), + "http://backend.internal:8080/") + + require.NoError(t, err) + assert.Equal(t, "https://public.example.com/", resp.Header.Get("Location")) + }) + + // --- Edge cases: host/port matching --- + + t.Run("does not rewrite when backend host matches but port differs", func(t *testing.T) { + resp, err := run(newProxy("https"), "", newReq("https://public.example.com/"), + "http://backend.internal:9090/other") + + require.NoError(t, err) + assert.Equal(t, "http://backend.internal:9090/other", resp.Header.Get("Location"), + "Different port means different host authority, must not rewrite") + }) + + t.Run("rewrites when redirect omits default port matching target", func(t *testing.T) { + // Target is backend.internal:8080, redirect is to backend.internal (no port). + // These are different authorities, so should NOT rewrite. + resp, err := run(newProxy("https"), "", newReq("https://public.example.com/"), + "http://backend.internal/path") + + require.NoError(t, err) + assert.Equal(t, "http://backend.internal/path", resp.Header.Get("Location"), + "backend.internal != backend.internal:8080, must not rewrite") + }) + + t.Run("rewrites when target has :443 but redirect omits it for https", func(t *testing.T) { + // Target: heise.de:443, redirect: https://heise.de/path (no :443 because it's default) + // Per RFC 3986, these are the same authority. + target443, _ := url.Parse("https://heise.de:443") + p := newProxy("https") + modifyResp := p.rewriteLocationFunc(target443, "", newReq("https://public.example.com/")) + resp := &http.Response{Header: http.Header{}} + resp.Header.Set("Location", "https://heise.de/path") + + err := modifyResp(resp) + + require.NoError(t, err) + assert.Equal(t, "https://public.example.com/path", resp.Header.Get("Location"), + "heise.de:443 and heise.de are the same for https") + }) + + t.Run("rewrites when target has :80 but redirect omits it for http", func(t *testing.T) { + target80, _ := url.Parse("http://backend.local:80") + p := newProxy("http") + modifyResp := p.rewriteLocationFunc(target80, "", newReq("http://public.example.com/")) + resp := &http.Response{Header: http.Header{}} + resp.Header.Set("Location", "http://backend.local/path") + + err := modifyResp(resp) + + require.NoError(t, err) + assert.Equal(t, "http://public.example.com/path", resp.Header.Get("Location"), + "backend.local:80 and backend.local are the same for http") + }) + + t.Run("rewrites when redirect has :443 but target omits it", func(t *testing.T) { + targetNoPort, _ := url.Parse("https://heise.de") + p := newProxy("https") + modifyResp := p.rewriteLocationFunc(targetNoPort, "", newReq("https://public.example.com/")) + resp := &http.Response{Header: http.Header{}} + resp.Header.Set("Location", "https://heise.de:443/path") + + err := modifyResp(resp) + + require.NoError(t, err) + assert.Equal(t, "https://public.example.com/path", resp.Header.Get("Location"), + "heise.de and heise.de:443 are the same for https") + }) + + t.Run("does not conflate non-default ports", func(t *testing.T) { + target8443, _ := url.Parse("https://backend.internal:8443") + p := newProxy("https") + modifyResp := p.rewriteLocationFunc(target8443, "", newReq("https://public.example.com/")) + resp := &http.Response{Header: http.Header{}} + resp.Header.Set("Location", "https://backend.internal/path") + + err := modifyResp(resp) + + require.NoError(t, err) + assert.Equal(t, "https://backend.internal/path", resp.Header.Get("Location"), + "backend.internal:8443 != backend.internal (port 443), must not rewrite") + }) + + // --- Edge cases: encoded paths --- + + t.Run("preserves percent-encoded path segments", func(t *testing.T) { + resp, err := run(newProxy("https"), "", newReq("https://public.example.com/"), + "http://backend.internal:8080/path%20with%20spaces/file%2Fname") + + require.NoError(t, err) + loc := resp.Header.Get("Location") + assert.Contains(t, loc, "public.example.com") + parsed, err := url.Parse(loc) + require.NoError(t, err) + assert.Equal(t, "/path with spaces/file/name", parsed.Path) + }) + + t.Run("preserves encoded query parameters with path prefix", func(t *testing.T) { + resp, err := run(newProxy("https"), "/v1", newReq("https://public.example.com/v1/"), + "http://backend.internal:8080/redirect?url=http%3A%2F%2Fexample.com") + + require.NoError(t, err) + assert.Equal(t, "https://public.example.com/v1/redirect?url=http%3A%2F%2Fexample.com", resp.Header.Get("Location")) + }) +} + // newProxyRequest creates an httputil.ProxyRequest suitable for testing // the Rewrite function. It simulates what httputil.ReverseProxy does internally: // Out is a shallow clone of In with headers copied. diff --git a/proxy/internal/proxy/servicemapping.go b/proxy/internal/proxy/servicemapping.go index cff2963bd..6f5829ebb 100644 --- a/proxy/internal/proxy/servicemapping.go +++ b/proxy/internal/proxy/servicemapping.go @@ -11,19 +11,21 @@ import ( ) type Mapping struct { - ID string - AccountID types.AccountID - Host string - Paths map[string]*url.URL - PassHostHeader bool + ID string + AccountID types.AccountID + Host string + Paths map[string]*url.URL + PassHostHeader bool + RewriteRedirects bool } type targetResult struct { - url *url.URL - matchedPath string - serviceID string - accountID types.AccountID - passHostHeader bool + url *url.URL + matchedPath string + serviceID string + accountID types.AccountID + passHostHeader bool + rewriteRedirects bool } func (p *ReverseProxy) findTargetForRequest(req *http.Request) (targetResult, bool) { @@ -56,11 +58,12 @@ func (p *ReverseProxy) findTargetForRequest(req *http.Request) (targetResult, bo target := m.Paths[path] p.logger.Debugf("matched host: %s, path: %s -> %s", host, path, target) return targetResult{ - url: target, - matchedPath: path, - serviceID: m.ID, - accountID: m.AccountID, - passHostHeader: m.PassHostHeader, + url: target, + matchedPath: path, + serviceID: m.ID, + accountID: m.AccountID, + passHostHeader: m.PassHostHeader, + rewriteRedirects: m.RewriteRedirects, }, true } } diff --git a/proxy/server.go b/proxy/server.go index 06012938a..67a13a7dd 100644 --- a/proxy/server.go +++ b/proxy/server.go @@ -491,11 +491,12 @@ func (s *Server) protoToMapping(mapping *proto.ProxyMapping) proxy.Mapping { paths[pathMapping.GetPath()] = targetURL } return proxy.Mapping{ - ID: mapping.GetId(), - AccountID: types.AccountID(mapping.GetAccountId()), - Host: mapping.GetDomain(), - Paths: paths, - PassHostHeader: mapping.GetPassHostHeader(), + ID: mapping.GetId(), + AccountID: types.AccountID(mapping.GetAccountId()), + Host: mapping.GetDomain(), + Paths: paths, + PassHostHeader: mapping.GetPassHostHeader(), + RewriteRedirects: mapping.GetRewriteRedirects(), } } diff --git a/shared/management/http/api/openapi.yml b/shared/management/http/api/openapi.yml index 7d3a95a5c..bcd67da1b 100644 --- a/shared/management/http/api/openapi.yml +++ b/shared/management/http/api/openapi.yml @@ -2865,6 +2865,9 @@ components: pass_host_header: type: boolean description: When true, the original client Host header is passed through to the backend instead of being rewritten to the backend's address + rewrite_redirects: + type: boolean + description: When true, Location headers in backend responses are rewritten to replace the backend address with the public-facing domain auth: $ref: '#/components/schemas/ReverseProxyAuthConfig' meta: @@ -2925,6 +2928,9 @@ components: pass_host_header: type: boolean description: When true, the original client Host header is passed through to the backend instead of being rewritten to the backend's address + rewrite_redirects: + type: boolean + description: When true, Location headers in backend responses are rewritten to replace the backend address with the public-facing domain auth: $ref: '#/components/schemas/ReverseProxyAuthConfig' required: diff --git a/shared/management/http/api/types.gen.go b/shared/management/http/api/types.gen.go index 7dec2bb7a..eb51d823e 100644 --- a/shared/management/http/api/types.gen.go +++ b/shared/management/http/api/types.gen.go @@ -1992,6 +1992,9 @@ type ReverseProxy struct { // ProxyCluster The proxy cluster handling this reverse proxy (derived from domain) ProxyCluster *string `json:"proxy_cluster,omitempty"` + // RewriteRedirects When true, Location headers in backend responses are rewritten to replace the backend address with the public-facing domain + RewriteRedirects *bool `json:"rewrite_redirects,omitempty"` + // Targets List of target backends for this reverse proxy Targets []ReverseProxyTarget `json:"targets"` } @@ -2065,6 +2068,9 @@ type ReverseProxyRequest struct { // PassHostHeader When true, the original client Host header is passed through to the backend instead of being rewritten to the backend's address PassHostHeader *bool `json:"pass_host_header,omitempty"` + // RewriteRedirects When true, Location headers in backend responses are rewritten to replace the backend address with the public-facing domain + RewriteRedirects *bool `json:"rewrite_redirects,omitempty"` + // Targets List of target backends for this reverse proxy Targets []ReverseProxyTarget `json:"targets"` } diff --git a/shared/management/proto/proxy_service.pb.go b/shared/management/proto/proxy_service.pb.go index 4c3a8ae7d..7176c72c5 100644 --- a/shared/management/proto/proxy_service.pb.go +++ b/shared/management/proto/proxy_service.pb.go @@ -399,6 +399,9 @@ type ProxyMapping struct { // When true, the original Host header from the client request is passed // through to the backend instead of being rewritten to the backend's address. PassHostHeader bool `protobuf:"varint,8,opt,name=pass_host_header,json=passHostHeader,proto3" json:"pass_host_header,omitempty"` + // When true, Location headers in backend responses are rewritten to replace + // the backend address with the public-facing domain. + RewriteRedirects bool `protobuf:"varint,9,opt,name=rewrite_redirects,json=rewriteRedirects,proto3" json:"rewrite_redirects,omitempty"` } func (x *ProxyMapping) Reset() { @@ -489,6 +492,13 @@ func (x *ProxyMapping) GetPassHostHeader() bool { return false } +func (x *ProxyMapping) GetRewriteRedirects() bool { + if x != nil { + return x.RewriteRedirects + } + return false +} + // SendAccessLogRequest consists of one or more AccessLogs from a Proxy. type SendAccessLogRequest struct { state protoimpl.MessageState diff --git a/shared/management/proto/proxy_service.proto b/shared/management/proto/proxy_service.proto index 3bd46aa2c..ef5361f56 100644 --- a/shared/management/proto/proxy_service.proto +++ b/shared/management/proto/proxy_service.proto @@ -67,6 +67,9 @@ message ProxyMapping { // When true, the original Host header from the client request is passed // through to the backend instead of being rewritten to the backend's address. bool pass_host_header = 8; + // When true, Location headers in backend responses are rewritten to replace + // the backend address with the public-facing domain. + bool rewrite_redirects = 9; } // SendAccessLogRequest consists of one or more AccessLogs from a Proxy.