diff --git a/ipn/ipnlocal/serve.go b/ipn/ipnlocal/serve.go index 4d6055bbd..ef3de235a 100644 --- a/ipn/ipnlocal/serve.go +++ b/ipn/ipnlocal/serve.go @@ -25,6 +25,7 @@ import ( "net/url" "os" "path" + "regexp" "slices" "strconv" "strings" @@ -82,6 +83,17 @@ var ErrProxyToTailscaledSocket = errors.New("cannot proxy to tailscaled socket") var serveHTTPContextKey ctxkey.Key[*serveHTTPContext] +// handleInvalidCharsRe matches any character that is not a word character, hyphen, underscore, or dot. +var handleInvalidCharsRe = regexp.MustCompile(`[^\w\-_.]+`) + +// handelizeLogin converts a login name (e.g. "John.Doe@example.com") into a +// lowercase handle with only word characters, hyphens, underscores, and dots. +// It takes the local part before '@' and replaces invalid characters with '-'. +func handelizeLogin(login string) string { + localPart := strings.Split(login, "@")[0] + return handleInvalidCharsRe.ReplaceAllString(strings.ToLower(localPart), "-") +} + type serveHTTPContext struct { SrcAddr netip.AddrPort ForVIPService tailcfg.ServiceName // "" means local @@ -1021,6 +1033,7 @@ func (b *LocalBackend) addTailscaleIdentityHeaders(r *httputil.ProxyRequest) { // Clear any incoming values squatting in the headers. r.Out.Header.Del("Tailscale-User-Login") r.Out.Header.Del("Tailscale-User-Name") + r.Out.Header.Del("Tailscale-User-Handle") r.Out.Header.Del("Tailscale-User-Profile-Pic") r.Out.Header.Del("Tailscale-Funnel-Request") r.Out.Header.Del("Tailscale-Headers-Info") @@ -1044,6 +1057,7 @@ func (b *LocalBackend) addTailscaleIdentityHeaders(r *httputil.ProxyRequest) { } r.Out.Header.Set("Tailscale-User-Login", encTailscaleHeaderValue(user.LoginName)) r.Out.Header.Set("Tailscale-User-Name", encTailscaleHeaderValue(user.DisplayName)) + r.Out.Header.Set("Tailscale-User-Handle", encTailscaleHeaderValue(handelizeLogin(user.LoginName))) r.Out.Header.Set("Tailscale-User-Profile-Pic", user.ProfilePicURL) r.Out.Header.Set("Tailscale-Headers-Info", "https://tailscale.com/s/serve-headers") } diff --git a/ipn/ipnlocal/serve_test.go b/ipn/ipnlocal/serve_test.go index 6ee2181a0..846502c73 100644 --- a/ipn/ipnlocal/serve_test.go +++ b/ipn/ipnlocal/serve_test.go @@ -751,6 +751,7 @@ func TestServeHTTPProxyHeaders(t *testing.T) { {"X-Forwarded-For", "100.150.151.152"}, {"Tailscale-User-Login", "someone@example.com"}, {"Tailscale-User-Name", "Some One"}, + {"Tailscale-User-Handle", "someone"}, {"Tailscale-User-Profile-Pic", "https://example.com/photo.jpg"}, {"Tailscale-Headers-Info", "https://tailscale.com/s/serve-headers"}, }, @@ -763,6 +764,7 @@ func TestServeHTTPProxyHeaders(t *testing.T) { {"X-Forwarded-For", "100.150.151.153"}, {"Tailscale-User-Login", ""}, {"Tailscale-User-Name", ""}, + {"Tailscale-User-Handle", ""}, {"Tailscale-User-Profile-Pic", ""}, {"Tailscale-Headers-Info", ""}, }, @@ -775,6 +777,7 @@ func TestServeHTTPProxyHeaders(t *testing.T) { {"X-Forwarded-For", "100.160.161.162"}, {"Tailscale-User-Login", ""}, {"Tailscale-User-Name", ""}, + {"Tailscale-User-Handle", ""}, {"Tailscale-User-Profile-Pic", ""}, {"Tailscale-Headers-Info", ""}, }, @@ -806,6 +809,33 @@ func TestServeHTTPProxyHeaders(t *testing.T) { } } +func TestHandelizeLogin(t *testing.T) { + tests := []struct { + in string + want string + }{ + {"someone@example.com", "someone"}, + {"john.doe@example.com", "john.doe"}, + {"John.Doe@example.com", "john.doe"}, + {"john-doe@example.com", "john-doe"}, + {"john_doe@example.com", "john_doe"}, + {"john123@example.com", "john123"}, + {"john+doe@example.com", "john-doe"}, + {"john!#$doe@example.com", "john-doe"}, + {"john doe@example.com", "john-doe"}, + {"jöhn@example.com", "j-hn"}, + {"someone", "someone"}, + {"@example.com", ""}, + {"John.Doe-Test_123@example.com", "john.doe-test_123"}, + } + for _, tt := range tests { + got := handelizeLogin(tt.in) + if got != tt.want { + t.Errorf("handelizeLogin(%q) = %q, want %q", tt.in, got, tt.want) + } + } +} + func TestServeHTTPProxyGrantHeader(t *testing.T) { b := newTestBackend(t) @@ -891,6 +921,7 @@ func TestServeHTTPProxyGrantHeader(t *testing.T) { {"X-Forwarded-For", "100.150.151.152"}, {"Tailscale-User-Login", "someone@example.com"}, {"Tailscale-User-Name", "Some One"}, + {"Tailscale-User-Handle", "someone"}, {"Tailscale-User-Profile-Pic", "https://example.com/photo.jpg"}, {"Tailscale-Headers-Info", "https://tailscale.com/s/serve-headers"}, {"Tailscale-App-Capabilities", `{"example.com/cap/interesting":[{"role":"🐿"}]}`}, @@ -904,6 +935,7 @@ func TestServeHTTPProxyGrantHeader(t *testing.T) { {"X-Forwarded-For", "100.150.151.153"}, {"Tailscale-User-Login", ""}, {"Tailscale-User-Name", ""}, + {"Tailscale-User-Handle", ""}, {"Tailscale-User-Profile-Pic", ""}, {"Tailscale-Headers-Info", ""}, {"Tailscale-App-Capabilities", `{"example.com/cap/boring":[{"role":"Viewer"}]}`}, @@ -917,6 +949,7 @@ func TestServeHTTPProxyGrantHeader(t *testing.T) { {"X-Forwarded-For", "100.160.161.162"}, {"Tailscale-User-Login", ""}, {"Tailscale-User-Name", ""}, + {"Tailscale-User-Handle", ""}, {"Tailscale-User-Profile-Pic", ""}, {"Tailscale-Headers-Info", ""}, {"Tailscale-App-Capabilities", ""},