diff --git a/server/cmd/api/api/browser_location.go b/server/cmd/api/api/browser_location.go index 961978d5..be3c9a82 100644 --- a/server/cmd/api/api/browser_location.go +++ b/server/cmd/api/api/browser_location.go @@ -22,6 +22,8 @@ import ( const ( browserLocationApplyTimeout = 5 * time.Second defaultBrowserLocationStatePath = "/run/kernel/browser-location.json" + trustedControlPlaneHeader = "X-Kernel-Trusted-Control-Plane" + trustedControlPlaneHeaderValue = "1" ) type browserLocationBundle struct { @@ -231,7 +233,7 @@ func (s *ApiService) reconcileBrowserLocation(ctx context.Context, bundle browse components.TimeZone = true lastErr = s.withCDPClientTimeout(ctx, time.Second, func(cdpCtx context.Context, client *cdpclient.Client) error { languages := strings.Join(bundle.Languages, ",") - if err := client.SetBrowserLocation(cdpCtx, bundle.Locale, languages); err != nil { + if err := client.SetBrowserLocation(cdpCtx, bundle.Locale, languages, bundle.TimeZone); err != nil { return err } observed, err := client.GetBrowserLocation(cdpCtx) @@ -298,14 +300,9 @@ func resolvedLocalesMatch(expected string, observed cdpclient.BrowserLocation) b if err != nil { return false } - wantBase, _ := want.Base() for _, value := range []string{observed.DateTimeLocale, observed.NumberLocale, observed.CollatorLocale} { tag, err := language.Parse(value) - if err != nil { - return false - } - base, _ := tag.Base() - if base != wantBase { + if err != nil || tag.String() != want.String() { return false } } @@ -455,12 +452,15 @@ func (s *ApiService) BrowserLocationMetrics() metrics.BrowserLocationSnapshot { } // ResetBrowserLocationHTTP is the lease-authoritative epoch transition. Ordinary -// configure requests cannot change epochs. The instance JWT is not exposed on -// customer CDP or session APIs and binds the reset to this VM lease. +// configure requests cannot change epochs. Direct callers authenticate with the +// instance JWT. Metro-api may instead add the trusted control-plane marker after +// it verifies Kernel's internal token and removes any caller-supplied marker. func (s *ApiService) ResetBrowserLocationHTTP(w http.ResponseWriter, r *http.Request) { token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") expected := os.Getenv("KERNEL_INSTANCE_JWT") - if expected == "" || subtle.ConstantTimeCompare([]byte(token), []byte(expected)) != 1 { + instanceAuthorized := expected != "" && subtle.ConstantTimeCompare([]byte(token), []byte(expected)) == 1 + controlPlaneAuthorized := subtle.ConstantTimeCompare([]byte(r.Header.Get(trustedControlPlaneHeader)), []byte(trustedControlPlaneHeaderValue)) == 1 + if !instanceAuthorized && !controlPlaneAuthorized { http.Error(w, "unauthorized", http.StatusUnauthorized) return } diff --git a/server/cmd/api/api/browser_location_test.go b/server/cmd/api/api/browser_location_test.go index 5923f9d5..5054f804 100644 --- a/server/cmd/api/api/browser_location_test.go +++ b/server/cmd/api/api/browser_location_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/kernel/kernel-images/server/lib/cdpclient" "github.com/kernel/kernel-images/server/lib/devtoolsproxy" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -37,6 +38,19 @@ func TestValidateBrowserLocationBundle(t *testing.T) { } } +func TestResolvedLocalesMatchRequiresRegionalResolution(t *testing.T) { + assert.True(t, resolvedLocalesMatch("de-DE", cdpclient.BrowserLocation{ + DateTimeLocale: "de-DE", + NumberLocale: "de-DE", + CollatorLocale: "de-DE", + })) + assert.False(t, resolvedLocalesMatch("de-DE", cdpclient.BrowserLocation{ + DateTimeLocale: "de", + NumberLocale: "de-DE", + CollatorLocale: "de-DE", + })) +} + func TestBrowserLocationBundlesEqual(t *testing.T) { a := browserLocationBundle{Epoch: "lease", Generation: 1, TimeZone: "UTC", Locale: "en-US", Languages: []string{"en-US", "en"}} b := a @@ -151,6 +165,20 @@ func TestResetBrowserLocationHTTPRequiresInstanceIdentity(t *testing.T) { assert.Equal(t, "lease-a", service.browserLocationSnapshot().ActiveEpoch) } +func TestResetBrowserLocationHTTPAcceptsTrustedControlPlane(t *testing.T) { + service := newBrowserLocationStateService(t) + service.browserLocationValidate = func(context.Context, browserLocationBundle) error { return nil } + body := `{"previous_epoch":"","bundle":{"epoch":"lease-a","generation":1,"timezone":"UTC","locale":"en-US","languages":["en-US","en"]}}` + + request := httptest.NewRequest(http.MethodPost, "/internal/browser-location/reset", strings.NewReader(body)) + request.Header.Set(trustedControlPlaneHeader, trustedControlPlaneHeaderValue) + response := httptest.NewRecorder() + service.ResetBrowserLocationHTTP(response, request) + + assert.Equal(t, http.StatusAccepted, response.Code) + assert.Equal(t, "lease-a", service.browserLocationSnapshot().ActiveEpoch) +} + func TestBrowserLocationPersistenceFailureDoesNotChangeEpoch(t *testing.T) { service := newBrowserLocationStateService(t) t.Setenv("KERNEL_BROWSER_LOCATION_STATE_PATH", "/proc/kernel-browser-location-state") diff --git a/server/lib/cdpclient/cdpclient.go b/server/lib/cdpclient/cdpclient.go index f81654b5..37eb73ae 100644 --- a/server/lib/cdpclient/cdpclient.go +++ b/server/lib/cdpclient/cdpclient.go @@ -758,9 +758,9 @@ func (c *Client) ValidateBrowserLocation(ctx context.Context, locale string) (Br } // SetBrowserLocation updates Chromium's session-only locale and language defaults. -func (c *Client) SetBrowserLocation(ctx context.Context, locale, acceptLanguages string) error { +func (c *Client) SetBrowserLocation(ctx context.Context, locale, acceptLanguages, timezone string) error { _, err := c.Send(ctx, "Browser.setKernelBrowserLocation", map[string]string{ - "locale": locale, "acceptLanguages": acceptLanguages, + "locale": locale, "acceptLanguages": acceptLanguages, "timezone": timezone, }, "") if err != nil { return fmt.Errorf("Browser.setKernelBrowserLocation: %w", err) diff --git a/server/lib/cdpclient/cdpclient_test.go b/server/lib/cdpclient/cdpclient_test.go index 711842ce..4f3fc862 100644 --- a/server/lib/cdpclient/cdpclient_test.go +++ b/server/lib/cdpclient/cdpclient_test.go @@ -130,6 +130,7 @@ func (f *fakeCDP) handler(w http.ResponseWriter, r *http.Request) { _ = json.Unmarshal(req.Params, ¶ms) f.browserLocale = params["locale"] f.browserLanguages = params["acceptLanguages"] + f.browserTimezone = params["timezone"] result = map[string]any{} case "Browser.getKernelBrowserLocation": result = map[string]any{"locale": f.browserLocale, "acceptLanguages": f.browserLanguages, "timezone": f.browserTimezone, "dateTimeLocale": f.browserDateTimeLocale, "numberLocale": f.browserNumberLocale, "collatorLocale": f.browserCollatorLocale, "renderersConverged": true, "networkContextsConverged": true} @@ -693,7 +694,7 @@ func TestBrowserLocation(t *testing.T) { require.NoError(t, err) assert.Equal(t, BrowserLocationResolution{DateTimeLocale: "de", NumberLocale: "de", CollatorLocale: "de"}, resolution) - require.NoError(t, client.SetBrowserLocation(context.Background(), "de-DE", "de-DE,de")) + require.NoError(t, client.SetBrowserLocation(context.Background(), "de-DE", "de-DE,de", "Europe/Berlin")) location, err := client.GetBrowserLocation(context.Background()) require.NoError(t, err) assert.Equal(t, BrowserLocation{Locale: "de-DE", AcceptLanguages: "de-DE,de", TimeZone: "Europe/Berlin", DateTimeLocale: "de", NumberLocale: "de", CollatorLocale: "de", RenderersConverged: true, NetworkContextsConverged: true}, location)