ipn: add support for HTTP Redirects

Adds a new Redirect field to HTTPHandler for serving HTTP redirects
from the Tailscale serve config. The redirect URL supports template
variables ${HOST} and ${REQUEST_URI} that are resolved per request.

Updates #11252
Updates #11330

Signed-off-by: Fernando Serboncini <fserb@tailscale.com>
k8s-ingress-redirect
Fernando Serboncini 2 months ago
parent 4673992b96
commit 130eb1f29c

@ -240,6 +240,7 @@ var _HTTPHandlerCloneNeedsRegeneration = HTTPHandler(struct {
Path string
Proxy string
Text string
Redirect string
}{})
// Clone makes a deep copy of WebServerConfig.

@ -891,11 +891,20 @@ func (v HTTPHandlerView) Proxy() string { return v.ж.Proxy }
// plaintext to serve (primarily for testing)
func (v HTTPHandlerView) Text() string { return v.ж.Text }
// Redirect is the target URL for an HTTP redirect.
// Redirects are always sent with HTTP 301 (Moved Permanently) status.
//
// The target URL supports the following expansion variables:
// - ${HOST}: replaced with the request's Host header value
// - ${REQUEST_URI}: replaced with the request's full URI (path and query string)
func (v HTTPHandlerView) Redirect() string { return v.ж.Redirect }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _HTTPHandlerViewNeedsRegeneration = HTTPHandler(struct {
Path string
Proxy string
Text string
Redirect string
}{})
// View returns a read-only view of WebServerConfig.

@ -940,6 +940,12 @@ func (b *LocalBackend) serveWebHandler(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, s)
return
}
if v := h.Redirect(); v != "" {
v = strings.ReplaceAll(v, "${HOST}", r.Host)
v = strings.ReplaceAll(v, "${REQUEST_URI}", r.RequestURI)
http.Redirect(w, r, v, http.StatusMovedPermanently)
return
}
if v := h.Path(); v != "" {
b.serveFileOrDirectory(w, r, v, mountPoint)
return

@ -1171,3 +1171,68 @@ func TestServeGRPCProxy(t *testing.T) {
})
}
}
func TestServeHTTPRedirect(t *testing.T) {
b := newTestBackend(t)
tests := []struct {
host string
path string
redirect string
reqURI string
wantLoc string
}{
{
host: "hardcoded-root",
path: "/",
redirect: "https://example.com/",
reqURI: "/old",
wantLoc: "https://example.com/",
},
{
host: "template-host-and-uri",
path: "/",
redirect: "https://${HOST}${REQUEST_URI}",
reqURI: "/path?foo=bar",
wantLoc: "https://template-host-and-uri/path?foo=bar",
},
}
for _, tt := range tests {
t.Run(tt.host, func(t *testing.T) {
conf := &ipn.ServeConfig{
Web: map[ipn.HostPort]*ipn.WebServerConfig{
ipn.HostPort(tt.host + ":80"): {
Handlers: map[string]*ipn.HTTPHandler{
tt.path: {Redirect: tt.redirect},
},
},
},
}
if err := b.SetServeConfig(conf, ""); err != nil {
t.Fatal(err)
}
req := &http.Request{
Host: tt.host,
URL: &url.URL{Path: tt.path},
RequestURI: tt.reqURI,
TLS: &tls.ConnectionState{ServerName: tt.host},
}
req = req.WithContext(serveHTTPContextKey.WithValue(req.Context(), &serveHTTPContext{
DestPort: 80,
SrcAddr: netip.MustParseAddrPort("1.2.3.4:1234"),
}))
w := httptest.NewRecorder()
b.serveWebHandler(w, req)
if w.Code != http.StatusMovedPermanently {
t.Errorf("got status %d, want %d", w.Code, http.StatusMovedPermanently)
}
if got := w.Header().Get("Location"); got != tt.wantLoc {
t.Errorf("got Location %q, want %q", got, tt.wantLoc)
}
})
}
}

@ -160,8 +160,16 @@ type HTTPHandler struct {
Text string `json:",omitempty"` // plaintext to serve (primarily for testing)
// Redirect is the target URL for an HTTP redirect.
// Redirects are always sent with HTTP 301 (Moved Permanently) status.
//
// The target URL supports the following expansion variables:
// - ${HOST}: replaced with the request's Host header value
// - ${REQUEST_URI}: replaced with the request's full URI (path and query string)
Redirect string `json:",omitempty"`
// TODO(bradfitz): bool to not enumerate directories? TTL on mapping for
// temporary ones? Error codes? Redirects?
// temporary ones? Error codes?
}
// WebHandlerExists reports whether if the ServeConfig Web handler exists for

Loading…
Cancel
Save